From 160ade0a72af1206ef011178306a828531af1a9e Mon Sep 17 00:00:00 2001 From: mukund1403 <108778858+mukund1403@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:41:07 +0800 Subject: [PATCH 001/270] Update README.md --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..378b652464 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# Duke +# Split-liang (An app to help you split expenses with friends in a fun way!) {Give product intro here} From ef1fe424fa6fdf3d4d1f7b107b6beecf80f243f9 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 7 Mar 2024 16:35:11 +0800 Subject: [PATCH 002/270] Update name in AboutUs --- docs/AboutUs.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..3bc6ecaf96 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +Display | Name | Github Profile | Portfolio +--------|:--------:|:--------------:|:---------: +![](https://via.placeholder.com/100.png?text=Photo) | Hafiz | [Github](https://github.com/hafizuddin-a) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) ![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) ![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) From ef66b5b45b63bd32daff9062fa89766374a31af8 Mon Sep 17 00:00:00 2001 From: Cohii Date: Thu, 7 Mar 2024 16:37:42 +0800 Subject: [PATCH 003/270] Update Docs --- .idea/.gitignore | 8 ++++++++ .idea/misc.xml | 10 ++++++++++ .idea/vcs.xml | 7 +++++++ docs/AboutUs.md | 14 +++++++------- 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..13566b81b0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..b9d0bedbed --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..f467d6c788 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..11ec7ec318 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +Display | Name | Github Profile | Portfolio +--------|:-------------:|:--------------:|:---------: +![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Heng Junxiang | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) From 705a83bba6036ad80bf35eacf02c723b7a188228 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Thu, 7 Mar 2024 16:41:50 +0800 Subject: [PATCH 004/270] Change Doe John to Akshan --- docs/AboutUs.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..fc3473e18d 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: +Display | Name | Github Profile | Portfolio +--------|:--------:|:--------------:|:---------: ![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Akshan | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) ![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) ![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) From b511729bfe51357aac472984bc557f4654ea15ea Mon Sep 17 00:00:00 2001 From: Cohii Date: Fri, 15 Mar 2024 02:20:08 +0800 Subject: [PATCH 005/270] Create Balance class Created Balance class to show list of members the user owes money to. Class currently uses Hash Map to store user & balance value pairs - will update for use with other classes once available. --- src/main/java/Balance.java | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/Balance.java diff --git a/src/main/java/Balance.java b/src/main/java/Balance.java new file mode 100644 index 0000000000..706d5b61c3 --- /dev/null +++ b/src/main/java/Balance.java @@ -0,0 +1,37 @@ +import java.util.HashMap; +import java.util.Map; + +public class Balance { + protected String userName; + protected Map userList = new HashMap<>(); + + public Balance(String userName, Map userList) { + this.userName = userName; + this.userList = userList; + } + + public void printBalance() { + String firstLine = String.format("User %s's Balance List:", userName); + System.out.println(firstLine); + + for (Map.Entry entry : userList.entrySet()) { + String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); + System.out.println(balanceLine); + } + + System.out.println("End of Balance List"); + } + +// public static void main(String[] args) { +// Map userList = new HashMap<>(Map.of( +// "Shaoliang", 5.0f, +// "Avril", -5.0f, +// "Hafiz", 10.0f, +// "Mukund", -10.0f +// )); +// +// Balance balance = new Balance("Junxiang", userList); +// +// balance.printBalance(); +// } +} From 67665a9bd86122c7c4ec732b897cd915951e1b4b Mon Sep 17 00:00:00 2001 From: Cohii Date: Fri, 15 Mar 2024 02:27:25 +0800 Subject: [PATCH 006/270] Delete test comment Deleted testing code that causes commit to fail auto-testing code. --- src/main/java/Balance.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/Balance.java b/src/main/java/Balance.java index 706d5b61c3..dfb9a5757c 100644 --- a/src/main/java/Balance.java +++ b/src/main/java/Balance.java @@ -21,17 +21,4 @@ public void printBalance() { System.out.println("End of Balance List"); } - -// public static void main(String[] args) { -// Map userList = new HashMap<>(Map.of( -// "Shaoliang", 5.0f, -// "Avril", -5.0f, -// "Hafiz", 10.0f, -// "Mukund", -10.0f -// )); -// -// Balance balance = new Balance("Junxiang", userList); -// -// balance.printBalance(); -// } } From afdfe5c71a339fb4902ee225dc062d27af3fefc5 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Fri, 15 Mar 2024 09:43:05 +0800 Subject: [PATCH 007/270] Add basic expense adding --- src/main/java/seedu/duke/Duke.java | 15 +++++++++++++++ src/main/java/seedu/duke/ExpenseAdder.java | 12 ++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/main/java/seedu/duke/ExpenseAdder.java diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 5c74e68d59..1af5a7b151 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -17,5 +17,20 @@ public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("Hello " + in.nextLine()); + String userInput; + while(true){ + userInput = in.nextLine(); + if(userInput.equals("bye")){ + break; + } + if(userInput.startsWith("expense")){ + String[] removeKeyWord = userInput.split(" ",2); + String[] extractExpense = removeKeyWord[1].split(" ", 2); + String expense = extractExpense[0]; + int amount = Integer.parseInt(extractExpense[1]); + ExpenseAdder newExpense = new ExpenseAdder(expense,amount); + } + System.out.println(); + } } } diff --git a/src/main/java/seedu/duke/ExpenseAdder.java b/src/main/java/seedu/duke/ExpenseAdder.java new file mode 100644 index 0000000000..a1003dd726 --- /dev/null +++ b/src/main/java/seedu/duke/ExpenseAdder.java @@ -0,0 +1,12 @@ +package seedu.duke; + +public class ExpenseAdder { + private String name; + private int amount; + ExpenseAdder(String name, int amount){ + this.name = name; + this.amount = amount; + System.out.printf("Added new expense %d owed by %s",amount,name); + } + +} From 44e68c79cf335c8cddb72805e383f2a9db50ac30 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Tue, 19 Mar 2024 15:20:22 +0800 Subject: [PATCH 008/270] Add Group function --- src/main/java/seedu/duke/Duke.java | 16 ++++++++++++++++ src/main/java/seedu/duke/Group.java | 9 +++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/main/java/seedu/duke/Group.java diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 1af5a7b151..588521ab96 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,11 +1,14 @@ package seedu.duke; import java.util.Scanner; +import java.util.HashMap; public class Duke { /** * Main entry-point for the java.duke.Duke application. */ + private static final HashMap groups = new HashMap<>(); + public static void main(String[] args) { String logo = " ____ _ \n" + "| _ \\ _ _| | _____ \n" @@ -30,6 +33,19 @@ public static void main(String[] args) { int amount = Integer.parseInt(extractExpense[1]); ExpenseAdder newExpense = new ExpenseAdder(expense,amount); } + + if (userInput.startsWith("group")) { + String groupName = userInput.substring(6).trim(); + Group group = groups.get(groupName); + + if (group == null) { + group = new Group(groupName); + groups.put(groupName, group); + System.out.println("Created New Group: " + groupName); + } else { + System.out.println("Entering group: " + groupName); + } + } System.out.println(); } } diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java new file mode 100644 index 0000000000..57f540ec34 --- /dev/null +++ b/src/main/java/seedu/duke/Group.java @@ -0,0 +1,9 @@ +package seedu.duke; + +public class Group { + String name; + + public Group(String name) { + this.name = name; + } +} From 3f3f40dbb91cc00700e4eb0f40aa8ab633b6cef7 Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 19 Mar 2024 23:01:36 +0800 Subject: [PATCH 009/270] Create Parser class Create Parser class to read user input, skeleton code to call function for each different case --- src/main/java/seedu/duke/Duke.java | 30 ++++--------------- src/main/java/seedu/duke/Parser.java | 43 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 src/main/java/seedu/duke/Parser.java diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 588521ab96..059955ba98 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -19,34 +19,14 @@ public static void main(String[] args) { System.out.println("What is your name?"); Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - String userInput; while(true){ - userInput = in.nextLine(); - if(userInput.equals("bye")){ + Parser parser = new Parser(in.nextLine()); + try { + parser.handleUserInput(); + } catch (Parser.EndProgramException e) { break; } - if(userInput.startsWith("expense")){ - String[] removeKeyWord = userInput.split(" ",2); - String[] extractExpense = removeKeyWord[1].split(" ", 2); - String expense = extractExpense[0]; - int amount = Integer.parseInt(extractExpense[1]); - ExpenseAdder newExpense = new ExpenseAdder(expense,amount); - } - - if (userInput.startsWith("group")) { - String groupName = userInput.substring(6).trim(); - Group group = groups.get(groupName); - - if (group == null) { - group = new Group(groupName); - groups.put(groupName, group); - System.out.println("Created New Group: " + groupName); - } else { - System.out.println("Entering group: " + groupName); - } - } - System.out.println(); } + System.out.println("Goodbye!"); } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java new file mode 100644 index 0000000000..486d3935bb --- /dev/null +++ b/src/main/java/seedu/duke/Parser.java @@ -0,0 +1,43 @@ +package seedu.duke; + +public class Parser { + protected String userInput; + + public static class EndProgramException extends Exception { + + } + + public Parser(String userInput) { + this.userInput = userInput; + } + + public void handleUserInput() throws EndProgramException { + String[] tokens = userInput.split(" ", 2); + + String command = tokens[0].toLowerCase().trim(); + String argument = ""; + if (tokens.length > 1) { + argument = tokens[1].trim(); + } + + switch(command) { + case "bye": + throw new EndProgramException(); + case "help": + // Help code here + case "group": + // Group code here + case "member": + // Member code here + case "expense": + String[] extractExpense = argument.split(" ", 2); + String expense = extractExpense[0]; + int amount = Integer.parseInt(extractExpense[1]); + ExpenseAdder newExpense = new ExpenseAdder(expense,amount); + case "list": + // List code here + case "balance": + // Balance code here + } + } +} From c149d3f0328344ea20d0530a13823ac6555344be Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 19 Mar 2024 23:06:31 +0800 Subject: [PATCH 010/270] Edit Parser Class Added default clause to switch statement --- src/main/java/seedu/duke/Parser.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 486d3935bb..b6c923f879 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -38,6 +38,8 @@ public void handleUserInput() throws EndProgramException { // List code here case "balance": // Balance code here + default: + // Default clause } } } From aa0089fc3f530fd7f97ad68e92461d77802b06eb Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 19 Mar 2024 23:17:54 +0800 Subject: [PATCH 011/270] Add break statements Added missing break statements to switch statement. --- src/main/java/seedu/duke/Parser.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index b6c923f879..4485ee6843 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -25,21 +25,28 @@ public void handleUserInput() throws EndProgramException { throw new EndProgramException(); case "help": // Help code here + break; case "group": // Group code here + break; case "member": // Member code here + break; case "expense": String[] extractExpense = argument.split(" ", 2); String expense = extractExpense[0]; int amount = Integer.parseInt(extractExpense[1]); ExpenseAdder newExpense = new ExpenseAdder(expense,amount); + break; case "list": // List code here + break; case "balance": // Balance code here + break; default: // Default clause + break; } } } From 26009f2f900e866eed2f05e3fdcee1896c2d234a Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 19 Mar 2024 23:35:11 +0800 Subject: [PATCH 012/270] Fix Parser --- src/main/java/seedu/duke/Duke.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 059955ba98..b533c532f9 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -20,7 +20,9 @@ public static void main(String[] args) { Scanner in = new Scanner(System.in); while(true){ - Parser parser = new Parser(in.nextLine()); + String userInput = in.nextLine(); + Parser parser = new Parser(userInput); + try { parser.handleUserInput(); } catch (Parser.EndProgramException e) { From 138e2b7023dc29793debe44a139cf6e0d007338e Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 19 Mar 2024 23:42:50 +0800 Subject: [PATCH 013/270] Fix Parser Resolve NoSuchElementException --- src/main/java/seedu/duke/Duke.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index b533c532f9..72291c17b3 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -19,7 +19,8 @@ public static void main(String[] args) { System.out.println("What is your name?"); Scanner in = new Scanner(System.in); - while(true){ + + while(in.hasNextLine()){ String userInput = in.nextLine(); Parser parser = new Parser(userInput); From 04b4fd1cf772f91fee5d55874921f742b144dc08 Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 19 Mar 2024 23:58:41 +0800 Subject: [PATCH 014/270] Remove input text --- text-ui-test/EXPECTED.TXT | 8 -------- text-ui-test/input.txt | 1 - 2 files changed, 9 deletions(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..8b13789179 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| -What is your name? -Hello James Gosling diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..e69de29bb2 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +0,0 @@ -James Gosling \ No newline at end of file From 87176a9685b43639b7bd4cb2f038e66a91ba4e59 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:00:55 +0800 Subject: [PATCH 015/270] Remove blank line --- text-ui-test/EXPECTED.TXT | 1 - 1 file changed, 1 deletion(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 8b13789179..e69de29bb2 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1 +0,0 @@ - From 21bcc2b55eabdd4415ab2fbba2a8cfc6dab2fadd Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:06:13 +0800 Subject: [PATCH 016/270] Change input file --- text-ui-test/EXPECTED.TXT | 8 ++++++++ text-ui-test/input.txt | 1 + 2 files changed, 9 insertions(+) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index e69de29bb2..d8efbf1e35 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -0,0 +1,8 @@ +Hello from + ____ _ +| _ \ _ _| | _____ +| | | | | | | |/ / _ \ +| |_| | |_| | < __/ +|____/ \__,_|_|\_\___| + +What is your name? \ No newline at end of file diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index e69de29bb2..f6ec2e9f95 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -0,0 +1 @@ +James Gosling \ No newline at end of file From 62350be9e6921fa10a30fcc33291680b42632f1e Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:08:12 +0800 Subject: [PATCH 017/270] Added newline --- text-ui-test/EXPECTED.TXT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index d8efbf1e35..164c9bd415 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -5,4 +5,4 @@ Hello from | |_| | |_| | < __/ |____/ \__,_|_|\_\___| -What is your name? \ No newline at end of file +What is your name? From 1ee18bfb2981844565b6af8c43b3310403597c0b Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:10:58 +0800 Subject: [PATCH 018/270] Revert to original Revert to original greeting --- src/main/java/seedu/duke/Duke.java | 3 ++- text-ui-test/EXPECTED.TXT | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 72291c17b3..870a0f3a8a 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -19,7 +19,8 @@ public static void main(String[] args) { System.out.println("What is your name?"); Scanner in = new Scanner(System.in); - + System.out.println("Hello " + in.nextLine()); + while(in.hasNextLine()){ String userInput = in.nextLine(); Parser parser = new Parser(userInput); diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 164c9bd415..f0b2ec9dc5 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -6,3 +6,4 @@ Hello from |____/ \__,_|_|\_\___| What is your name? +Hello James Gosling \ No newline at end of file From 06d4048291251b827c13bee716d2fc82e0f7d010 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:14:25 +0800 Subject: [PATCH 019/270] Add Goodbye Add line of "Goodbye!" to end of expected --- text-ui-test/EXPECTED.TXT | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index f0b2ec9dc5..1e8b61a1b5 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -6,4 +6,5 @@ Hello from |____/ \__,_|_|\_\___| What is your name? -Hello James Gosling \ No newline at end of file +Hello James Gosling +Goodbye! \ No newline at end of file From 8bf174ef63bf1f806cdda95dd44100fa4419524d Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:17:17 +0800 Subject: [PATCH 020/270] Add newline --- text-ui-test/EXPECTED.TXT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 1e8b61a1b5..b1504aa394 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -7,4 +7,4 @@ Hello from What is your name? Hello James Gosling -Goodbye! \ No newline at end of file +Goodbye! From bf7aeb4c1c4ab433355effe35674dbfa6abd0d73 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 00:33:05 +0800 Subject: [PATCH 021/270] Use file difference checker --- text-ui-test/EXPECTED.TXT | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index b1504aa394..1d4e8312f6 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,6 +1,6 @@ Hello from - ____ _ -| _ \ _ _| | _____ + ____ _ +| _ \ _ _| | _____ | | | | | | | |/ / _ \ | |_| | |_| | < __/ |____/ \__,_|_|\_\___| From d0dc8daf815063687398885c80fa666c75e85bac Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 01:06:58 +0800 Subject: [PATCH 022/270] Edit classes Created new Member Class, edited Group and Expense Classes --- src/main/java/{ => seedu/duke}/Balance.java | 6 ++++-- .../seedu/duke/{ExpenseAdder.java => Expense.java} | 10 ++++++---- src/main/java/seedu/duke/Group.java | 10 ++++++++-- src/main/java/seedu/duke/Member.java | 9 +++++++++ src/main/java/seedu/duke/Parser.java | 4 ++-- 5 files changed, 29 insertions(+), 10 deletions(-) rename src/main/java/{ => seedu/duke}/Balance.java (77%) rename src/main/java/seedu/duke/{ExpenseAdder.java => Expense.java} (51%) create mode 100644 src/main/java/seedu/duke/Member.java diff --git a/src/main/java/Balance.java b/src/main/java/seedu/duke/Balance.java similarity index 77% rename from src/main/java/Balance.java rename to src/main/java/seedu/duke/Balance.java index dfb9a5757c..774c5ac431 100644 --- a/src/main/java/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -1,3 +1,5 @@ +package seedu.duke; + import java.util.HashMap; import java.util.Map; @@ -11,7 +13,7 @@ public Balance(String userName, Map userList) { } public void printBalance() { - String firstLine = String.format("User %s's Balance List:", userName); + String firstLine = String.format("User %s's seedu.duke.Balance List:", userName); System.out.println(firstLine); for (Map.Entry entry : userList.entrySet()) { @@ -19,6 +21,6 @@ public void printBalance() { System.out.println(balanceLine); } - System.out.println("End of Balance List"); + System.out.println("End of seedu.duke.Balance List"); } } diff --git a/src/main/java/seedu/duke/ExpenseAdder.java b/src/main/java/seedu/duke/Expense.java similarity index 51% rename from src/main/java/seedu/duke/ExpenseAdder.java rename to src/main/java/seedu/duke/Expense.java index a1003dd726..e08c0b86e3 100644 --- a/src/main/java/seedu/duke/ExpenseAdder.java +++ b/src/main/java/seedu/duke/Expense.java @@ -1,9 +1,11 @@ package seedu.duke; -public class ExpenseAdder { - private String name; - private int amount; - ExpenseAdder(String name, int amount){ +public class Expense { + protected String name; + protected float amount; + protected Member payingMember; + + Expense(String name, int amount){ this.name = name; this.amount = amount; System.out.printf("Added new expense %d owed by %s",amount,name); diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 57f540ec34..3dd864859a 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,9 +1,15 @@ package seedu.duke; +import java.util.ArrayList; + public class Group { - String name; + protected String name; + protected ArrayList members; + protected ArrayList expenses; - public Group(String name) { + public Group(String name, ArrayList members, ArrayList expenses) { this.name = name; + this.members = members; + this.expenses = expenses; } } diff --git a/src/main/java/seedu/duke/Member.java b/src/main/java/seedu/duke/Member.java new file mode 100644 index 0000000000..9c4b3d01e2 --- /dev/null +++ b/src/main/java/seedu/duke/Member.java @@ -0,0 +1,9 @@ +package seedu.duke; + +public class Member { + protected String userName; + + public Member(String userName) { + this.userName = userName; + } +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 4485ee6843..5e4ffecf2c 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -36,13 +36,13 @@ public void handleUserInput() throws EndProgramException { String[] extractExpense = argument.split(" ", 2); String expense = extractExpense[0]; int amount = Integer.parseInt(extractExpense[1]); - ExpenseAdder newExpense = new ExpenseAdder(expense,amount); + Expense newExpense = new Expense(expense,amount); break; case "list": // List code here break; case "balance": - // Balance code here + // seedu.duke.Balance code here break; default: // Default clause From dd69ac58a77b1a4e88e22ddd3335b2bd448c70d0 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 08:33:14 +0800 Subject: [PATCH 023/270] Add User class --- .idea/.gitignore | 2 ++ .idea/misc.xml | 2 +- .idea/vcs.xml | 1 - src/main/java/seedu/duke/User.java | 13 +++++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/main/java/seedu/duke/User.java diff --git a/.idea/.gitignore b/.idea/.gitignore index 13566b81b0..a9d7db9c0a 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.idea/misc.xml b/.idea/misc.xml index b9d0bedbed..5d9825616f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index f467d6c788..94a25f7f4c 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/src/main/java/seedu/duke/User.java b/src/main/java/seedu/duke/User.java new file mode 100644 index 0000000000..321e4af01f --- /dev/null +++ b/src/main/java/seedu/duke/User.java @@ -0,0 +1,13 @@ +package seedu.duke; + +public class User { + private String name; + + public User(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} From fed1d326d41df02f876f614228de9dcef2ece259 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 08:35:44 +0800 Subject: [PATCH 024/270] Rename name attribute to userName - namu attribute already used in Group class --- src/main/java/seedu/duke/User.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/seedu/duke/User.java b/src/main/java/seedu/duke/User.java index 321e4af01f..01b42dc971 100644 --- a/src/main/java/seedu/duke/User.java +++ b/src/main/java/seedu/duke/User.java @@ -1,13 +1,13 @@ package seedu.duke; public class User { - private String name; + private String userName; - public User(String name) { - this.name = name; + public User(String userName) { + this.userName = userName; } public String getName() { - return name; + return userName; } } From 3cb8d7c97db4bba33ba0406b621c584fee247496 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 20 Mar 2024 11:00:15 +0800 Subject: [PATCH 025/270] Add help menu and logo --- src/main/java/seedu/duke/Duke.java | 13 ++++++++----- src/main/java/seedu/duke/Help.java | 16 ++++++++++++++++ src/main/java/seedu/duke/Parser.java | 3 ++- 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 src/main/java/seedu/duke/Help.java diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 870a0f3a8a..20b1795185 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -10,11 +10,14 @@ public class Duke { private static final HashMap groups = new HashMap<>(); public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; + String logo = + ".------..------..------..------..------..------..------..------..------..------.\n" + + "|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. |\n" + + "| :/\\: || :/\\: || :/\\: || (\\/) || :/\\: || :/\\: || (\\/) || (\\/) || :(): || :/\\: |\n" + + "| :\\/: || (__) || (__) || :\\/: || (__) || (__) || :\\/: || :\\/: || ()() || :\\/: |\n" + + "| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G|\n" + + "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; + System.out.println("Hello from\n" + logo); System.out.println("What is your name?"); diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java new file mode 100644 index 0000000000..00282f0a14 --- /dev/null +++ b/src/main/java/seedu/duke/Help.java @@ -0,0 +1,16 @@ +package seedu.duke; + +public class Help { + private static final String prompt = + "Welcome\n" + + "help: Access help menu.\n" + + "group : Create or enter a group.\n" + + "member : Add a member to the group.\n" + + "expense /amount /paid /user /user ...: Add an expense.\n" + + "list: List all expenses in the group.\n" + + "balance : Show user's balance."; + + static void printHelp() { + System.out.println(prompt); + } +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 4485ee6843..b390cbc08b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -25,6 +25,7 @@ public void handleUserInput() throws EndProgramException { throw new EndProgramException(); case "help": // Help code here + Help.printHelp(); break; case "group": // Group code here @@ -36,7 +37,7 @@ public void handleUserInput() throws EndProgramException { String[] extractExpense = argument.split(" ", 2); String expense = extractExpense[0]; int amount = Integer.parseInt(extractExpense[1]); - ExpenseAdder newExpense = new ExpenseAdder(expense,amount); + ExpenseAdder newExpense = new ExpenseAdder(expense, amount); break; case "list": // List code here From 3816e8709e332092ee99fb777d8d7f02d5f6ca1d Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 20 Mar 2024 11:06:49 +0800 Subject: [PATCH 026/270] Correct indentation for logo --- src/main/java/seedu/duke/Duke.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 20b1795185..cd5cf8502e 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -11,12 +11,12 @@ public class Duke { public static void main(String[] args) { String logo = - ".------..------..------..------..------..------..------..------..------..------.\n" + - "|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. |\n" + - "| :/\\: || :/\\: || :/\\: || (\\/) || :/\\: || :/\\: || (\\/) || (\\/) || :(): || :/\\: |\n" + - "| :\\/: || (__) || (__) || :\\/: || (__) || (__) || :\\/: || :\\/: || ()() || :\\/: |\n" + - "| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G|\n" + - "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; + ".------..------..------..------..------..------..------..------..------..------.\n" + + "|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. |\n" + + "| :/\\: || :/\\: || :/\\: || (\\/) || :/\\: || :/\\: || (\\/) || (\\/) || :(): || :/\\: |\n" + + "| :\\/: || (__) || (__) || :\\/: || (__) || (__) || :\\/: || :\\/: || ()() || :\\/: |\n" + + "| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G|\n" + + "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; System.out.println("Hello from\n" + logo); System.out.println("What is your name?"); From eea9ab18513e3043453f145dd147f6d5801dbf46 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 20 Mar 2024 11:12:24 +0800 Subject: [PATCH 027/270] new test file --- text-ui-test/EXPECTED.TXT | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 1d4e8312f6..3119547477 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,10 @@ Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| +.------..------..------..------..------..------..------..------..------..------. +|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. | +| :/\: || :/\: || :/\: || (\/) || :/\: || :/\: || (\/) || (\/) || :(): || :/\: | +| :\/: || (__) || (__) || :\/: || (__) || (__) || :\/: || :\/: || ()() || :\/: | +| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G| +`------'`------'`------'`------'`------'`------'`------'`------'`------'`------' What is your name? Hello James Gosling From 68458c3fec6a985e4590e37952ab728505f7704d Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Wed, 20 Mar 2024 12:36:02 +0800 Subject: [PATCH 028/270] Add expense parsing and formatting for expense objects. --- .idea/vcs.xml | 1 - src/main/java/seedu/duke/Expense.java | 34 ++++++++++++++++++++++ src/main/java/seedu/duke/ExpenseAdder.java | 12 -------- src/main/java/seedu/duke/Parser.java | 10 ++++--- 4 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 src/main/java/seedu/duke/Expense.java delete mode 100644 src/main/java/seedu/duke/ExpenseAdder.java diff --git a/.idea/vcs.xml b/.idea/vcs.xml index f467d6c788..94a25f7f4c 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java new file mode 100644 index 0000000000..dc35093dfa --- /dev/null +++ b/src/main/java/seedu/duke/Expense.java @@ -0,0 +1,34 @@ +package seedu.duke; + + +import java.util.ArrayList; + +/** + * A class to add a new expense + */ +public class Expense { + private String payer_name; + private float total_amount; + + private ArrayList payees = new ArrayList<>(); + Expense(String payer_name, String total_amount, String[] payee_list){ + total_amount = removeWhitespaces(total_amount); + payer_name = removeWhitespaces(payer_name); + for(int i = 1; i < payee_list.length; i++){ + payees.add(removeWhitespaces(payee_list[i])); + } + this.payer_name = payer_name; + this.total_amount = Float.parseFloat(total_amount); + System.out.printf("Added new expense %.2f owed to %s by:",this.total_amount,this.payer_name); + for(String payee : payees){ + System.out.print(payee + ", "); + } + System.out.println(); + } + + private String removeWhitespaces(String item){ + String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); + return itemWithoutWhitespaces; + } + +} diff --git a/src/main/java/seedu/duke/ExpenseAdder.java b/src/main/java/seedu/duke/ExpenseAdder.java deleted file mode 100644 index a1003dd726..0000000000 --- a/src/main/java/seedu/duke/ExpenseAdder.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -public class ExpenseAdder { - private String name; - private int amount; - ExpenseAdder(String name, int amount){ - this.name = name; - this.amount = amount; - System.out.printf("Added new expense %d owed by %s",amount,name); - } - -} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 4485ee6843..965281e98b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -33,10 +33,12 @@ public void handleUserInput() throws EndProgramException { // Member code here break; case "expense": - String[] extractExpense = argument.split(" ", 2); - String expense = extractExpense[0]; - int amount = Integer.parseInt(extractExpense[1]); - ExpenseAdder newExpense = new ExpenseAdder(expense,amount); + String[] removeExpenseTag = argument.split("/amount"); + String[] extractAmount = removeExpenseTag[1].split("/paid"); + String amount = extractAmount[0]; + String[] extractPayer = extractAmount[1].split("/user"); + String payer_name = extractPayer[0]; + Expense newTransaction = new Expense(payer_name,amount,extractPayer); break; case "list": // List code here From 2c74ba2ff32ec22bb061face567c7378bb39e292 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Wed, 20 Mar 2024 13:01:40 +0800 Subject: [PATCH 029/270] Add Javadoc comments to expense Class. --- src/main/java/seedu/duke/Expense.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index dc35093dfa..46a86890c5 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -11,6 +11,13 @@ public class Expense { private float total_amount; private ArrayList payees = new ArrayList<>(); + + /** + * Creates the Expense object corresponding to one expense. + * @param payer_name : The person who paid for the expense + * @param total_amount : The total amount before dividing between members of the group + * @param payee_list : The list of people who owe money for this expense (The payer is included as index 0 and will not be added as a payee) + */ Expense(String payer_name, String total_amount, String[] payee_list){ total_amount = removeWhitespaces(total_amount); payer_name = removeWhitespaces(payer_name); From 037667ab1c4e2e308e1eeeed0a45ae0122df3ae8 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 14:21:48 +0800 Subject: [PATCH 030/270] Revert "Edit classes" This reverts commit d0dc8daf815063687398885c80fa666c75e85bac. --- src/main/java/{seedu/duke => }/Balance.java | 6 ++---- .../seedu/duke/{Expense.java => ExpenseAdder.java} | 10 ++++------ src/main/java/seedu/duke/Group.java | 10 ++-------- src/main/java/seedu/duke/Member.java | 9 --------- src/main/java/seedu/duke/Parser.java | 4 ++-- 5 files changed, 10 insertions(+), 29 deletions(-) rename src/main/java/{seedu/duke => }/Balance.java (77%) rename src/main/java/seedu/duke/{Expense.java => ExpenseAdder.java} (51%) delete mode 100644 src/main/java/seedu/duke/Member.java diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/Balance.java similarity index 77% rename from src/main/java/seedu/duke/Balance.java rename to src/main/java/Balance.java index 774c5ac431..dfb9a5757c 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/Balance.java @@ -1,5 +1,3 @@ -package seedu.duke; - import java.util.HashMap; import java.util.Map; @@ -13,7 +11,7 @@ public Balance(String userName, Map userList) { } public void printBalance() { - String firstLine = String.format("User %s's seedu.duke.Balance List:", userName); + String firstLine = String.format("User %s's Balance List:", userName); System.out.println(firstLine); for (Map.Entry entry : userList.entrySet()) { @@ -21,6 +19,6 @@ public void printBalance() { System.out.println(balanceLine); } - System.out.println("End of seedu.duke.Balance List"); + System.out.println("End of Balance List"); } } diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/ExpenseAdder.java similarity index 51% rename from src/main/java/seedu/duke/Expense.java rename to src/main/java/seedu/duke/ExpenseAdder.java index e08c0b86e3..a1003dd726 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/ExpenseAdder.java @@ -1,11 +1,9 @@ package seedu.duke; -public class Expense { - protected String name; - protected float amount; - protected Member payingMember; - - Expense(String name, int amount){ +public class ExpenseAdder { + private String name; + private int amount; + ExpenseAdder(String name, int amount){ this.name = name; this.amount = amount; System.out.printf("Added new expense %d owed by %s",amount,name); diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 3dd864859a..57f540ec34 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,15 +1,9 @@ package seedu.duke; -import java.util.ArrayList; - public class Group { - protected String name; - protected ArrayList members; - protected ArrayList expenses; + String name; - public Group(String name, ArrayList members, ArrayList expenses) { + public Group(String name) { this.name = name; - this.members = members; - this.expenses = expenses; } } diff --git a/src/main/java/seedu/duke/Member.java b/src/main/java/seedu/duke/Member.java deleted file mode 100644 index 9c4b3d01e2..0000000000 --- a/src/main/java/seedu/duke/Member.java +++ /dev/null @@ -1,9 +0,0 @@ -package seedu.duke; - -public class Member { - protected String userName; - - public Member(String userName) { - this.userName = userName; - } -} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 5e4ffecf2c..4485ee6843 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -36,13 +36,13 @@ public void handleUserInput() throws EndProgramException { String[] extractExpense = argument.split(" ", 2); String expense = extractExpense[0]; int amount = Integer.parseInt(extractExpense[1]); - Expense newExpense = new Expense(expense,amount); + ExpenseAdder newExpense = new ExpenseAdder(expense,amount); break; case "list": // List code here break; case "balance": - // seedu.duke.Balance code here + // Balance code here break; default: // Default clause From 15385603b0d21663300434b9bd87f4a773195833 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 15:14:56 +0800 Subject: [PATCH 031/270] Update Balance Class Moved Balance Class to seedu.duke Updated Balance Class constructor to work with User and Expense classes. --- src/main/java/Balance.java | 24 ---------- src/main/java/seedu/duke/Balance.java | 65 +++++++++++++++++++++++++++ src/main/java/seedu/duke/Duke.java | 1 + src/main/java/seedu/duke/Expense.java | 23 +++++++--- 4 files changed, 83 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/Balance.java create mode 100644 src/main/java/seedu/duke/Balance.java diff --git a/src/main/java/Balance.java b/src/main/java/Balance.java deleted file mode 100644 index dfb9a5757c..0000000000 --- a/src/main/java/Balance.java +++ /dev/null @@ -1,24 +0,0 @@ -import java.util.HashMap; -import java.util.Map; - -public class Balance { - protected String userName; - protected Map userList = new HashMap<>(); - - public Balance(String userName, Map userList) { - this.userName = userName; - this.userList = userList; - } - - public void printBalance() { - String firstLine = String.format("User %s's Balance List:", userName); - System.out.println(firstLine); - - for (Map.Entry entry : userList.entrySet()) { - String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); - System.out.println(balanceLine); - } - - System.out.println("End of Balance List"); - } -} diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java new file mode 100644 index 0000000000..8249dc74fc --- /dev/null +++ b/src/main/java/seedu/duke/Balance.java @@ -0,0 +1,65 @@ +package seedu.duke; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class Balance { + protected String userName; + protected Map balanceList; + + public Balance(String userName, Map userList) { + this.userName = userName; + this.balanceList = userList; + } + + public Balance(String userName, ArrayList expenses, ArrayList users) { + this.userName = userName; + this.balanceList = new HashMap<>(); + + // Populate balanceList with other Users from Group + for (User user : users){ + if(!user.getName().equals(userName)){ + balanceList.put(user.getName(), 0f); + } + } + + // Add Expenses to balanceList + for (Expense expense : expenses){ + addExpense(expense); + } + } + + private void addExpense(Expense expense) { + ArrayList payees = expense.getPayees(); + int numberOfUsers = payees.size() + 1; + Float amountPerUser = expense.getTotalAmount() / numberOfUsers; + + if(expense.getPayerName().equals(userName)){ + for(String payee : payees){ + Float currentOwed = balanceList.get(payee); + Float newOwed = currentOwed + amountPerUser; + + balanceList.put(payee, newOwed); + } + } else if (expense.getPayees().contains(userName)) { + String payerName = expense.getPayerName(); + Float currentOwed = balanceList.get(payerName); + Float newOwed = currentOwed - amountPerUser; + + balanceList.put(payerName, newOwed); + } + } + + public void printBalance() { + String firstLine = String.format("User %s's Balance List:", userName); + System.out.println(firstLine); + + for (Map.Entry entry : balanceList.entrySet()) { + String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); + System.out.println(balanceLine); + } + + System.out.println("End of Balance List"); + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index cd5cf8502e..c265002891 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,5 +1,6 @@ package seedu.duke; +import java.util.ArrayList; import java.util.Scanner; import java.util.HashMap; diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index dc35093dfa..b287eb3626 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -7,19 +7,30 @@ * A class to add a new expense */ public class Expense { - private String payer_name; - private float total_amount; - + private String payerName; + private float totalAmount; private ArrayList payees = new ArrayList<>(); + public String getPayerName() { + return payerName; + } + + public float getTotalAmount() { + return totalAmount; + } + + public ArrayList getPayees() { + return payees; + } + Expense(String payer_name, String total_amount, String[] payee_list){ total_amount = removeWhitespaces(total_amount); payer_name = removeWhitespaces(payer_name); for(int i = 1; i < payee_list.length; i++){ payees.add(removeWhitespaces(payee_list[i])); } - this.payer_name = payer_name; - this.total_amount = Float.parseFloat(total_amount); - System.out.printf("Added new expense %.2f owed to %s by:",this.total_amount,this.payer_name); + this.payerName = payer_name; + this.totalAmount = Float.parseFloat(total_amount); + System.out.printf("Added new expense %.2f owed to %s by:",this.totalAmount,this.payerName); for(String payee : payees){ System.out.print(payee + ", "); } From d6c0677f7aa6ef25c13171ba4c567ead5cdee6ee Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 15:19:01 +0800 Subject: [PATCH 032/270] Add test file --- src/test/java/seedu/duke/AddUserTest.java | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/test/java/seedu/duke/AddUserTest.java diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java new file mode 100644 index 0000000000..8ee6cdcd19 --- /dev/null +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -0,0 +1,31 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class AddUserTest { + + @Test + public void testUser() { + try { + User user = new User("John"); + assertEquals("John", user.getName()); + } catch (Exception e) { + fail(); + } + } + + @Test + public void testAddUserToGroup() { + try { + Group group = new Group("TestGroup"); + User user = new User("John"); + group.addUsers(user); + assertEquals("John", group.users.get(0).getName()); + } catch (Exception e) { + fail(); + } + } +} From 0e6d0729c9b7f6ed4c91f2486873202ba485faf0 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 15:28:37 +0800 Subject: [PATCH 033/270] Implement user ArrayList --- src/main/java/seedu/duke/Group.java | 9 +++++++++ src/test/java/seedu/duke/AddUserTest.java | 1 + 2 files changed, 10 insertions(+) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 57f540ec34..8947157f5f 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,9 +1,18 @@ package seedu.duke; +import java.util.ArrayList; + public class Group { String name; + protected ArrayList users; public Group(String name) { this.name = name; + this.users = new ArrayList<>(); + } + + public void addUsers(User user) { + users.add(user); + System.out.println("User " + user.getName() + " added to group " + this.name); } } diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 8ee6cdcd19..5ddffd69ca 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -28,4 +28,5 @@ public void testAddUserToGroup() { fail(); } } + } From e0441897b7b45e03f4eca416e3e4afeb190f4228 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 15:33:40 +0800 Subject: [PATCH 034/270] Add JUnit testing to Balance Class --- src/main/java/seedu/duke/Balance.java | 8 +++++ src/test/java/seedu/duke/BalanceTest.java | 38 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/test/java/seedu/duke/BalanceTest.java diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index 8249dc74fc..78a3ba957e 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -8,6 +8,14 @@ public class Balance { protected String userName; protected Map balanceList; + public String getUserName() { + return userName; + } + + public Map getBalanceList() { + return balanceList; + } + public Balance(String userName, Map userList) { this.userName = userName; this.balanceList = userList; diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java new file mode 100644 index 0000000000..9c17a6aefb --- /dev/null +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -0,0 +1,38 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; + +public class BalanceTest { + @Test + public void testConstructor(){ + ArrayList users = new ArrayList<>(); + users.add(new User("member1")); + users.add(new User("member2")); + users.add(new User("member3")); + + ArrayList expenses = new ArrayList<>(); + expenses.add(new Expense("member1", "15", new String[]{"", "member2", "member3"})); + expenses.add(new Expense("member2", "30", new String[]{"", "member1", "member3"})); + expenses.add(new Expense("member3", "100", new String[]{"", "member1"})); + + Balance member1Balance = new Balance("member1", expenses, users); + member1Balance.printBalance(); + Balance member2Balance = new Balance("member2", expenses, users); + member2Balance.printBalance(); + Balance member3Balance = new Balance("member3", expenses, users); + member3Balance.printBalance(); + + assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); + assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); + + assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); + assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); + + assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); + assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); + } + +} From 3cd6db224834bd252c6517720995a5b2fc7e1a2e Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 15:42:52 +0800 Subject: [PATCH 035/270] Fix checkstyle error Change order of cosntructor and methods in Expense and Balance class --- src/main/java/seedu/duke/Balance.java | 16 ++++++++-------- src/main/java/seedu/duke/Expense.java | 21 ++++++++++----------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index 78a3ba957e..abec08d31f 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -8,14 +8,6 @@ public class Balance { protected String userName; protected Map balanceList; - public String getUserName() { - return userName; - } - - public Map getBalanceList() { - return balanceList; - } - public Balance(String userName, Map userList) { this.userName = userName; this.balanceList = userList; @@ -38,6 +30,14 @@ public Balance(String userName, ArrayList expenses, ArrayList use } } + public String getUserName() { + return userName; + } + + public Map getBalanceList() { + return balanceList; + } + private void addExpense(Expense expense) { ArrayList payees = expense.getPayees(); int numberOfUsers = payees.size() + 1; diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index b287eb3626..952be0cbf3 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -10,17 +10,6 @@ public class Expense { private String payerName; private float totalAmount; private ArrayList payees = new ArrayList<>(); - public String getPayerName() { - return payerName; - } - - public float getTotalAmount() { - return totalAmount; - } - - public ArrayList getPayees() { - return payees; - } Expense(String payer_name, String total_amount, String[] payee_list){ total_amount = removeWhitespaces(total_amount); @@ -41,5 +30,15 @@ private String removeWhitespaces(String item){ String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } + public String getPayerName() { + return payerName; + } + + public float getTotalAmount() { + return totalAmount; + } + public ArrayList getPayees() { + return payees; + } } From f85a3ce09ec7dee289db22b5e626aaaac3d7f1dc Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 15:46:55 +0800 Subject: [PATCH 036/270] Fix checkstyle errors Change name field in Group to protected Remove unused ArrayList import in Duke Class --- src/main/java/seedu/duke/Duke.java | 1 - src/main/java/seedu/duke/Group.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index c265002891..cd5cf8502e 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,6 +1,5 @@ package seedu.duke; -import java.util.ArrayList; import java.util.Scanner; import java.util.HashMap; diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 57f540ec34..4ad0a86e6d 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,7 +1,7 @@ package seedu.duke; public class Group { - String name; + protected String name; public Group(String name) { this.name = name; From f043832eca2593c7e97938b6d8d5dc962860e527 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 15:52:35 +0800 Subject: [PATCH 037/270] Fix checkstyle error again Change name param in Group Class to protected --- src/main/java/seedu/duke/Group.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 8947157f5f..39f2b88c9b 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -3,7 +3,7 @@ import java.util.ArrayList; public class Group { - String name; + protected String name; protected ArrayList users; public Group(String name) { From f021b52e6318b67651d954ad4ecc22c0d8965d02 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Wed, 20 Mar 2024 18:15:27 +0800 Subject: [PATCH 038/270] Update GroupTest --- src/main/java/seedu/duke/Parser.java | 14 +++++++++++++- src/test/java/seedu/duke/GroupTest.java | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/test/java/seedu/duke/GroupTest.java diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 874a082541..bf39c300df 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,7 +1,10 @@ package seedu.duke; +import java.util.HashMap; public class Parser { protected String userInput; + private static final HashMap groups = new HashMap<>(); + public static class EndProgramException extends Exception { @@ -28,7 +31,16 @@ public void handleUserInput() throws EndProgramException { Help.printHelp(); break; case "group": - // Group code here + String groupName = userInput.substring(6).trim(); + Group group = groups.get(groupName); + + if (group == null) { + group = new Group(groupName); + groups.put(groupName, group); + System.out.println("Created New Group: " + groupName); + } else { + System.out.println("Entering group: " + groupName); + } break; case "member": // Member code here diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java new file mode 100644 index 0000000000..c1ab7c039b --- /dev/null +++ b/src/test/java/seedu/duke/GroupTest.java @@ -0,0 +1,14 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GroupTest { + @Test + public void groupTest() { + String expectedName = "GroupName"; + Group group = new Group(expectedName); + assertTrue(group.name.equals(expectedName), "Group name should be snt to the provided constructor argument."); + } +} \ No newline at end of file From 702c9333a158bd756d4addfd941b5356df2f58dd Mon Sep 17 00:00:00 2001 From: avrilgk Date: Wed, 20 Mar 2024 18:29:40 +0800 Subject: [PATCH 039/270] Update GroupTest and group parser --- src/main/java/seedu/duke/Group.java | 16 +++++++++++++++- src/main/java/seedu/duke/Parser.java | 12 ++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 8947157f5f..2c575062cf 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,5 +1,5 @@ package seedu.duke; - +import java.util.HashMap; import java.util.ArrayList; public class Group { @@ -11,6 +11,20 @@ public Group(String name) { this.users = new ArrayList<>(); } + private static HashMap groups = new HashMap<>(); + + public static Group getOrCreateGroup(String groupName) { + Group group = groups.get(groupName); + if (group == null) { + group = new Group(groupName); + groups.put(groupName, group); + System.out.println("Created New Group: " + groupName); + } else { + System.out.println("Entering Group: " + groupName); + } + return group; + } + public void addUsers(User user) { users.add(user); System.out.println("User " + user.getName() + " added to group " + this.name); diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index bf39c300df..061a882e7e 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -31,16 +31,8 @@ public void handleUserInput() throws EndProgramException { Help.printHelp(); break; case "group": - String groupName = userInput.substring(6).trim(); - Group group = groups.get(groupName); - - if (group == null) { - group = new Group(groupName); - groups.put(groupName, group); - System.out.println("Created New Group: " + groupName); - } else { - System.out.println("Entering group: " + groupName); - } + String groupName = argument; + Group.getOrCreateGroup(groupName); break; case "member": // Member code here From fb7a8e3cb84a96beef0feb635f4b1d8bf2c3dc59 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Wed, 20 Mar 2024 18:31:39 +0800 Subject: [PATCH 040/270] Update GroupTest and group parser --- src/main/java/seedu/duke/Group.java | 2 +- src/main/java/seedu/duke/Parser.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 2c575062cf..cd29180bc3 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -20,7 +20,7 @@ public static Group getOrCreateGroup(String groupName) { groups.put(groupName, group); System.out.println("Created New Group: " + groupName); } else { - System.out.println("Entering Group: " + groupName); + System.out.println("Entering group: " + groupName); } return group; } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 061a882e7e..226b54c6a6 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,10 +1,7 @@ package seedu.duke; -import java.util.HashMap; public class Parser { protected String userInput; - private static final HashMap groups = new HashMap<>(); - public static class EndProgramException extends Exception { From af9b4f457f9e9827fc6e2d28ebac9f34961ba6d1 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Wed, 20 Mar 2024 18:39:02 +0800 Subject: [PATCH 041/270] Add Javadoc comments and JUnit test for expense --- src/main/java/seedu/duke/Expense.java | 16 +++++++++++++++- src/test/java/seedu/duke/DukeTest.java | 1 + src/test/java/seedu/duke/ExpenseTest.java | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/test/java/seedu/duke/ExpenseTest.java diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index dc35093dfa..9adbd42e7e 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -11,6 +11,13 @@ public class Expense { private float total_amount; private ArrayList payees = new ArrayList<>(); + + /** + * Constructor to create new Expense + * @param payer_name : The name of the user who paid for the Expense + * @param total_amount : The total amount before being divided + * @param payee_list : String array of people who owe the payer money (Index 0 is the payer and will not be added to the payee list) + */ Expense(String payer_name, String total_amount, String[] payee_list){ total_amount = removeWhitespaces(total_amount); payer_name = removeWhitespaces(payer_name); @@ -20,12 +27,19 @@ public class Expense { this.payer_name = payer_name; this.total_amount = Float.parseFloat(total_amount); System.out.printf("Added new expense %.2f owed to %s by:",this.total_amount,this.payer_name); - for(String payee : payees){ + for(String payee : this.payees){ System.out.print(payee + ", "); } System.out.println(); } + /** + * + * @return : float showing the total amount before division + */ + public float getTotal_amount(){ + return this.total_amount; + } private String removeWhitespaces(String item){ String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java index 2dda5fd651..8037f5da3f 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/duke/DukeTest.java @@ -1,5 +1,6 @@ package seedu.duke; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java new file mode 100644 index 0000000000..230d212c7d --- /dev/null +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -0,0 +1,14 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class ExpenseTest{ + @Test + public void NewExpenseTest(){ + Expense testExpense = new Expense("Mukund"," 10 ", new String[]{"Mukund", " JX", "hehe"}); + assertEquals((float) 10, testExpense.getTotal_amount()); + } +} \ No newline at end of file From b7d896a70a53779913ee6a98707e3be755835d2a Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 20 Mar 2024 19:16:38 +0800 Subject: [PATCH 042/270] JUnit test for help functionality --- src/test/java/seedu/duke/HelpTest.java | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/test/java/seedu/duke/HelpTest.java diff --git a/src/test/java/seedu/duke/HelpTest.java b/src/test/java/seedu/duke/HelpTest.java new file mode 100644 index 0000000000..fa7cd281d2 --- /dev/null +++ b/src/test/java/seedu/duke/HelpTest.java @@ -0,0 +1,36 @@ +package seedu.duke; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +public class HelpTest { + private static final String prompt = + "Welcome\n" + + "help: Access help menu.\n" + + "group : Create or enter a group.\n" + + "member : Add a member to the group.\n" + + "expense /amount /paid /user /user ...: Add an expense.\n" + + "list: List all expenses in the group.\n" + + "balance : Show user's balance.\n"; + + @Test + public void dummyTest() { + assertEquals(2, 2); + } + @Test + public void testPrint() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + System.setOut(ps); + printHelp(); + String output = baos.toString(); + assertEquals(prompt, output); + } + + static void printHelp() { + System.out.print(prompt); + + } +} \ No newline at end of file From 1302735c7c6a0518bed4d05c44b173104142a280 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 20 Mar 2024 19:20:42 +0800 Subject: [PATCH 043/270] help test ends with a new line --- src/test/java/seedu/duke/HelpTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/seedu/duke/HelpTest.java b/src/test/java/seedu/duke/HelpTest.java index fa7cd281d2..7d0727f910 100644 --- a/src/test/java/seedu/duke/HelpTest.java +++ b/src/test/java/seedu/duke/HelpTest.java @@ -31,6 +31,5 @@ public void testPrint() { static void printHelp() { System.out.print(prompt); - } -} \ No newline at end of file +} From 8c56d833218ad13c505ed1776732c5fe9eedaa5d Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Wed, 20 Mar 2024 20:08:54 +0800 Subject: [PATCH 044/270] Add exception handling to Expenses. Modify parser to handle conversion of expense amount to float. --- src/main/java/seedu/duke/Duke.java | 2 ++ src/main/java/seedu/duke/Expense.java | 21 ++++++------ .../java/seedu/duke/ExpensesException.java | 11 +++++++ src/main/java/seedu/duke/Parser.java | 32 +++++++++++++++---- src/test/java/seedu/duke/BalanceTest.java | 6 ++-- src/test/java/seedu/duke/ExpenseTest.java | 4 +-- 6 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 src/main/java/seedu/duke/ExpensesException.java diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index cd5cf8502e..d7b2d204ed 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -32,6 +32,8 @@ public static void main(String[] args) { parser.handleUserInput(); } catch (Parser.EndProgramException e) { break; + } catch (ExpensesException e) { + System.out.println(e.getMessage()); } } System.out.println("Goodbye!"); diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 4e4bd1fce0..b397338154 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -13,18 +13,19 @@ public class Expense { /** * Constructor to create new Expense - * @param payer_name : The name of the user who paid for the Expense - * @param total_amount : The total amount before being divided - * @param payee_list : String array of people who owe the payer money (Index 0 is the payer and will not be added to the payee list) + * @param payerName : The name of the user who paid for the Expense + * @param totalAmount : The total amount before being divided + * @param payeeList : String array of people who owe the payer money + * (Index 0 is the payer and will not be added to the payee list) */ - Expense(String payer_name, String total_amount, String[] payee_list){ - total_amount = removeWhitespaces(total_amount); - payer_name = removeWhitespaces(payer_name); - for(int i = 1; i < payee_list.length; i++){ - payees.add(removeWhitespaces(payee_list[i])); + Expense(String payerName, float totalAmount, String[] payeeList){ + payerName = removeWhitespaces(payerName); + for(int i = 1; i < payeeList.length; i++){ + payees.add(removeWhitespaces(payeeList[i])); } - this.payerName = payer_name; - this.totalAmount = Float.parseFloat(total_amount); + this.payerName = payerName; + this.totalAmount = totalAmount; + System.out.printf("Added new expense %.2f owed to %s by:",this.totalAmount,this.payerName); for(String payee : payees){ System.out.print(payee + ", "); diff --git a/src/main/java/seedu/duke/ExpensesException.java b/src/main/java/seedu/duke/ExpensesException.java new file mode 100644 index 0000000000..aefcba6352 --- /dev/null +++ b/src/main/java/seedu/duke/ExpensesException.java @@ -0,0 +1,11 @@ +package seedu.duke; + +public class ExpensesException extends Exception{ + public ExpensesException(String s, Throwable err){ + super(s,err); + } + + public ExpensesException(String s){ + super(s); + } +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 874a082541..fc6f0bd414 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -11,7 +11,7 @@ public Parser(String userInput) { this.userInput = userInput; } - public void handleUserInput() throws EndProgramException { + public void handleUserInput() throws EndProgramException, ExpensesException { String[] tokens = userInput.split(" ", 2); String command = tokens[0].toLowerCase().trim(); @@ -34,12 +34,26 @@ public void handleUserInput() throws EndProgramException { // Member code here break; case "expense": - String[] removeExpenseTag = argument.split("/amount"); - String[] extractAmount = removeExpenseTag[1].split("/paid"); - String amount = extractAmount[0]; - String[] extractPayer = extractAmount[1].split("/user"); - String payer_name = extractPayer[0]; - Expense newTransaction = new Expense(payer_name,amount,extractPayer); + try{ + String[] removeExpenseTag = argument.split("/amount"); + if(removeExpenseTag.length == 1){ + throw new ExpensesException("No description for expenses! Add /amount /paid /user"); + } + String[] extractAmount = removeExpenseTag[1].split("/paid"); + String amount = extractAmount[0]; + amount = removeWhitespaces(amount); + + try{ + float totalAmount = Float.parseFloat(amount); + String[] extractPayer = extractAmount[1].split("/user"); + String payerName = extractPayer[0]; + Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); + } catch (NumberFormatException e){ + System.out.println("Re-enter expense with amount as a proper number."); + } + } catch(ArrayIndexOutOfBoundsException e){ + System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); + } break; case "list": // List code here @@ -52,4 +66,8 @@ public void handleUserInput() throws EndProgramException { break; } } + private String removeWhitespaces(String item){ + String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); + return itemWithoutWhitespaces; + } } diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 9c17a6aefb..f22882d2fd 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -14,9 +14,9 @@ public void testConstructor(){ users.add(new User("member3")); ArrayList expenses = new ArrayList<>(); - expenses.add(new Expense("member1", "15", new String[]{"", "member2", "member3"})); - expenses.add(new Expense("member2", "30", new String[]{"", "member1", "member3"})); - expenses.add(new Expense("member3", "100", new String[]{"", "member1"})); + expenses.add(new Expense("member1", 15, new String[]{"", "member2", "member3"})); + expenses.add(new Expense("member2", 30, new String[]{"", "member1", "member3"})); + expenses.add(new Expense("member3", 100, new String[]{"", "member1"})); Balance member1Balance = new Balance("member1", expenses, users); member1Balance.printBalance(); diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java index 230d212c7d..d9043b6523 100644 --- a/src/test/java/seedu/duke/ExpenseTest.java +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -8,7 +8,7 @@ class ExpenseTest{ @Test public void NewExpenseTest(){ - Expense testExpense = new Expense("Mukund"," 10 ", new String[]{"Mukund", " JX", "hehe"}); - assertEquals((float) 10, testExpense.getTotal_amount()); + Expense testExpense = new Expense("Mukund",10, new String[]{"Mukund", " JX", "hehe"}); + assertEquals((float) 10, testExpense.getTotalAmount()); } } \ No newline at end of file From 91b6860d6cf37905f94acf410428b4cd893292f3 Mon Sep 17 00:00:00 2001 From: mukund1403 <108778858+mukund1403@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:15:22 +0800 Subject: [PATCH 045/270] Fix errors causing checkstyle failures --- src/test/java/seedu/duke/ExpenseTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java index d9043b6523..9f0941ce56 100644 --- a/src/test/java/seedu/duke/ExpenseTest.java +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -7,8 +7,8 @@ class ExpenseTest{ @Test - public void NewExpenseTest(){ + public void newExpenseTest(){ Expense testExpense = new Expense("Mukund",10, new String[]{"Mukund", " JX", "hehe"}); assertEquals((float) 10, testExpense.getTotalAmount()); } -} \ No newline at end of file +} From 4533a88b75c9392a4605c0d7992fcbd634ba5cde Mon Sep 17 00:00:00 2001 From: mukund1403 <108778858+mukund1403@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:16:08 +0800 Subject: [PATCH 046/270] Update DukeTest.java --- src/test/java/seedu/duke/DukeTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java index 8037f5da3f..2dda5fd651 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/duke/DukeTest.java @@ -1,6 +1,5 @@ package seedu.duke; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; From 909acc0d4fe67e2da756d1bec57ba0a1f17d651a Mon Sep 17 00:00:00 2001 From: mukund1403 <108778858+mukund1403@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:59:31 +0800 Subject: [PATCH 047/270] Fix Javadoc comment in Expense.java Fixed incorrect style for Javadoc comment. --- src/main/java/seedu/duke/Expense.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index b397338154..e22f55d786 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -43,7 +43,6 @@ public String getPayerName() { } /** - * * @return : float showing the total amount before division */ public float getTotalAmount() { From 584c87bfc1e1d730631432d90843d25ebe4bed34 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 21:22:39 +0800 Subject: [PATCH 048/270] Fix checkstyle error --- src/main/java/seedu/duke/Group.java | 3 +-- src/test/java/seedu/duke/GroupTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index b2f0f6d2f0..ab9bffb0a9 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -5,14 +5,13 @@ public class Group { protected String name; protected ArrayList users; + public static final HashMap groups = new HashMap<>(); public Group(String name) { this.name = name; this.users = new ArrayList<>(); } - private static HashMap groups = new HashMap<>(); - public static Group getOrCreateGroup(String groupName) { Group group = groups.get(groupName); if (group == null) { diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index c1ab7c039b..4645b78445 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -11,4 +11,4 @@ public void groupTest() { Group group = new Group(expectedName); assertTrue(group.name.equals(expectedName), "Group name should be snt to the provided constructor argument."); } -} \ No newline at end of file +} From 068b5be51c711a85b5010d0aee47a1cb16e70137 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 21:31:29 +0800 Subject: [PATCH 049/270] Implement exception handling for adding user to group * name renamed to groupName in group class --- src/main/java/seedu/duke/Group.java | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index ab9bffb0a9..6071e5b40b 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -3,15 +3,23 @@ import java.util.ArrayList; public class Group { - protected String name; + protected String groupName; protected ArrayList users; public static final HashMap groups = new HashMap<>(); - public Group(String name) { - this.name = name; + public Group(String groupName) { + this.groupName = groupName; this.users = new ArrayList<>(); } + public ArrayList getUsers() { + return users; + } + + public String getGroupName() { + return groupName; + } + public static Group getOrCreateGroup(String groupName) { Group group = groups.get(groupName); if (group == null) { @@ -25,7 +33,15 @@ public static Group getOrCreateGroup(String groupName) { } public void addUsers(User user) { - users.add(user); - System.out.println("User " + user.getName() + " added to group " + this.name); + try { + for (User u : users) { + if (u.getName().equals(user.getName())) { + throw new Exception("User already exists in group."); + } + } + users.add(user); + } catch (Exception e) { + System.out.println(e.getMessage()); + } } } From f229ad3ea0838f855c7daf8f8d58ea7401b2b284 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 21:42:13 +0800 Subject: [PATCH 050/270] Add member logic to Parser class --- src/main/java/seedu/duke/Group.java | 2 +- src/main/java/seedu/duke/Parser.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 6071e5b40b..19a6c10ec2 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -3,9 +3,9 @@ import java.util.ArrayList; public class Group { + public static final HashMap groups = new HashMap<>(); protected String groupName; protected ArrayList users; - public static final HashMap groups = new HashMap<>(); public Group(String groupName) { this.groupName = groupName; diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 376d495bc1..ce0ccf0737 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -32,7 +32,12 @@ public void handleUserInput() throws EndProgramException, ExpensesException { Group.getOrCreateGroup(groupName); break; case "member": - // Member code here + String[] memberDetails = argument.split("/group"); + String memberName = memberDetails[0].trim(); + String groupNameForUser = memberDetails[1].trim(); + User newUser = new User(memberName); + Group group = Group.getOrCreateGroup(groupNameForUser); + group.addUsers(newUser); break; case "expense": try{ From 285e030be297ba031e480f1ec46355f39fcb1a89 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 21:43:37 +0800 Subject: [PATCH 051/270] Exception handling for addiing member in Parser --- src/main/java/seedu/duke/Parser.java | 22 ++++++++++++++++------ src/test/java/seedu/duke/GroupTest.java | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index ce0ccf0737..1aa2281d1c 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -32,12 +32,22 @@ public void handleUserInput() throws EndProgramException, ExpensesException { Group.getOrCreateGroup(groupName); break; case "member": - String[] memberDetails = argument.split("/group"); - String memberName = memberDetails[0].trim(); - String groupNameForUser = memberDetails[1].trim(); - User newUser = new User(memberName); - Group group = Group.getOrCreateGroup(groupNameForUser); - group.addUsers(newUser); + try { + String[] memberDetails = argument.split("/group"); + if(memberDetails.length == 1){ + throw new ExpensesException("No group name for user! Add /group "); + } + String memberName = memberDetails[0].trim(); + if (memberName.isEmpty()) { + throw new ExpensesException("No name for user! Add a name for the user."); + } + String groupNameForUser = memberDetails[1].trim(); + User newUser = new User(memberName); + Group group = Group.getOrCreateGroup(groupNameForUser); + group.addUsers(newUser); + } catch (Exception e){ + System.out.println(e.getMessage()); + } break; case "expense": try{ diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 4645b78445..94c80c120f 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -9,6 +9,6 @@ public class GroupTest { public void groupTest() { String expectedName = "GroupName"; Group group = new Group(expectedName); - assertTrue(group.name.equals(expectedName), "Group name should be snt to the provided constructor argument."); + assertTrue(group.groupName.equals(expectedName), "Group name should be snt to the provided constructor argument."); } } From fe15f63838961a56b7a726d829eb47bccccab8b7 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 21:46:57 +0800 Subject: [PATCH 052/270] Update help --- src/main/java/seedu/duke/Group.java | 3 ++- src/main/java/seedu/duke/Help.java | 2 +- src/main/java/seedu/duke/Parser.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 19a6c10ec2..fafab15b69 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -36,10 +36,11 @@ public void addUsers(User user) { try { for (User u : users) { if (u.getName().equals(user.getName())) { - throw new Exception("User already exists in group."); + throw new Exception("User already exists in group"); } } users.add(user); + System.out.println("Added " + user.getName() + " to " + groupName); } catch (Exception e) { System.out.println(e.getMessage()); } diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index 00282f0a14..6f7806225f 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -5,7 +5,7 @@ public class Help { "Welcome\n" + "help: Access help menu.\n" + "group : Create or enter a group.\n" + - "member : Add a member to the group.\n" + + "member /group : Add a member to the group.\n" + "expense /amount /paid /user /user ...: Add an expense.\n" + "list: List all expenses in the group.\n" + "balance : Show user's balance."; diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 1aa2281d1c..17608ace24 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -39,7 +39,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { } String memberName = memberDetails[0].trim(); if (memberName.isEmpty()) { - throw new ExpensesException("No name for user! Add a name for the user."); + throw new ExpensesException("No name for user! Add a name for the user"); } String groupNameForUser = memberDetails[1].trim(); User newUser = new User(memberName); From 034289683eb769be2e896f4b6fcc19d4036fd477 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 20 Mar 2024 21:51:51 +0800 Subject: [PATCH 053/270] Fix line more than 120 char in GroupTest --- src/test/java/seedu/duke/GroupTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 94c80c120f..f919787fd9 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -9,6 +9,6 @@ public class GroupTest { public void groupTest() { String expectedName = "GroupName"; Group group = new Group(expectedName); - assertTrue(group.groupName.equals(expectedName), "Group name should be snt to the provided constructor argument."); + assertTrue(group.groupName.equals(expectedName), "Group name is not the same as expected"); } } From bee024b8eecbe74fdf2e71c9f5b4ce3b0fe1c588 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 23:00:03 +0800 Subject: [PATCH 054/270] Integrate everything Got it to work "Freaking dumb" -Shaoliang --- src/main/java/seedu/duke/Expense.java | 11 +++++++++++ src/main/java/seedu/duke/Group.java | 14 ++++++++++++++ src/main/java/seedu/duke/Parser.java | 8 +++++--- text-ui-test/EXPECTED.TXT | 10 ++++++++++ text-ui-test/input.txt | 9 ++++++++- 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index e22f55d786..7239f29cbb 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -52,4 +52,15 @@ public float getTotalAmount() { public ArrayList getPayees() { return payees; } + + @Override + public String toString() { + StringBuilder expenseString = new StringBuilder(); + expenseString.append( + String.format("Payer: %s|Amount: %.2f|Payees: ", payerName, totalAmount)); + for(String payee : payees){ + expenseString.append(payee).append(" "); + } + return expenseString.toString().trim(); + } } diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index fafab15b69..55f5943a86 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -6,10 +6,12 @@ public class Group { public static final HashMap groups = new HashMap<>(); protected String groupName; protected ArrayList users; + protected ArrayList expenses; public Group(String groupName) { this.groupName = groupName; this.users = new ArrayList<>(); + this.expenses = new ArrayList<>(); } public ArrayList getUsers() { @@ -20,6 +22,8 @@ public String getGroupName() { return groupName; } + public ArrayList getExpenses() { return expenses; } + public static Group getOrCreateGroup(String groupName) { Group group = groups.get(groupName); if (group == null) { @@ -45,4 +49,14 @@ public void addUsers(User user) { System.out.println(e.getMessage()); } } + + public void addExpense(Expense expense){ + expenses.add(expense); + } + + public void printExpenses() { + for(Expense expense : expenses){ + System.out.println(expense); + } + } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 17608ace24..4d6d6a517c 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -2,6 +2,7 @@ public class Parser { protected String userInput; + public static Group currentGroup = new Group(""); public static class EndProgramException extends Exception { @@ -29,7 +30,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; case "group": String groupName = argument; - Group.getOrCreateGroup(groupName); + currentGroup = Group.getOrCreateGroup(groupName); break; case "member": try { @@ -64,6 +65,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { String[] extractPayer = extractAmount[1].split("/user"); String payerName = extractPayer[0]; Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); + currentGroup.addExpense(newTransaction); } catch (NumberFormatException e){ System.out.println("Re-enter expense with amount as a proper number."); } @@ -72,10 +74,10 @@ public void handleUserInput() throws EndProgramException, ExpensesException { } break; case "list": - // List code here + currentGroup.printExpenses(); break; case "balance": - // Balance code here + new Balance(argument, currentGroup.expenses, currentGroup.users).printBalance(); break; default: // Default clause diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 3119547477..e4458e79c7 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -8,4 +8,14 @@ Hello from What is your name? Hello James Gosling +Created New Group: group1 +Entering group: group1 +Added mem1 to group1 +Entering group: group1 +Added mem2 to group1 +Added new expense 10.00 owed to mem1 by:mem2, +Payer: mem1|Amount: 10.00|Payees: mem2 +User mem1's Balance List: + mem2 : 5.00 +End of Balance List Goodbye! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..70ff341c8b 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,8 @@ -James Gosling \ No newline at end of file +James Gosling +group group1 +member mem1 /group group1 +member mem2 /group group1 +expense /amount 10 /paid mem1 /user mem2 +list +balance mem1 +bye \ No newline at end of file From a309653bfdddd1e345e3b403b7f21ef784268029 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 23:03:04 +0800 Subject: [PATCH 055/270] Revert "Integrate everything" This reverts commit bee024b8eecbe74fdf2e71c9f5b4ce3b0fe1c588. --- src/main/java/seedu/duke/Expense.java | 11 ----------- src/main/java/seedu/duke/Group.java | 14 -------------- src/main/java/seedu/duke/Parser.java | 8 +++----- text-ui-test/EXPECTED.TXT | 10 ---------- text-ui-test/input.txt | 9 +-------- 5 files changed, 4 insertions(+), 48 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 7239f29cbb..e22f55d786 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -52,15 +52,4 @@ public float getTotalAmount() { public ArrayList getPayees() { return payees; } - - @Override - public String toString() { - StringBuilder expenseString = new StringBuilder(); - expenseString.append( - String.format("Payer: %s|Amount: %.2f|Payees: ", payerName, totalAmount)); - for(String payee : payees){ - expenseString.append(payee).append(" "); - } - return expenseString.toString().trim(); - } } diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 55f5943a86..fafab15b69 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -6,12 +6,10 @@ public class Group { public static final HashMap groups = new HashMap<>(); protected String groupName; protected ArrayList users; - protected ArrayList expenses; public Group(String groupName) { this.groupName = groupName; this.users = new ArrayList<>(); - this.expenses = new ArrayList<>(); } public ArrayList getUsers() { @@ -22,8 +20,6 @@ public String getGroupName() { return groupName; } - public ArrayList getExpenses() { return expenses; } - public static Group getOrCreateGroup(String groupName) { Group group = groups.get(groupName); if (group == null) { @@ -49,14 +45,4 @@ public void addUsers(User user) { System.out.println(e.getMessage()); } } - - public void addExpense(Expense expense){ - expenses.add(expense); - } - - public void printExpenses() { - for(Expense expense : expenses){ - System.out.println(expense); - } - } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 4d6d6a517c..17608ace24 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -2,7 +2,6 @@ public class Parser { protected String userInput; - public static Group currentGroup = new Group(""); public static class EndProgramException extends Exception { @@ -30,7 +29,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; case "group": String groupName = argument; - currentGroup = Group.getOrCreateGroup(groupName); + Group.getOrCreateGroup(groupName); break; case "member": try { @@ -65,7 +64,6 @@ public void handleUserInput() throws EndProgramException, ExpensesException { String[] extractPayer = extractAmount[1].split("/user"); String payerName = extractPayer[0]; Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); - currentGroup.addExpense(newTransaction); } catch (NumberFormatException e){ System.out.println("Re-enter expense with amount as a proper number."); } @@ -74,10 +72,10 @@ public void handleUserInput() throws EndProgramException, ExpensesException { } break; case "list": - currentGroup.printExpenses(); + // List code here break; case "balance": - new Balance(argument, currentGroup.expenses, currentGroup.users).printBalance(); + // Balance code here break; default: // Default clause diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index e4458e79c7..3119547477 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -8,14 +8,4 @@ Hello from What is your name? Hello James Gosling -Created New Group: group1 -Entering group: group1 -Added mem1 to group1 -Entering group: group1 -Added mem2 to group1 -Added new expense 10.00 owed to mem1 by:mem2, -Payer: mem1|Amount: 10.00|Payees: mem2 -User mem1's Balance List: - mem2 : 5.00 -End of Balance List Goodbye! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index 70ff341c8b..f6ec2e9f95 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1,8 +1 @@ -James Gosling -group group1 -member mem1 /group group1 -member mem2 /group group1 -expense /amount 10 /paid mem1 /user mem2 -list -balance mem1 -bye \ No newline at end of file +James Gosling \ No newline at end of file From 649d4157f01f73139efd574142ab6d430a42345e Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 20 Mar 2024 23:23:23 +0800 Subject: [PATCH 056/270] Fix checkstyle error variable declaration order --- src/main/java/seedu/duke/Parser.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 4d6d6a517c..1404869ee0 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,9 +1,8 @@ package seedu.duke; public class Parser { + protected static Group currentGroup = new Group(""); protected String userInput; - public static Group currentGroup = new Group(""); - public static class EndProgramException extends Exception { } From 9ff8858398809cfb39103922dc2906826e030502 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Fri, 22 Mar 2024 21:20:21 +0800 Subject: [PATCH 057/270] Update group functions and tests --- src/main/java/seedu/duke/Duke.java | 14 +-- src/main/java/seedu/duke/Group.java | 56 +++++++++-- src/main/java/seedu/duke/Help.java | 15 +-- src/main/java/seedu/duke/Parser.java | 119 +++++++++++++----------- src/test/java/seedu/duke/GroupTest.java | 31 +++++- 5 files changed, 155 insertions(+), 80 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index d7b2d204ed..4b7b333eaf 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -11,12 +11,12 @@ public class Duke { public static void main(String[] args) { String logo = - ".------..------..------..------..------..------..------..------..------..------.\n" + - "|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. |\n" + - "| :/\\: || :/\\: || :/\\: || (\\/) || :/\\: || :/\\: || (\\/) || (\\/) || :(): || :/\\: |\n" + - "| :\\/: || (__) || (__) || :\\/: || (__) || (__) || :\\/: || :\\/: || ()() || :\\/: |\n" + - "| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G|\n" + - "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; + ".------..------..------..------..------..------..------..------..------..------.\n" + + "|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. |\n" + + "| :/\\: || :/\\: || :/\\: || (\\/) || :/\\: || :/\\: || (\\/) || (\\/) || :(): || :/\\: |\n" + + "| :\\/: || (__) || (__) || :\\/: || (__) || (__) || :\\/: || :\\/: || ()() || :\\/: |\n" + + "| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G|\n" + + "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; System.out.println("Hello from\n" + logo); System.out.println("What is your name?"); @@ -38,4 +38,4 @@ public static void main(String[] args) { } System.out.println("Goodbye!"); } -} +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index fafab15b69..77b3d7438e 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,11 +1,13 @@ package seedu.duke; import java.util.HashMap; import java.util.ArrayList; +import java.util.Optional; public class Group { public static final HashMap groups = new HashMap<>(); protected String groupName; protected ArrayList users; + static String currentGroupName = null; public Group(String groupName) { this.groupName = groupName; @@ -20,15 +22,46 @@ public String getGroupName() { return groupName; } + /** + * Retrieves an existing group by its name or creates a new one if it does not exist. + * It ensures that a user cannot create or join a new group without exiting their current group. + * + * @param groupName The name of the group to get or create. + * @return The existing or newly created group. + * @throws IllegalStateException If trying to create or join a new group while already in another group. + */ public static Group getOrCreateGroup(String groupName) { - Group group = groups.get(groupName); - if (group == null) { - group = new Group(groupName); - groups.put(groupName, group); - System.out.println("Created New Group: " + groupName); - } else { - System.out.println("Entering group: " + groupName); + + // Check if user is accessing a group they are already in + if (currentGroupName != null && currentGroupName.equals(groupName)) { + System.out.println("You are in " + groupName); + return groups.get(groupName); } + + // Use of Optional to handle non-existing groups in hashmap + Optional optionalGroup = Optional.ofNullable(groups.get(groupName)); + + // Tracker for existing group + final boolean[] isNewGroupCreated = {false}; + + // If the user is in a different group, prevent them from creating or joining a new group. + Group group = optionalGroup.orElseGet(() -> { + if (currentGroupName != null && !currentGroupName.equals(groupName)) { + throw new IllegalStateException("Please exit the current group '" + currentGroupName + "' to create or join another group."); + } + Group newGroup = new Group(groupName); + groups.put(groupName, newGroup); + System.out.println(groupName + " created."); + isNewGroupCreated[0] = true; + return newGroup; + }); + + // If a new group was created, update currentGroupName to reflect this. + if (isNewGroupCreated[0]) { + currentGroupName = groupName; + } + + System.out.println("You are now in " + groupName); return group; } @@ -45,4 +78,13 @@ public void addUsers(User user) { System.out.println(e.getMessage()); } } + + public static void exitGroup() { + if (currentGroupName != null) { + System.out.println("You have exited " + currentGroupName + "."); + currentGroupName = null; + } else { + System.out.println("Please try again."); + } + } } diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index 6f7806225f..f84c733825 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -3,14 +3,15 @@ public class Help { private static final String prompt = "Welcome\n" + - "help: Access help menu.\n" + - "group : Create or enter a group.\n" + - "member /group : Add a member to the group.\n" + - "expense /amount /paid /user /user ...: Add an expense.\n" + - "list: List all expenses in the group.\n" + - "balance : Show user's balance."; + "help: Access help menu.\n" + + "create : Create a group.\n" + + "exit : Exit current group.\n" + + "member /group : Add a member to the group.\n" + + "expense /amount /paid /user /user ...: Add an expense.\n" + + "list: List all expenses in the group.\n" + + "balance : Show user's balance."; static void printHelp() { System.out.println(prompt); } -} +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index cd9da5de35..f2a8a5c0ac 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -22,69 +22,76 @@ public void handleUserInput() throws EndProgramException, ExpensesException { } switch(command) { - case "bye": - throw new EndProgramException(); - case "help": - // Help code here - Help.printHelp(); - break; - case "group": - String groupName = argument; - Group.getOrCreateGroup(groupName); - break; - case "member": - try { - String[] memberDetails = argument.split("/group"); - if(memberDetails.length == 1){ - throw new ExpensesException("No group name for user! Add /group "); + case "bye": + throw new EndProgramException(); + case "exit": + Group.exitGroup(); + break; + case "help": + // Help code here + Help.printHelp(); + break; + case "create": + try { + String groupName = argument; + Group.getOrCreateGroup(groupName); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); // Print the message instructing to exit current group. } - String memberName = memberDetails[0].trim(); - if (memberName.isEmpty()) { - throw new ExpensesException("No name for user! Add a name for the user"); + break; + case "member": + try { + String[] memberDetails = argument.split("/group"); + if(memberDetails.length == 1){ + throw new ExpensesException("No group name for user! Add /group "); + } + String memberName = memberDetails[0].trim(); + if (memberName.isEmpty()) { + throw new ExpensesException("No name for user! Add a name for the user"); + } + String groupNameForUser = memberDetails[1].trim(); + User newUser = new User(memberName); + Group group = Group.getOrCreateGroup(groupNameForUser); + group.addUsers(newUser); + } catch (Exception e){ + System.out.println(e.getMessage()); } - String groupNameForUser = memberDetails[1].trim(); - User newUser = new User(memberName); - Group group = Group.getOrCreateGroup(groupNameForUser); - group.addUsers(newUser); - } catch (Exception e){ - System.out.println(e.getMessage()); - } - break; - case "expense": - try{ - String[] removeExpenseTag = argument.split("/amount"); - if(removeExpenseTag.length == 1){ - throw new ExpensesException("No description for expenses! Add /amount /paid /user"); - } - String[] extractAmount = removeExpenseTag[1].split("/paid"); - String amount = extractAmount[0]; - amount = removeWhitespaces(amount); - + break; + case "expense": try{ - float totalAmount = Float.parseFloat(amount); - String[] extractPayer = extractAmount[1].split("/user"); - String payerName = extractPayer[0]; - Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); - } catch (NumberFormatException e){ - System.out.println("Re-enter expense with amount as a proper number."); + String[] removeExpenseTag = argument.split("/amount"); + if(removeExpenseTag.length == 1){ + throw new ExpensesException("No description for expenses! Add /amount /paid /user"); + } + String[] extractAmount = removeExpenseTag[1].split("/paid"); + String amount = extractAmount[0]; + amount = removeWhitespaces(amount); + + try{ + float totalAmount = Float.parseFloat(amount); + String[] extractPayer = extractAmount[1].split("/user"); + String payerName = extractPayer[0]; + Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); + } catch (NumberFormatException e){ + System.out.println("Re-enter expense with amount as a proper number."); + } + } catch(ArrayIndexOutOfBoundsException e){ + System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); } - } catch(ArrayIndexOutOfBoundsException e){ - System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); - } - break; - case "list": - // List code here - break; - case "balance": - // Balance code here - break; - default: - // Default clause - break; + break; + case "list": + // List code here + break; + case "balance": + // Balance code here + break; + default: + // Default clause + break; } } private String removeWhitespaces(String item){ String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } -} +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index f919787fd9..1c9f6ba6a4 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -1,14 +1,39 @@ package seedu.duke; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; - +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class GroupTest { @Test - public void groupTest() { + public void groupCreationTest() { String expectedName = "GroupName"; Group group = new Group(expectedName); - assertTrue(group.groupName.equals(expectedName), "Group name is not the same as expected"); + assertEquals(expectedName, group.getGroupName(), "Group name is not the same as expected"); + } + @Test + public void addUserToGroupTest() { + String groupName = "TestGroup"; + Group group = new Group(groupName); + User user = new User("TestUser"); + + group.addUsers(user); + + assertTrue(group.getUsers().contains(user), "User was not added to the group"); + } + @Test + public void getOrCreateGroupTest() { + String groupName = "NewGroup"; + Group.getOrCreateGroup(groupName); + + assertTrue(Group.groups.containsKey(groupName), "New group was not created"); + } + @Test + public void exitGroupTest() { + String groupName = "ExitingGroup"; + Group.getOrCreateGroup(groupName); + Group.exitGroup(); + Assertions.assertNull(Group.currentGroupName, "Did not successfully exit the group"); } } From bec302eb21ee890663c0d77d99defcee40e9fe55 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Fri, 22 Mar 2024 21:45:59 +0800 Subject: [PATCH 058/270] Fixed indentations --- src/main/java/seedu/duke/Balance.java | 10 +++++----- src/main/java/seedu/duke/Duke.java | 2 +- src/main/java/seedu/duke/Expense.java | 6 +++--- src/main/java/seedu/duke/ExpensesException.java | 2 +- src/main/java/seedu/duke/Parser.java | 17 ++++++++--------- src/test/java/seedu/duke/BalanceTest.java | 2 +- src/test/java/seedu/duke/ExpenseTest.java | 2 +- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index abec08d31f..e15600edf1 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -18,14 +18,14 @@ public Balance(String userName, ArrayList expenses, ArrayList use this.balanceList = new HashMap<>(); // Populate balanceList with other Users from Group - for (User user : users){ - if(!user.getName().equals(userName)){ + for (User user : users) { + if(!user.getName().equals(userName)) { balanceList.put(user.getName(), 0f); } } // Add Expenses to balanceList - for (Expense expense : expenses){ + for (Expense expense : expenses) { addExpense(expense); } } @@ -43,8 +43,8 @@ private void addExpense(Expense expense) { int numberOfUsers = payees.size() + 1; Float amountPerUser = expense.getTotalAmount() / numberOfUsers; - if(expense.getPayerName().equals(userName)){ - for(String payee : payees){ + if(expense.getPayerName().equals(userName)) { + for(String payee : payees) { Float currentOwed = balanceList.get(payee); Float newOwed = currentOwed + amountPerUser; diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 4b7b333eaf..cdb31975eb 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -24,7 +24,7 @@ public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("Hello " + in.nextLine()); - while(in.hasNextLine()){ + while(in.hasNextLine()) { String userInput = in.nextLine(); Parser parser = new Parser(userInput); diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index e22f55d786..f76307543d 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -18,7 +18,7 @@ public class Expense { * @param payeeList : String array of people who owe the payer money * (Index 0 is the payer and will not be added to the payee list) */ - Expense(String payerName, float totalAmount, String[] payeeList){ + Expense(String payerName, float totalAmount, String[] payeeList) { payerName = removeWhitespaces(payerName); for(int i = 1; i < payeeList.length; i++){ payees.add(removeWhitespaces(payeeList[i])); @@ -27,14 +27,14 @@ public class Expense { this.totalAmount = totalAmount; System.out.printf("Added new expense %.2f owed to %s by:",this.totalAmount,this.payerName); - for(String payee : payees){ + for(String payee : payees) { System.out.print(payee + ", "); } System.out.println(); } - private String removeWhitespaces(String item){ + private String removeWhitespaces(String item) { String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } diff --git a/src/main/java/seedu/duke/ExpensesException.java b/src/main/java/seedu/duke/ExpensesException.java index aefcba6352..5fee696bb3 100644 --- a/src/main/java/seedu/duke/ExpensesException.java +++ b/src/main/java/seedu/duke/ExpensesException.java @@ -1,6 +1,6 @@ package seedu.duke; -public class ExpensesException extends Exception{ +public class ExpensesException extends Exception { public ExpensesException(String s, Throwable err){ super(s,err); } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index f2a8a5c0ac..caa652d34d 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,7 +1,6 @@ package seedu.duke; public class Parser { - protected static Group currentGroup = new Group(""); protected String userInput; public static class EndProgramException extends Exception { @@ -42,7 +41,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "member": try { String[] memberDetails = argument.split("/group"); - if(memberDetails.length == 1){ + if(memberDetails.length == 1) { throw new ExpensesException("No group name for user! Add /group "); } String memberName = memberDetails[0].trim(); @@ -53,29 +52,29 @@ public void handleUserInput() throws EndProgramException, ExpensesException { User newUser = new User(memberName); Group group = Group.getOrCreateGroup(groupNameForUser); group.addUsers(newUser); - } catch (Exception e){ + } catch (Exception e) { System.out.println(e.getMessage()); } break; case "expense": - try{ + try { String[] removeExpenseTag = argument.split("/amount"); - if(removeExpenseTag.length == 1){ + if(removeExpenseTag.length == 1) { throw new ExpensesException("No description for expenses! Add /amount /paid /user"); } String[] extractAmount = removeExpenseTag[1].split("/paid"); String amount = extractAmount[0]; amount = removeWhitespaces(amount); - try{ + try { float totalAmount = Float.parseFloat(amount); String[] extractPayer = extractAmount[1].split("/user"); String payerName = extractPayer[0]; Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); - } catch (NumberFormatException e){ + } catch (NumberFormatException e) { System.out.println("Re-enter expense with amount as a proper number."); } - } catch(ArrayIndexOutOfBoundsException e){ + } catch(ArrayIndexOutOfBoundsException e) { System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); } break; @@ -90,7 +89,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; } } - private String removeWhitespaces(String item){ + private String removeWhitespaces(String item) { String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index f22882d2fd..9eaa541683 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -7,7 +7,7 @@ public class BalanceTest { @Test - public void testConstructor(){ + public void testConstructor() { ArrayList users = new ArrayList<>(); users.add(new User("member1")); users.add(new User("member2")); diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java index 9f0941ce56..b0d1e0b51a 100644 --- a/src/test/java/seedu/duke/ExpenseTest.java +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -7,7 +7,7 @@ class ExpenseTest{ @Test - public void newExpenseTest(){ + public void newExpenseTest() { Expense testExpense = new Expense("Mukund",10, new String[]{"Mukund", " JX", "hehe"}); assertEquals((float) 10, testExpense.getTotalAmount()); } From 37c8a9006e254d317d84d41a0e2ea29842de96ab Mon Sep 17 00:00:00 2001 From: avrilgk Date: Fri, 22 Mar 2024 21:46:30 +0800 Subject: [PATCH 059/270] Fixed indentations --- src/main/java/seedu/duke/Duke.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index cdb31975eb..010c2dd780 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -7,8 +7,6 @@ public class Duke { /** * Main entry-point for the java.duke.Duke application. */ - private static final HashMap groups = new HashMap<>(); - public static void main(String[] args) { String logo = ".------..------..------..------..------..------..------..------..------..------.\n" + From f333e227938a0ab72c9e4ae8b6d01ed2e57c0e04 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Fri, 22 Mar 2024 22:49:03 +0800 Subject: [PATCH 060/270] Fix checkstyle error * end file with newline * static variables before instance variables * line less than 120 char * case inline with switch for switch case --- src/main/java/seedu/duke/Duke.java | 3 +- src/main/java/seedu/duke/Group.java | 5 +- src/main/java/seedu/duke/Help.java | 2 +- src/main/java/seedu/duke/Parser.java | 128 +++++++++++++-------------- 4 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 010c2dd780..4ce8030ef2 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,7 +1,6 @@ package seedu.duke; import java.util.Scanner; -import java.util.HashMap; public class Duke { /** @@ -36,4 +35,4 @@ public static void main(String[] args) { } System.out.println("Goodbye!"); } -} \ No newline at end of file +} diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 77b3d7438e..0b70df33e5 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -5,9 +5,9 @@ public class Group { public static final HashMap groups = new HashMap<>(); + public static String currentGroupName = null; protected String groupName; protected ArrayList users; - static String currentGroupName = null; public Group(String groupName) { this.groupName = groupName; @@ -47,7 +47,8 @@ public static Group getOrCreateGroup(String groupName) { // If the user is in a different group, prevent them from creating or joining a new group. Group group = optionalGroup.orElseGet(() -> { if (currentGroupName != null && !currentGroupName.equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroupName + "' to create or join another group."); + throw new IllegalStateException("Please exit the current group '" + currentGroupName + + "' to create or join another group."); } Group newGroup = new Group(groupName); groups.put(groupName, newGroup); diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index f84c733825..dfd04bfe70 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -14,4 +14,4 @@ public class Help { static void printHelp() { System.out.println(prompt); } -} \ No newline at end of file +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index caa652d34d..ded8075b43 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -20,77 +20,77 @@ public void handleUserInput() throws EndProgramException, ExpensesException { argument = tokens[1].trim(); } - switch(command) { - case "bye": - throw new EndProgramException(); - case "exit": - Group.exitGroup(); - break; - case "help": - // Help code here - Help.printHelp(); - break; - case "create": - try { - String groupName = argument; - Group.getOrCreateGroup(groupName); - } catch (IllegalStateException e) { - System.out.println(e.getMessage()); // Print the message instructing to exit current group. + switch (command) { + case "bye": + throw new EndProgramException(); + case "exit": + Group.exitGroup(); + break; + case "help": + // Help code here + Help.printHelp(); + break; + case "create": + try { + String groupName = argument; + Group.getOrCreateGroup(groupName); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); // Print the message instructing to exit current group. + } + break; + case "member": + try { + String[] memberDetails = argument.split("/group"); + if (memberDetails.length == 1) { + throw new ExpensesException("No group name for user! Add /group "); } - break; - case "member": - try { - String[] memberDetails = argument.split("/group"); - if(memberDetails.length == 1) { - throw new ExpensesException("No group name for user! Add /group "); - } - String memberName = memberDetails[0].trim(); - if (memberName.isEmpty()) { - throw new ExpensesException("No name for user! Add a name for the user"); - } - String groupNameForUser = memberDetails[1].trim(); - User newUser = new User(memberName); - Group group = Group.getOrCreateGroup(groupNameForUser); - group.addUsers(newUser); - } catch (Exception e) { - System.out.println(e.getMessage()); + String memberName = memberDetails[0].trim(); + if (memberName.isEmpty()) { + throw new ExpensesException("No name for user! Add a name for the user"); } - break; - case "expense": - try { - String[] removeExpenseTag = argument.split("/amount"); - if(removeExpenseTag.length == 1) { - throw new ExpensesException("No description for expenses! Add /amount /paid /user"); - } - String[] extractAmount = removeExpenseTag[1].split("/paid"); - String amount = extractAmount[0]; - amount = removeWhitespaces(amount); + String groupNameForUser = memberDetails[1].trim(); + User newUser = new User(memberName); + Group group = Group.getOrCreateGroup(groupNameForUser); + group.addUsers(newUser); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + break; + case "expense": + try { + String[] removeExpenseTag = argument.split("/amount"); + if (removeExpenseTag.length == 1) { + throw new ExpensesException("No description for expenses! Add /amount /paid /user"); + } + String[] extractAmount = removeExpenseTag[1].split("/paid"); + String amount = extractAmount[0]; + amount = removeWhitespaces(amount); - try { - float totalAmount = Float.parseFloat(amount); - String[] extractPayer = extractAmount[1].split("/user"); - String payerName = extractPayer[0]; - Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); - } catch (NumberFormatException e) { - System.out.println("Re-enter expense with amount as a proper number."); - } - } catch(ArrayIndexOutOfBoundsException e) { - System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); + try { + float totalAmount = Float.parseFloat(amount); + String[] extractPayer = extractAmount[1].split("/user"); + String payerName = extractPayer[0]; + Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); + } catch (NumberFormatException e) { + System.out.println("Re-enter expense with amount as a proper number."); } - break; - case "list": - // List code here - break; - case "balance": - // Balance code here - break; - default: - // Default clause - break; + } catch(ArrayIndexOutOfBoundsException e) { + System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); + } + break; + case "list": + // List code here + break; + case "balance": + // Balance code here + break; + default: + // Default clause + break; } } private String removeWhitespaces(String item) { String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } -} \ No newline at end of file +} From 4a34bd96ea3fade9163dc1fd38c1c95375bf5403 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 23 Mar 2024 00:18:02 +0800 Subject: [PATCH 061/270] Add parser add member logic method --- src/main/java/seedu/duke/Group.java | 23 ++++++++++++----------- src/main/java/seedu/duke/Parser.java | 16 ---------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 0b70df33e5..17bf73b8ef 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -66,18 +66,19 @@ public static Group getOrCreateGroup(String groupName) { return group; } - public void addUsers(User user) { - try { - for (User u : users) { - if (u.getName().equals(user.getName())) { - throw new Exception("User already exists in group"); - } - } - users.add(user); - System.out.println("Added " + user.getName() + " to " + groupName); - } catch (Exception e) { - System.out.println(e.getMessage()); + public Group parseAddMember(String argument) { + if (currentGroupName == null) { + throw new IllegalStateException("Please create or join a group first."); + } + String[] tokens = argument.split(" "); + if (tokens.length == 0) { + throw new IllegalArgumentException("Please enter a name for the member."); } + String memberName = tokens[0]; + User newMember = new User(memberName); + users.add(newMember); + System.out.println(memberName + " has been added to " + currentGroupName + "."); + return this; } public static void exitGroup() { diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index ded8075b43..cf59b7cf5b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -39,22 +39,6 @@ public void handleUserInput() throws EndProgramException, ExpensesException { } break; case "member": - try { - String[] memberDetails = argument.split("/group"); - if (memberDetails.length == 1) { - throw new ExpensesException("No group name for user! Add /group "); - } - String memberName = memberDetails[0].trim(); - if (memberName.isEmpty()) { - throw new ExpensesException("No name for user! Add a name for the user"); - } - String groupNameForUser = memberDetails[1].trim(); - User newUser = new User(memberName); - Group group = Group.getOrCreateGroup(groupNameForUser); - group.addUsers(newUser); - } catch (Exception e) { - System.out.println(e.getMessage()); - } break; case "expense": try { From 1496657778c7e203c8e9f74381ddd54b1541fd8c Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 23 Mar 2024 00:22:11 +0800 Subject: [PATCH 062/270] Update help --- src/main/java/seedu/duke/Help.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index dfd04bfe70..515cbadc7d 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -6,7 +6,7 @@ public class Help { "help: Access help menu.\n" + "create : Create a group.\n" + "exit : Exit current group.\n" + - "member /group : Add a member to the group.\n" + + "member : Add a member to the group.\n" + "expense /amount /paid /user /user ...: Add an expense.\n" + "list: List all expenses in the group.\n" + "balance : Show user's balance."; From a401749d116c8d9856255b9772e4218b7c43fb82 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 24 Mar 2024 03:47:53 +0800 Subject: [PATCH 063/270] Refactor getOrCreateGroup - follow SLAP for the method - using Optional was abit complex. practice KISS --- src/main/java/seedu/duke/Group.java | 131 +++++++++++++++++----------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 17bf73b8ef..04b0c36beb 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,25 +1,19 @@ package seedu.duke; -import java.util.HashMap; import java.util.ArrayList; -import java.util.Optional; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class Group { - public static final HashMap groups = new HashMap<>(); - public static String currentGroupName = null; - protected String groupName; - protected ArrayList users; + private static final Map groups = new HashMap<>(); + private static String currentGroupName; - public Group(String groupName) { - this.groupName = groupName; - this.users = new ArrayList<>(); - } + private final String groupName; + private final List members; - public ArrayList getUsers() { - return users; - } - - public String getGroupName() { - return groupName; + private Group(String groupName) { + this.groupName = groupName; + this.members = new ArrayList<>(); } /** @@ -31,34 +25,24 @@ public String getGroupName() { * @throws IllegalStateException If trying to create or join a new group while already in another group. */ public static Group getOrCreateGroup(String groupName) { - // Check if user is accessing a group they are already in - if (currentGroupName != null && currentGroupName.equals(groupName)) { + if (isInGroup() && getCurrentGroup().getGroupName().equals(groupName)) { System.out.println("You are in " + groupName); - return groups.get(groupName); + return getCurrentGroup(); } - // Use of Optional to handle non-existing groups in hashmap - Optional optionalGroup = Optional.ofNullable(groups.get(groupName)); + // If the user is in a different group, prevent them from creating or joining a new group. + if (isInGroup() && !getCurrentGroup().getGroupName().equals(groupName)) { + throw new IllegalStateException("Please exit the current group '" + currentGroupName + + "' to create or join another group."); + } - // Tracker for existing group - final boolean[] isNewGroupCreated = {false}; + Group group = groups.get(groupName); - // If the user is in a different group, prevent them from creating or joining a new group. - Group group = optionalGroup.orElseGet(() -> { - if (currentGroupName != null && !currentGroupName.equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroupName - + "' to create or join another group."); - } - Group newGroup = new Group(groupName); - groups.put(groupName, newGroup); + if (group == null) { + group = new Group(groupName); + groups.put(groupName, group); System.out.println(groupName + " created."); - isNewGroupCreated[0] = true; - return newGroup; - }); - - // If a new group was created, update currentGroupName to reflect this. - if (isNewGroupCreated[0]) { currentGroupName = groupName; } @@ -66,27 +50,68 @@ public static Group getOrCreateGroup(String groupName) { return group; } - public Group parseAddMember(String argument) { - if (currentGroupName == null) { - throw new IllegalStateException("Please create or join a group first."); - } - String[] tokens = argument.split(" "); - if (tokens.length == 0) { - throw new IllegalArgumentException("Please enter a name for the member."); - } - String memberName = tokens[0]; - User newMember = new User(memberName); - users.add(newMember); - System.out.println(memberName + " has been added to " + currentGroupName + "."); - return this; - } - + /** + * Exits the current group. + * If the user is not in any group, it displays a message asking the user to try again. + */ public static void exitGroup() { if (currentGroupName != null) { System.out.println("You have exited " + currentGroupName + "."); currentGroupName = null; } else { - System.out.println("Please try again."); + System.out.println("You are not currently in any group. Please try again."); } } + + /** + * Retrieves the current group. + * + * @return The current group, or null if the user is not in any group. + */ + public static Group getCurrentGroup() { + if (currentGroupName != null) { + return groups.get(currentGroupName); + } + return null; + } + + /** + * Checks if the user is currently in a group. + * + * @return true if the user is in a group, false otherwise. + */ + public static boolean isInGroup() { + return currentGroupName != null; + } + + /** + * Adds a new member to the group. + * + * @param memberName The name of the member to add. + * @return The newly added user. + */ + public User addMember(String memberName) { + User newMember = new User(memberName); + members.add(newMember); + System.out.println(memberName + " has been added to " + groupName + "."); + return newMember; + } + + /** + * Retrieves the name of the group. + * + * @return The name of the group. + */ + public String getGroupName() { + return groupName; + } + + /** + * Retrieves the list of members in the group. + * + * @return The list of members in the group. + */ + public List getMembers() { + return new ArrayList<>(members); + } } From 97bccd9b8929046183319d0697b43f63734a49d8 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 24 Mar 2024 03:57:50 +0800 Subject: [PATCH 064/270] Update AddUserTest --- src/test/java/seedu/duke/AddUserTest.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 5ddffd69ca..2e86d8daa0 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -1,8 +1,8 @@ package seedu.duke; import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class AddUserTest { @@ -13,20 +13,21 @@ public void testUser() { User user = new User("John"); assertEquals("John", user.getName()); } catch (Exception e) { - fail(); + fail("Exception occurred while creating a User object: " + e.getMessage()); } } @Test public void testAddUserToGroup() { try { - Group group = new Group("TestGroup"); - User user = new User("John"); - group.addUsers(user); - assertEquals("John", group.users.get(0).getName()); + Group group = Group.getOrCreateGroup("TestGroup"); + User user = group.addMember("John"); + + assertTrue(group.getMembers().contains(user), "User was not added to the group"); + assertEquals("John", user.getName(), "User name is not the expected value"); } catch (Exception e) { - fail(); + fail("Exception occurred while adding a user to the group: " + e.getMessage()); } } - } + From e674ac7a265d5d54a04618095011ba27336340d9 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 24 Mar 2024 04:07:07 +0800 Subject: [PATCH 065/270] Update GroupTest - add testCurrentGroup - unit tests to start with test in the name --- src/test/java/seedu/duke/GroupTest.java | 59 +++++++++++++++++++------ 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 1c9f6ba6a4..f923178ca2 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -1,39 +1,70 @@ package seedu.duke; -import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; public class GroupTest { + @BeforeEach + public void setup() { + // Reset the state before each test + Group.exitGroup(); + } + + @AfterEach + public void teardown() { + // Clean up after each test + Group.exitGroup(); + } + @Test - public void groupCreationTest() { + public void testGroupCreation() { String expectedName = "GroupName"; - Group group = new Group(expectedName); + Group group = Group.getOrCreateGroup(expectedName); assertEquals(expectedName, group.getGroupName(), "Group name is not the same as expected"); } + @Test - public void addUserToGroupTest() { + public void testAddUserToGroup() { String groupName = "TestGroup"; - Group group = new Group(groupName); - User user = new User("TestUser"); - - group.addUsers(user); + Group group = Group.getOrCreateGroup(groupName); + User user = group.addMember("TestUser"); - assertTrue(group.getUsers().contains(user), "User was not added to the group"); + assertTrue(group.getMembers().contains(user), "User was not added to the group"); } + @Test - public void getOrCreateGroupTest() { + public void testGetOrCreateGroup() { String groupName = "NewGroup"; - Group.getOrCreateGroup(groupName); + Group newGroup = Group.getOrCreateGroup(groupName); - assertTrue(Group.groups.containsKey(groupName), "New group was not created"); + assertEquals(groupName, newGroup.getGroupName(), "Group name is not the expected value"); + + Group.exitGroup(); + Group existingGroup = Group.getOrCreateGroup(groupName); + + assertEquals(newGroup, existingGroup, "getOrCreateGroup should return the existing group"); } + @Test - public void exitGroupTest() { + public void testExitGroup() { String groupName = "ExitingGroup"; Group.getOrCreateGroup(groupName); Group.exitGroup(); - Assertions.assertNull(Group.currentGroupName, "Did not successfully exit the group"); + assertNull(Group.getCurrentGroup(), "Did not successfully exit the group"); + } + + @Test + public void testGetCurrentGroup() { + String groupName = "CurrentGroup"; + Group group = Group.getOrCreateGroup(groupName); + + assertEquals(group, Group.getCurrentGroup(), "Current group is not the expected group"); + + Group.exitGroup(); + assertNull(Group.getCurrentGroup(), "Current group should be null after exiting"); } } From 41c982175410d47d6bc56b9deb784512a9e6f44b Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 24 Mar 2024 04:26:35 +0800 Subject: [PATCH 066/270] Create GroupCommand class - class handles group related commands like create exit and member --- src/main/java/seedu/duke/GroupCommand.java | 44 ++++++++++++++++++++++ src/main/java/seedu/duke/Parser.java | 10 ++--- 2 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 src/main/java/seedu/duke/GroupCommand.java diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java new file mode 100644 index 0000000000..e6bd745a1b --- /dev/null +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -0,0 +1,44 @@ +package seedu.duke; + +/** + * Represents a command handler for group-related operations. + * Provides static methods to create groups, add members to groups, and exit groups. + */ +public class GroupCommand { + /** + * Creates a new group or retrieves an existing group with the specified name. + * + * @param groupName the name of the group to create or retrieve + */ + public static void createGroup(String groupName) { + try { + Group.getOrCreateGroup(groupName); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Adds a member with the specified name to the current group. + * If the user is not currently in a group, prints a message asking them to create or join a group first. + * + * @param memberName the name of the member to add + */ + public static void addMember(String memberName) { + Group currentGroup = Group.getCurrentGroup(); + if (currentGroup == null) { + System.out.println("Please create or join a group first."); + return; + } + + currentGroup.addMember(memberName); + } + + /** + * Exits the current group. + * If the user is not currently in a group, prints a message indicating so. + */ + public static void exitGroup() { + Group.exitGroup(); + } +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index cf59b7cf5b..68905874b3 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -24,21 +24,17 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "bye": throw new EndProgramException(); case "exit": - Group.exitGroup(); + GroupCommand.exitGroup(); break; case "help": // Help code here Help.printHelp(); break; case "create": - try { - String groupName = argument; - Group.getOrCreateGroup(groupName); - } catch (IllegalStateException e) { - System.out.println(e.getMessage()); // Print the message instructing to exit current group. - } + GroupCommand.createGroup(argument); break; case "member": + GroupCommand.addMember(argument); break; case "expense": try { From 24431d72f786e389ac74102b4147aba23b2bd4e9 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 24 Mar 2024 04:34:34 +0800 Subject: [PATCH 067/270] Rearrange parser --- src/main/java/seedu/duke/Parser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 68905874b3..328a4182ff 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -23,9 +23,6 @@ public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": throw new EndProgramException(); - case "exit": - GroupCommand.exitGroup(); - break; case "help": // Help code here Help.printHelp(); @@ -36,6 +33,9 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "member": GroupCommand.addMember(argument); break; + case "exit": + GroupCommand.exitGroup(); + break; case "expense": try { String[] removeExpenseTag = argument.split("/amount"); From 3da39cb86a400334858fc3064d62a085e4b10732 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 24 Mar 2024 04:50:44 +0800 Subject: [PATCH 068/270] Add isMember method - checks if new member already a member --- src/main/java/seedu/duke/Group.java | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 04b0c36beb..1096dbbfe0 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -84,13 +84,33 @@ public static boolean isInGroup() { return currentGroupName != null; } + /** + * Checks if a user with the given name is a member of the group. + * + * @param memberName The name of the member to check. + * @return true if the user is a member of the group, false otherwise. + */ + public boolean isMember(String memberName) { + for (User member : members) { + if (member.getName().equals(memberName)) { + return true; + } + } + return false; + } + /** * Adds a new member to the group. * * @param memberName The name of the member to add. - * @return The newly added user. + * @return The newly added user, or null if the user is already a member of the group. */ public User addMember(String memberName) { + if (isMember(memberName)) { + System.out.println(memberName + " is already a member of " + groupName + "."); + return null; + } + User newMember = new User(memberName); members.add(newMember); System.out.println(memberName + " has been added to " + groupName + "."); From f3490f36ac2692cadf5c5b541043392b90c761f1 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Mon, 25 Mar 2024 00:11:27 +0800 Subject: [PATCH 069/270] Remove duplicate function in expenses and refactor parser --- src/main/java/seedu/duke/Duke.java | 1 + src/main/java/seedu/duke/Expense.java | 14 ++++---------- src/main/java/seedu/duke/Help.java | 2 +- src/main/java/seedu/duke/Parser.java | 14 +++++++++++--- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 4ce8030ef2..1ab7b9d88e 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -20,6 +20,7 @@ public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("Hello " + in.nextLine()); + Help.printHelp(); while(in.hasNextLine()) { String userInput = in.nextLine(); diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index f76307543d..5cb8dbeb31 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -2,6 +2,7 @@ import java.util.ArrayList; +import java.util.Arrays; /** * A class to add a new expense @@ -19,25 +20,18 @@ public class Expense { * (Index 0 is the payer and will not be added to the payee list) */ Expense(String payerName, float totalAmount, String[] payeeList) { - payerName = removeWhitespaces(payerName); - for(int i = 1; i < payeeList.length; i++){ - payees.add(removeWhitespaces(payeeList[i])); - } + payees.addAll(Arrays.asList(payeeList)); this.payerName = payerName; this.totalAmount = totalAmount; - System.out.printf("Added new expense %.2f owed to %s by:",this.totalAmount,this.payerName); + System.out.printf("Added new expense %.2f paid by %s and split between:" + ,this.totalAmount,this.payerName); for(String payee : payees) { System.out.print(payee + ", "); } System.out.println(); } - - private String removeWhitespaces(String item) { - String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); - return itemWithoutWhitespaces; - } public String getPayerName() { return payerName; } diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index 515cbadc7d..ec194b83ce 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -2,7 +2,7 @@ public class Help { private static final String prompt = - "Welcome\n" + + "Welcome, here is a list of commands:\n" + "help: Access help menu.\n" + "create : Create a group.\n" + "exit : Exit current group.\n" + diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 328a4182ff..42ca964e03 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -48,9 +48,12 @@ public void handleUserInput() throws EndProgramException, ExpensesException { try { float totalAmount = Float.parseFloat(amount); - String[] extractPayer = extractAmount[1].split("/user"); - String payerName = extractPayer[0]; - Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); + String[] payeeList = extractAmount[1].split("/user"); + String payerName = removeWhitespaces(payeeList[0]); + for(int i = 0; i < payeeList.length; i++){ + payeeList[i] = removeWhitespaces(payeeList[i]); + } + Expense newTransaction = new Expense(payerName,totalAmount, payeeList); } catch (NumberFormatException e) { System.out.println("Re-enter expense with amount as a proper number."); } @@ -66,11 +69,16 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; default: // Default clause + System.out.println("That is not a command. " + + "Please use one of the commands given here"); + Help.printHelp(); break; } + } private String removeWhitespaces(String item) { String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } + } From d3bee7374a4b85a579e198935d10b276d742c8db Mon Sep 17 00:00:00 2001 From: Cohii Date: Mon, 25 Mar 2024 15:34:51 +0800 Subject: [PATCH 070/270] Redo Parser Add improved parser functionality to Parser Class. --- src/main/java/seedu/duke/Parser.java | 102 ++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 42ca964e03..ea6cc1d099 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,16 +1,116 @@ package seedu.duke; +import java.util.ArrayList; +import java.util.HashMap; + public class Parser { - protected String userInput; + private final String userInput; + + /** + * List of parameters to extract from user input. + * For example, "/amount (amount)". + * Add new Keys to extract additional user parameters for future functionality. + */ + private static final String[] paramKeys = {"amount", "paid", "user"}; + + /** + * First word of user input. + */ + private String command = null; + + /** + * Input between first word and '/' character. + * For example, "(command) (argument) /(parameter) (parameter input)...". + */ + private String argument = null; + + /** + * Additional parameters provided by user. + */ + private HashMap> params = createParams(); public static class EndProgramException extends Exception { } + /** + * Creates a new HashMap with Keys equal to additional parameters users might input. + * Values are arrays that store user input. + * + * @return HashMap with Keys in 'additionalFields' and empty array Values. + */ + private HashMap> createParams() { + HashMap> additionalInfo = new HashMap<>(); + + for(String paramKey : paramKeys){ + additionalInfo.put(paramKey, new ArrayList<>()); + } + + return additionalInfo; + } + public Parser(String userInput) { this.userInput = userInput; + this.parseUserInput(); } + /** + * Process the String userInput and populates corresponding fields of Parser object. + */ + public void parseUserInput() { + String[] tokens = userInput.split(" ", 2); + this.command = tokens[0].toLowerCase().trim(); + + if (tokens.length == 1){ + System.out.print(this); + return; + } + + String[] arguments = tokens[1].split("/"); + this.argument = arguments[0].trim(); + + for(int i = 1; i < arguments.length; i++){ + String[] subTokens = arguments[i].split(" ", 2); + if (subTokens.length == 1){ + continue; + } + + String subCommand = subTokens[0].toLowerCase().trim(); + String subArgument = subTokens[1].trim(); + if (!subArgument.isEmpty() && params.containsKey(subCommand)){ + params.get(subCommand).add(subArgument); + } + } + + System.out.print(this); + } + + /** + * Returns String summarising contents of Parser object. + * For easier debug printing. + * + * @return Contents of Parser object. + */ + @Override + public String toString(){ + StringBuilder parser = new StringBuilder(); + + parser.append("command: ").append(command).append("\n"); + + parser.append("argument: ").append(argument).append("\n"); + + for(String paramKey : paramKeys){ + parser.append(paramKey).append(": "); + for(String item : params.get(paramKey)){ + parser.append(item).append(" "); + } + parser.append("\n"); + } + + return parser.toString(); + } + + public void handleUserInput() throws EndProgramException, ExpensesException { String[] tokens = userInput.split(" ", 2); From 857f6c2a40cada4cc762b0bc96ac65285221d125 Mon Sep 17 00:00:00 2001 From: Cohii Date: Mon, 25 Mar 2024 15:56:53 +0800 Subject: [PATCH 071/270] Add Parser tests --- src/main/java/seedu/duke/Parser.java | 10 ++++--- src/test/java/seedu/duke/ParserTest.java | 33 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 src/test/java/seedu/duke/ParserTest.java diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index ea6cc1d099..fbfebcd7d3 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -49,6 +49,13 @@ private HashMap> createParams() { return additionalInfo; } + public Parser(String userInput, String command, String argument, HashMap> params) { + this.userInput = userInput; + this.command = command; + this.argument = argument; + this.params = params; + } + public Parser(String userInput) { this.userInput = userInput; this.parseUserInput(); @@ -62,7 +69,6 @@ public void parseUserInput() { this.command = tokens[0].toLowerCase().trim(); if (tokens.length == 1){ - System.out.print(this); return; } @@ -81,8 +87,6 @@ public void parseUserInput() { params.get(subCommand).add(subArgument); } } - - System.out.print(this); } /** diff --git a/src/test/java/seedu/duke/ParserTest.java b/src/test/java/seedu/duke/ParserTest.java new file mode 100644 index 0000000000..b5c9cb1408 --- /dev/null +++ b/src/test/java/seedu/duke/ParserTest.java @@ -0,0 +1,33 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +public class ParserTest { + + public void testParser(String userInput, Parser parser){ + Parser parserFromInput = new Parser(userInput); + + System.out.println(parserFromInput); + assert parserFromInput.toString().equals(parser.toString()); + } + + @Test + public void test1(){ + String userInput = "command argument /amount amount /paid paid /user user1 /user user2"; + HashMap> params = new HashMap<>(); + + params.put("amount", new ArrayList<>(List.of("amount"))); + params.put("paid", new ArrayList<>(List.of("paid"))); + params.put("user", new ArrayList<>(Arrays.asList("user1", "user2"))); + Parser parser = new Parser(userInput,"command", "argument", params); + + System.out.println(parser); + + testParser(userInput, parser); + } +} From 60fc539410ba57ad6f11bb4cbca4f8b4e42b1792 Mon Sep 17 00:00:00 2001 From: Cohii Date: Mon, 25 Mar 2024 16:14:33 +0800 Subject: [PATCH 072/270] Update Parser tests Update Parser tests for easier use. --- src/main/java/seedu/duke/Parser.java | 11 ++++++++-- src/test/java/seedu/duke/ParserTest.java | 28 +++++++++++------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index fbfebcd7d3..009cb3ee5d 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; public class Parser { private final String userInput; @@ -49,11 +50,17 @@ private HashMap> createParams() { return additionalInfo; } - public Parser(String userInput, String command, String argument, HashMap> params) { + /** + * Constructor for Test purposes. + */ + public Parser(String userInput, String command, String argument, + String[] amount, String[] paid, String[] user) { this.userInput = userInput; this.command = command; this.argument = argument; - this.params = params; + this.params.put("amount", new ArrayList<>(List.of(amount))); + this.params.put("paid", new ArrayList<>(List.of(paid))); + this.params.put("user", new ArrayList<>(List.of(user))); } public Parser(String userInput) { diff --git a/src/test/java/seedu/duke/ParserTest.java b/src/test/java/seedu/duke/ParserTest.java index b5c9cb1408..80a498ec6a 100644 --- a/src/test/java/seedu/duke/ParserTest.java +++ b/src/test/java/seedu/duke/ParserTest.java @@ -5,29 +5,27 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; public class ParserTest { - public void testParser(String userInput, Parser parser){ - Parser parserFromInput = new Parser(userInput); + public void testParser(String userInput, String command, String argument, + String[] amount, String[] paid, String[] user){ + Parser parserFromInput = new Parser(userInput); System.out.println(parserFromInput); - assert parserFromInput.toString().equals(parser.toString()); + + Parser parserFromParams = new Parser(userInput, command, argument, amount, paid, user); + System.out.println(parserFromParams); + + assert parserFromInput.toString().equals(parserFromParams.toString()); } @Test public void test1(){ - String userInput = "command argument /amount amount /paid paid /user user1 /user user2"; - HashMap> params = new HashMap<>(); - - params.put("amount", new ArrayList<>(List.of("amount"))); - params.put("paid", new ArrayList<>(List.of("paid"))); - params.put("user", new ArrayList<>(Arrays.asList("user1", "user2"))); - Parser parser = new Parser(userInput,"command", "argument", params); - - System.out.println(parser); - - testParser(userInput, parser); + testParser("command argument /amount amount /paid paid /user user1 /user user2", + "command", "argument", + new String[]{"amount"}, + new String[]{"paid"}, + new String[]{"user1", "user2"}); } } From c9ca69992e5d80f7b1f2550401832df2a0ebea76 Mon Sep 17 00:00:00 2001 From: Cohii Date: Mon, 25 Mar 2024 16:27:18 +0800 Subject: [PATCH 073/270] Fix checkstyle errors --- src/main/java/seedu/duke/Parser.java | 35 ++++++++++++------------ src/test/java/seedu/duke/ParserTest.java | 4 --- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 009cb3ee5d..1f2cf3630e 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -5,7 +5,6 @@ import java.util.List; public class Parser { - private final String userInput; /** * List of parameters to extract from user input. @@ -14,6 +13,8 @@ public class Parser { */ private static final String[] paramKeys = {"amount", "paid", "user"}; + private final String userInput; + /** * First word of user input. */ @@ -34,22 +35,6 @@ public static class EndProgramException extends Exception { } - /** - * Creates a new HashMap with Keys equal to additional parameters users might input. - * Values are arrays that store user input. - * - * @return HashMap with Keys in 'additionalFields' and empty array Values. - */ - private HashMap> createParams() { - HashMap> additionalInfo = new HashMap<>(); - - for(String paramKey : paramKeys){ - additionalInfo.put(paramKey, new ArrayList<>()); - } - - return additionalInfo; - } - /** * Constructor for Test purposes. */ @@ -68,6 +53,22 @@ public Parser(String userInput) { this.parseUserInput(); } + /** + * Creates a new HashMap with Keys equal to additional parameters users might input. + * Values are arrays that store user input. + * + * @return HashMap with Keys in 'additionalFields' and empty array Values. + */ + private HashMap> createParams() { + HashMap> additionalInfo = new HashMap<>(); + + for(String paramKey : paramKeys){ + additionalInfo.put(paramKey, new ArrayList<>()); + } + + return additionalInfo; + } + /** * Process the String userInput and populates corresponding fields of Parser object. */ diff --git a/src/test/java/seedu/duke/ParserTest.java b/src/test/java/seedu/duke/ParserTest.java index 80a498ec6a..42724687d7 100644 --- a/src/test/java/seedu/duke/ParserTest.java +++ b/src/test/java/seedu/duke/ParserTest.java @@ -2,10 +2,6 @@ import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; - public class ParserTest { From 536dab790fddb77f423ddb12d120d6a7e5f0ac2d Mon Sep 17 00:00:00 2001 From: avrilgk Date: Mon, 25 Mar 2024 22:32:17 +0800 Subject: [PATCH 074/270] Addded use of optional to handle null and fixed checkstyle --- docs/AboutUs.md | 15 ++- src/main/java/seedu/duke/Duke.java | 5 +- src/main/java/seedu/duke/Group.java | 27 ++--- src/main/java/seedu/duke/Help.java | 2 +- src/main/java/seedu/duke/Parser.java | 136 +++++++++++++----------- src/test/java/seedu/duke/GroupTest.java | 6 +- 6 files changed, 103 insertions(+), 88 deletions(-) diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 9622e98bee..e4c3286ec0 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,11 +1,10 @@ # About us - -Display | Name | Github Profile | Portfolio ---------|:-------------:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | Hafiz | [Github](https://github.com/hafizuddin-a) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Heng Junxiang | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Akshan | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + Display | Name | Github Profile | Portfolio +-----------------------------------------------------|:-------------:|:-----------------------------------------:|:---------------------------------: + ![](https://via.placeholder.com/100.png?text=Photo) | Hafiz | [Github](https://github.com/hafizuddin-a) | [Portfolio](docs/team/johndoe.md) + ![](https://via.placeholder.com/100.png?text=Photo) | Heng Junxiang | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + ![](https://via.placeholder.com/100.png?text=Photo) | Akshan | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + ![](https://via.placeholder.com/100.png?text=Photo) | Avril | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + ![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 010c2dd780..32e9acc51e 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,7 +1,6 @@ package seedu.duke; import java.util.Scanner; -import java.util.HashMap; public class Duke { /** @@ -22,7 +21,7 @@ public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("Hello " + in.nextLine()); - while(in.hasNextLine()) { + while (in.hasNextLine()) { String userInput = in.nextLine(); Parser parser = new Parser(userInput); @@ -36,4 +35,4 @@ public static void main(String[] args) { } System.out.println("Goodbye!"); } -} \ No newline at end of file +} diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 77b3d7438e..c2589b4910 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,13 +1,14 @@ package seedu.duke; + import java.util.HashMap; import java.util.ArrayList; import java.util.Optional; public class Group { - public static final HashMap groups = new HashMap<>(); + static Optional currentGroupName = Optional.empty(); + static final HashMap groups = new HashMap<>(); protected String groupName; protected ArrayList users; - static String currentGroupName = null; public Group(String groupName) { this.groupName = groupName; @@ -30,12 +31,12 @@ public String getGroupName() { * @return The existing or newly created group. * @throws IllegalStateException If trying to create or join a new group while already in another group. */ - public static Group getOrCreateGroup(String groupName) { + public static Optional getOrCreateGroup(String groupName) { // Check if user is accessing a group they are already in - if (currentGroupName != null && currentGroupName.equals(groupName)) { + if (currentGroupName.isPresent() && currentGroupName.get().equals(groupName)) { System.out.println("You are in " + groupName); - return groups.get(groupName); + return Optional.ofNullable(groups.get(groupName)); } // Use of Optional to handle non-existing groups in hashmap @@ -46,8 +47,8 @@ public static Group getOrCreateGroup(String groupName) { // If the user is in a different group, prevent them from creating or joining a new group. Group group = optionalGroup.orElseGet(() -> { - if (currentGroupName != null && !currentGroupName.equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroupName + "' to create or join another group."); + if (currentGroupName.isPresent() && !currentGroupName.get().equals(groupName)) { + throw new IllegalStateException("Please exit the current group '" + currentGroupName + " first."); } Group newGroup = new Group(groupName); groups.put(groupName, newGroup); @@ -58,11 +59,11 @@ public static Group getOrCreateGroup(String groupName) { // If a new group was created, update currentGroupName to reflect this. if (isNewGroupCreated[0]) { - currentGroupName = groupName; + currentGroupName = Optional.of(groupName); } System.out.println("You are now in " + groupName); - return group; + return Optional.ofNullable(groups.get(groupName)); } public void addUsers(User user) { @@ -80,11 +81,13 @@ public void addUsers(User user) { } public static void exitGroup() { - if (currentGroupName != null) { - System.out.println("You have exited " + currentGroupName + "."); - currentGroupName = null; + if (currentGroupName.isPresent()) { + System.out.println("You have exited " + currentGroupName.get() + "."); + currentGroupName = Optional.empty(); } else { System.out.println("Please try again."); } + System.out.println("Current group: " + currentGroupName.orElse(null)); } } + diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index f84c733825..dfd04bfe70 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -14,4 +14,4 @@ public class Help { static void printHelp() { System.out.println(prompt); } -} \ No newline at end of file +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index caa652d34d..eedb01e095 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,5 +1,7 @@ package seedu.duke; +import java.util.Optional; + public class Parser { protected String userInput; @@ -20,77 +22,85 @@ public void handleUserInput() throws EndProgramException, ExpensesException { argument = tokens[1].trim(); } - switch(command) { - case "bye": - throw new EndProgramException(); - case "exit": - Group.exitGroup(); - break; - case "help": - // Help code here - Help.printHelp(); - break; - case "create": - try { - String groupName = argument; - Group.getOrCreateGroup(groupName); - } catch (IllegalStateException e) { - System.out.println(e.getMessage()); // Print the message instructing to exit current group. + switch (command) { + case "bye": + throw new EndProgramException(); + case "exit": + Group.exitGroup(); + break; + case "help": + // Help code here + Help.printHelp(); + break; + case "create": + try { + Optional optionalGroup = Group.getOrCreateGroup(argument); + if (optionalGroup.isEmpty()) { + System.out.println("The group does not exist."); } - break; - case "member": - try { - String[] memberDetails = argument.split("/group"); - if(memberDetails.length == 1) { - throw new ExpensesException("No group name for user! Add /group "); - } - String memberName = memberDetails[0].trim(); - if (memberName.isEmpty()) { - throw new ExpensesException("No name for user! Add a name for the user"); - } - String groupNameForUser = memberDetails[1].trim(); - User newUser = new User(memberName); - Group group = Group.getOrCreateGroup(groupNameForUser); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); // Print the message instructing to exit current group. + } + break; + case "member": + try { + String[] memberDetails = argument.split("/group"); + if (memberDetails.length == 1) { + throw new ExpensesException("No group name for user! Add /group "); + } + String memberName = memberDetails[0].trim(); + if (memberName.isEmpty()) { + throw new ExpensesException("No name for user! Add a name for the user"); + } + String groupNameForUser = memberDetails[1].trim(); + User newUser = new User(memberName); + Optional optionalGroup = Group.getOrCreateGroup(groupNameForUser); + if (optionalGroup.isPresent()) { + Group group = optionalGroup.get(); group.addUsers(newUser); - } catch (Exception e) { - System.out.println(e.getMessage()); + } else { + System.out.println("The group does not exist."); } - break; - case "expense": - try { - String[] removeExpenseTag = argument.split("/amount"); - if(removeExpenseTag.length == 1) { - throw new ExpensesException("No description for expenses! Add /amount /paid /user"); - } - String[] extractAmount = removeExpenseTag[1].split("/paid"); - String amount = extractAmount[0]; - amount = removeWhitespaces(amount); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + break; + case "expense": + try { + String[] removeExpenseTag = argument.split("/amount"); + if (removeExpenseTag.length == 1) { + throw new ExpensesException("No description for expenses! Add /amount /paid /user"); + } + String[] extractAmount = removeExpenseTag[1].split("/paid"); + String amount = extractAmount[0]; + amount = removeWhitespaces(amount); - try { - float totalAmount = Float.parseFloat(amount); - String[] extractPayer = extractAmount[1].split("/user"); - String payerName = extractPayer[0]; - Expense newTransaction = new Expense(payerName,totalAmount,extractPayer); - } catch (NumberFormatException e) { - System.out.println("Re-enter expense with amount as a proper number."); - } - } catch(ArrayIndexOutOfBoundsException e) { - System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); + try { + float totalAmount = Float.parseFloat(amount); + String[] extractPayer = extractAmount[1].split("/user"); + String payerName = extractPayer[0]; + Expense newTransaction = new Expense(payerName, totalAmount, extractPayer); + } catch (NumberFormatException e) { + System.out.println("Re-enter expense with amount as a proper number."); } - break; - case "list": - // List code here - break; - case "balance": - // Balance code here - break; - default: - // Default clause - break; + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); + } + break; + case "list": + // List code here + break; + case "balance": + // Balance code here + break; + default: + // Default clause + break; } } + private String removeWhitespaces(String item) { String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); return itemWithoutWhitespaces; } -} \ No newline at end of file +} diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 1c9f6ba6a4..d2effd0c39 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,6 +13,7 @@ public void groupCreationTest() { Group group = new Group(expectedName); assertEquals(expectedName, group.getGroupName(), "Group name is not the same as expected"); } + @Test public void addUserToGroupTest() { String groupName = "TestGroup"; @@ -22,6 +24,7 @@ public void addUserToGroupTest() { assertTrue(group.getUsers().contains(user), "User was not added to the group"); } + @Test public void getOrCreateGroupTest() { String groupName = "NewGroup"; @@ -29,11 +32,12 @@ public void getOrCreateGroupTest() { assertTrue(Group.groups.containsKey(groupName), "New group was not created"); } + @Test public void exitGroupTest() { String groupName = "ExitingGroup"; Group.getOrCreateGroup(groupName); Group.exitGroup(); - Assertions.assertNull(Group.currentGroupName, "Did not successfully exit the group"); + Assertions.assertTrue(Group.currentGroupName.isEmpty(), "Did not successfully exit the group"); } } From aefb4100b1acead726b6f919813b8b62b32954ae Mon Sep 17 00:00:00 2001 From: mukund1403 <108778858+mukund1403@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:20:30 +0800 Subject: [PATCH 075/270] Update build.gradle to enable assertions --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index ea82051fab..593bf8299e 100644 --- a/build.gradle +++ b/build.gradle @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } From 9ac52f22eed87a4ea38db46fe67b9da00974abe6 Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 26 Mar 2024 14:26:55 +0800 Subject: [PATCH 076/270] Integrate updated Parser with Expense Class Rewrite switch statement to handle creating Expense --- src/main/java/seedu/duke/Parser.java | 57 +++++++++++----------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 1f2cf3630e..f3473a6110 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -124,14 +124,6 @@ public String toString(){ public void handleUserInput() throws EndProgramException, ExpensesException { - String[] tokens = userInput.split(" ", 2); - - String command = tokens[0].toLowerCase().trim(); - String argument = ""; - if (tokens.length > 1) { - argument = tokens[1].trim(); - } - switch (command) { case "bye": throw new EndProgramException(); @@ -149,29 +141,31 @@ public void handleUserInput() throws EndProgramException, ExpensesException { GroupCommand.exitGroup(); break; case "expense": - try { - String[] removeExpenseTag = argument.split("/amount"); - if (removeExpenseTag.length == 1) { - throw new ExpensesException("No description for expenses! Add /amount /paid /user"); - } - String[] extractAmount = removeExpenseTag[1].split("/paid"); - String amount = extractAmount[0]; - amount = removeWhitespaces(amount); - - try { - float totalAmount = Float.parseFloat(amount); - String[] payeeList = extractAmount[1].split("/user"); - String payerName = removeWhitespaces(payeeList[0]); - for(int i = 0; i < payeeList.length; i++){ - payeeList[i] = removeWhitespaces(payeeList[i]); - } - Expense newTransaction = new Expense(payerName,totalAmount, payeeList); - } catch (NumberFormatException e) { - System.out.println("Re-enter expense with amount as a proper number."); + + // Checks for missing Expense Parameters + String[] expenseParams = {"amount", "paid", "user"}; + for(String expenseParam : expenseParams){ + if(params.get(expenseParam).isEmpty()){ + String exceptionMessage = "No description for expenses! Add /" + expenseParam; + throw new ExpensesException(exceptionMessage); } - } catch(ArrayIndexOutOfBoundsException e) { - System.out.println("Empty /amount, /paid or /user. Add expenses using the correct format."); } + + // Checks if amount is a valid Float + float totalAmount; + try { + totalAmount = Float.parseFloat(params.get("amount").get(0)); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter expense with amount as a proper number."; + throw new ExpensesException(exceptionMessage); + } + + // Obtain necessary information from 'params' and create new Expense + ArrayList payeeList = params.get("user"); + String payerName = params.get("paid").get(0); + payeeList.add(0, payerName); + + Expense newTransaction = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); break; case "list": // List code here @@ -188,9 +182,4 @@ public void handleUserInput() throws EndProgramException, ExpensesException { } } - private String removeWhitespaces(String item) { - String itemWithoutWhitespaces = item.replaceAll("\\s+", " ").trim(); - return itemWithoutWhitespaces; - } - } From 0eede69914c333d8b7cb372cc83271631600ab12 Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 26 Mar 2024 14:27:35 +0800 Subject: [PATCH 077/270] Fix Balance tests Balance tests now pass. Fix format to create Expense object. --- src/main/java/seedu/duke/Balance.java | 5 ++++- src/test/java/seedu/duke/BalanceTest.java | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index e15600edf1..4423d2bc11 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -40,11 +40,14 @@ public Map getBalanceList() { private void addExpense(Expense expense) { ArrayList payees = expense.getPayees(); - int numberOfUsers = payees.size() + 1; + int numberOfUsers = payees.size(); Float amountPerUser = expense.getTotalAmount() / numberOfUsers; if(expense.getPayerName().equals(userName)) { for(String payee : payees) { + if(payee.equals(userName)){ + continue; + } Float currentOwed = balanceList.get(payee); Float newOwed = currentOwed + amountPerUser; diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 9eaa541683..24fc583573 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -14,9 +14,9 @@ public void testConstructor() { users.add(new User("member3")); ArrayList expenses = new ArrayList<>(); - expenses.add(new Expense("member1", 15, new String[]{"", "member2", "member3"})); - expenses.add(new Expense("member2", 30, new String[]{"", "member1", "member3"})); - expenses.add(new Expense("member3", 100, new String[]{"", "member1"})); + expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); + expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); + expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); Balance member1Balance = new Balance("member1", expenses, users); member1Balance.printBalance(); From 17753a83be7a7a321b8afb27a8ffa730fd9bddbe Mon Sep 17 00:00:00 2001 From: Cohii Date: Tue, 26 Mar 2024 14:31:20 +0800 Subject: [PATCH 078/270] Fix IO test Replace expected.txt with correct output --- text-ui-test/EXPECTED.TXT | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 3119547477..0de20b7ef1 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -8,4 +8,12 @@ Hello from What is your name? Hello James Gosling +Welcome, here is a list of commands: +help: Access help menu. +create : Create a group. +exit : Exit current group. +member : Add a member to the group. +expense /amount /paid /user /user ...: Add an expense. +list: List all expenses in the group. +balance : Show user's balance. Goodbye! From d6336305425d7110a08361e409d29225c7e2a383 Mon Sep 17 00:00:00 2001 From: MonkeScripts <104839312+MonkeScripts@users.noreply.github.com> Date: Tue, 26 Mar 2024 18:32:14 +0800 Subject: [PATCH 079/270] Revert "Addded use of optional to handle null and fixed checkstyle" --- docs/AboutUs.md | 15 +++---- src/main/java/seedu/duke/Duke.java | 2 +- src/main/java/seedu/duke/Group.java | 54 ++++++++----------------- src/main/java/seedu/duke/Parser.java | 40 ++++-------------- src/test/java/seedu/duke/GroupTest.java | 14 ++++++- 5 files changed, 44 insertions(+), 81 deletions(-) diff --git a/docs/AboutUs.md b/docs/AboutUs.md index e4c3286ec0..9622e98bee 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,10 +1,11 @@ # About us - Display | Name | Github Profile | Portfolio ------------------------------------------------------|:-------------:|:-----------------------------------------:|:---------------------------------: - ![](https://via.placeholder.com/100.png?text=Photo) | Hafiz | [Github](https://github.com/hafizuddin-a) | [Portfolio](docs/team/johndoe.md) - ![](https://via.placeholder.com/100.png?text=Photo) | Heng Junxiang | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) - ![](https://via.placeholder.com/100.png?text=Photo) | Akshan | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) - ![](https://via.placeholder.com/100.png?text=Photo) | Avril | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) - ![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + +Display | Name | Github Profile | Portfolio +--------|:-------------:|:--------------:|:---------: +![](https://via.placeholder.com/100.png?text=Photo) | Hafiz | [Github](https://github.com/hafizuddin-a) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Heng Junxiang | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Akshan | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 40acb7efa7..1ab7b9d88e 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -22,7 +22,7 @@ public static void main(String[] args) { System.out.println("Hello " + in.nextLine()); Help.printHelp(); - while (in.hasNextLine()) { + while(in.hasNextLine()) { String userInput = in.nextLine(); Parser parser = new Parser(userInput); diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 17c393cc28..1096dbbfe0 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,19 +1,12 @@ package seedu.duke; - +import java.util.ArrayList; import java.util.HashMap; - +import java.util.List; +import java.util.Map; public class Group { - static Optional currentGroupName = Optional.empty(); - static final HashMap groups = new HashMap<>(); - protected String groupName; - protected ArrayList users; - - public Group(String groupName) { - this.groupName = groupName; - this.users = new ArrayList<>(); - } - + private static final Map groups = new HashMap<>(); + private static String currentGroupName; private final String groupName; private final List members; @@ -31,13 +24,11 @@ private Group(String groupName) { * @return The existing or newly created group. * @throws IllegalStateException If trying to create or join a new group while already in another group. */ - - public static Optional getOrCreateGroup(String groupName) { - + public static Group getOrCreateGroup(String groupName) { // Check if user is accessing a group they are already in - if (currentGroupName.isPresent() && currentGroupName.get().equals(groupName)) { + if (isInGroup() && getCurrentGroup().getGroupName().equals(groupName)) { System.out.println("You are in " + groupName); - return Optional.ofNullable(groups.get(groupName)); + return getCurrentGroup(); } // If the user is in a different group, prevent them from creating or joining a new group. @@ -48,26 +39,15 @@ public static Optional getOrCreateGroup(String groupName) { Group group = groups.get(groupName); - - // If the user is in a different group, prevent them from creating or joining a new group. - Group group = optionalGroup.orElseGet(() -> { - if (currentGroupName.isPresent() && !currentGroupName.get().equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroupName + " first."); - } - Group newGroup = new Group(groupName); - groups.put(groupName, newGroup); + if (group == null) { + group = new Group(groupName); + groups.put(groupName, group); System.out.println(groupName + " created."); - isNewGroupCreated[0] = true; - return newGroup; - }); - - // If a new group was created, update currentGroupName to reflect this. - if (isNewGroupCreated[0]) { - currentGroupName = Optional.of(groupName); + currentGroupName = groupName; } System.out.println("You are now in " + groupName); - return Optional.ofNullable(groups.get(groupName)); + return group; } /** @@ -75,13 +55,12 @@ public static Optional getOrCreateGroup(String groupName) { * If the user is not in any group, it displays a message asking the user to try again. */ public static void exitGroup() { - if (currentGroupName.isPresent()) { - System.out.println("You have exited " + currentGroupName.get() + "."); - currentGroupName = Optional.empty(); + if (currentGroupName != null) { + System.out.println("You have exited " + currentGroupName + "."); + currentGroupName = null; } else { System.out.println("You are not currently in any group. Please try again."); } - System.out.println("Current group: " + currentGroupName.orElse(null)); } /** @@ -156,4 +135,3 @@ public List getMembers() { return new ArrayList<>(members); } } - diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 2d9caa8ddc..f3473a6110 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,6 +1,5 @@ package seedu.duke; -import java.util.Optional; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -80,6 +79,7 @@ public void parseUserInput() { if (tokens.length == 1){ return; } + String[] arguments = tokens[1].split("/"); this.argument = arguments[0].trim(); @@ -127,45 +127,18 @@ public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": throw new EndProgramException(); - case "exit": - Group.exitGroup(); - break; case "help": // Help code here Help.printHelp(); break; case "create": - try { - Optional optionalGroup = Group.getOrCreateGroup(argument); - if (optionalGroup.isEmpty()) { - System.out.println("The group does not exist."); - } - } catch (IllegalStateException e) { - System.out.println(e.getMessage()); // Print the message instructing to exit current group. - } + GroupCommand.createGroup(argument); break; case "member": - try { - String[] memberDetails = argument.split("/group"); - if (memberDetails.length == 1) { - throw new ExpensesException("No group name for user! Add /group "); - } - String memberName = memberDetails[0].trim(); - if (memberName.isEmpty()) { - throw new ExpensesException("No name for user! Add a name for the user"); - } - String groupNameForUser = memberDetails[1].trim(); - User newUser = new User(memberName); - Optional optionalGroup = Group.getOrCreateGroup(groupNameForUser); - if (optionalGroup.isPresent()) { - Group group = optionalGroup.get(); - group.addUsers(newUser); - } else { - System.out.println("The group does not exist."); - } - } catch (Exception e) { - System.out.println(e.getMessage()); - } + GroupCommand.addMember(argument); + break; + case "exit": + GroupCommand.exitGroup(); break; case "expense": @@ -207,5 +180,6 @@ public void handleUserInput() throws EndProgramException, ExpensesException { Help.printHelp(); break; } + } } diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index f16e1a3640..f923178ca2 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNull; @@ -55,6 +54,17 @@ public void testExitGroup() { String groupName = "ExitingGroup"; Group.getOrCreateGroup(groupName); Group.exitGroup(); - Assertions.assertTrue(Group.currentGroupName.isEmpty(), "Did not successfully exit the group"); + assertNull(Group.getCurrentGroup(), "Did not successfully exit the group"); + } + + @Test + public void testGetCurrentGroup() { + String groupName = "CurrentGroup"; + Group group = Group.getOrCreateGroup(groupName); + + assertEquals(group, Group.getCurrentGroup(), "Current group is not the expected group"); + + Group.exitGroup(); + assertNull(Group.getCurrentGroup(), "Current group should be null after exiting"); } } From dc1f18f13e256c45cdf94e9e9997e289b456f4d3 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Tue, 26 Mar 2024 22:07:15 +0800 Subject: [PATCH 080/270] optional update --- src/main/java/seedu/duke/Group.java | 51 ++++++++++++---------- src/main/java/seedu/duke/GroupCommand.java | 8 ++-- src/test/java/seedu/duke/AddUserTest.java | 21 +++++---- src/test/java/seedu/duke/GroupTest.java | 36 +++++++++------ 4 files changed, 68 insertions(+), 48 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 1096dbbfe0..4293e5076b 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,12 +1,14 @@ package seedu.duke; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; public class Group { private static final Map groups = new HashMap<>(); - private static String currentGroupName; + private static Optional currentGroupName = Optional.empty(); private final String groupName; private final List members; @@ -24,26 +26,32 @@ private Group(String groupName) { * @return The existing or newly created group. * @throws IllegalStateException If trying to create or join a new group while already in another group. */ - public static Group getOrCreateGroup(String groupName) { + public static Optional getOrCreateGroup(String groupName) { // Check if user is accessing a group they are already in - if (isInGroup() && getCurrentGroup().getGroupName().equals(groupName)) { - System.out.println("You are in " + groupName); - return getCurrentGroup(); - } + getCurrentGroup().ifPresent(currentGroup -> { + if (currentGroup.getGroupName().equals(groupName)) { + System.out.println("You are in " + groupName); + } + }); // If the user is in a different group, prevent them from creating or joining a new group. - if (isInGroup() && !getCurrentGroup().getGroupName().equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroupName - + "' to create or join another group."); + if (isInGroup()) { + getCurrentGroup().ifPresent(currentGroup -> { + if (!currentGroup.getGroupName().equals(groupName)) { + throw new IllegalStateException("Please exit the current group '" + currentGroup.getGroupName() + + "' to create or join another group."); + } + }); } - Group group = groups.get(groupName); + Optional group = Optional.ofNullable(groups.get(groupName)); - if (group == null) { - group = new Group(groupName); - groups.put(groupName, group); + if (group.isEmpty()) { + Group newGroup = new Group(groupName); + groups.put(groupName, newGroup); System.out.println(groupName + " created."); - currentGroupName = groupName; + currentGroupName = Optional.of(groupName); + group = Optional.of(newGroup); } System.out.println("You are now in " + groupName); @@ -55,9 +63,9 @@ public static Group getOrCreateGroup(String groupName) { * If the user is not in any group, it displays a message asking the user to try again. */ public static void exitGroup() { - if (currentGroupName != null) { - System.out.println("You have exited " + currentGroupName + "."); - currentGroupName = null; + if (currentGroupName.isPresent()) { + System.out.println("You have exited " + currentGroupName.get() + "."); + currentGroupName = Optional.empty(); } else { System.out.println("You are not currently in any group. Please try again."); } @@ -68,11 +76,8 @@ public static void exitGroup() { * * @return The current group, or null if the user is not in any group. */ - public static Group getCurrentGroup() { - if (currentGroupName != null) { - return groups.get(currentGroupName); - } - return null; + public static Optional getCurrentGroup() { + return currentGroupName.map(groups::get); } /** @@ -81,7 +86,7 @@ public static Group getCurrentGroup() { * @return true if the user is in a group, false otherwise. */ public static boolean isInGroup() { - return currentGroupName != null; + return currentGroupName.isPresent(); } /** diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index e6bd745a1b..6ac83a914c 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -1,5 +1,7 @@ package seedu.duke; +import java.util.Optional; + /** * Represents a command handler for group-related operations. * Provides static methods to create groups, add members to groups, and exit groups. @@ -25,13 +27,13 @@ public static void createGroup(String groupName) { * @param memberName the name of the member to add */ public static void addMember(String memberName) { - Group currentGroup = Group.getCurrentGroup(); - if (currentGroup == null) { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { System.out.println("Please create or join a group first."); return; } - currentGroup.addMember(memberName); + currentGroup.get().addMember(memberName); } /** diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 2e86d8daa0..5298c9137c 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -1,6 +1,9 @@ package seedu.duke; import org.junit.jupiter.api.Test; + +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -19,15 +22,17 @@ public void testUser() { @Test public void testAddUserToGroup() { - try { - Group group = Group.getOrCreateGroup("TestGroup"); - User user = group.addMember("John"); - - assertTrue(group.getMembers().contains(user), "User was not added to the group"); - assertEquals("John", user.getName(), "User name is not the expected value"); - } catch (Exception e) { - fail("Exception occurred while adding a user to the group: " + e.getMessage()); + String groupName = "TestGroup"; + Optional group = Group.getOrCreateGroup(groupName); + if (group.isEmpty()) { + System.out.println("Group does not exist."); + return; } + + User user = group.get().addMember("TestUser"); + + assertTrue(group.get().getMembers().contains(user), "User was not added to the group"); } } + diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index f923178ca2..f4dec8d3b2 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -3,9 +3,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertNull; public class GroupTest { @BeforeEach @@ -23,30 +25,36 @@ public void teardown() { @Test public void testGroupCreation() { String expectedName = "GroupName"; - Group group = Group.getOrCreateGroup(expectedName); - assertEquals(expectedName, group.getGroupName(), "Group name is not the same as expected"); + Optional group = Group.getOrCreateGroup(expectedName); + assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); } @Test public void testAddUserToGroup() { String groupName = "TestGroup"; - Group group = Group.getOrCreateGroup(groupName); - User user = group.addMember("TestUser"); + Optional group = Group.getOrCreateGroup(groupName); + if (group.isEmpty()) { + System.out.println("Group does not exist."); + return; + } + + User user = group.get().addMember("TestUser"); - assertTrue(group.getMembers().contains(user), "User was not added to the group"); + assertTrue(group.get().getMembers().contains(user), "User was not added to the group"); } @Test public void testGetOrCreateGroup() { String groupName = "NewGroup"; - Group newGroup = Group.getOrCreateGroup(groupName); + Optional newGroup = Group.getOrCreateGroup(groupName); - assertEquals(groupName, newGroup.getGroupName(), "Group name is not the expected value"); + assertEquals(groupName, newGroup.get().getGroupName(), "Group name is not the expected value"); Group.exitGroup(); - Group existingGroup = Group.getOrCreateGroup(groupName); + Optional existingGroup = Group.getOrCreateGroup(groupName); - assertEquals(newGroup, existingGroup, "getOrCreateGroup should return the existing group"); + assertEquals(newGroup.get(), existingGroup.get(), "getOrCreateGroup should return the existing group"); + assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); } @Test @@ -54,17 +62,17 @@ public void testExitGroup() { String groupName = "ExitingGroup"; Group.getOrCreateGroup(groupName); Group.exitGroup(); - assertNull(Group.getCurrentGroup(), "Did not successfully exit the group"); + assertTrue(Group.getCurrentGroup().isEmpty(), "Did not successfully exit the group"); } @Test public void testGetCurrentGroup() { String groupName = "CurrentGroup"; - Group group = Group.getOrCreateGroup(groupName); + Optional group = Group.getOrCreateGroup(groupName); - assertEquals(group, Group.getCurrentGroup(), "Current group is not the expected group"); + assertEquals(group.get(), Group.getCurrentGroup().get(), "Current group is not the expected group"); Group.exitGroup(); - assertNull(Group.getCurrentGroup(), "Current group should be null after exiting"); + assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); } } From 63af72342d3b57b75823b1d578d818fbab6c1e99 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Tue, 26 Mar 2024 22:44:58 +0800 Subject: [PATCH 081/270] optional update --- src/test/java/seedu/duke/GroupTest.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index f4dec8d3b2..7dac8cdf70 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -29,20 +29,6 @@ public void testGroupCreation() { assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); } - @Test - public void testAddUserToGroup() { - String groupName = "TestGroup"; - Optional group = Group.getOrCreateGroup(groupName); - if (group.isEmpty()) { - System.out.println("Group does not exist."); - return; - } - - User user = group.get().addMember("TestUser"); - - assertTrue(group.get().getMembers().contains(user), "User was not added to the group"); - } - @Test public void testGetOrCreateGroup() { String groupName = "NewGroup"; From a756c57ea7b4a0691c4202aea3ffdf781a408776 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 27 Mar 2024 01:36:44 +0800 Subject: [PATCH 082/270] Refactor group, group commands into an optional context in Parser.java --- src/main/java/seedu/duke/Group.java | 90 +++------------ src/main/java/seedu/duke/GroupCommand.java | 92 +++++++-------- src/main/java/seedu/duke/Parser.java | 59 +++++++--- src/test/java/seedu/duke/AddUserTest.java | 16 +-- src/test/java/seedu/duke/GroupTest.java | 128 ++++++++++----------- 5 files changed, 177 insertions(+), 208 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 4293e5076b..868ca21b80 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -7,9 +7,6 @@ import java.util.Optional; public class Group { - private static final Map groups = new HashMap<>(); - private static Optional currentGroupName = Optional.empty(); - private final String groupName; private final List members; @@ -19,74 +16,13 @@ private Group(String groupName) { } /** - * Retrieves an existing group by its name or creates a new one if it does not exist. - * It ensures that a user cannot create or join a new group without exiting their current group. + * Factory method to create a new Group * * @param groupName The name of the group to get or create. - * @return The existing or newly created group. - * @throws IllegalStateException If trying to create or join a new group while already in another group. - */ - public static Optional getOrCreateGroup(String groupName) { - // Check if user is accessing a group they are already in - getCurrentGroup().ifPresent(currentGroup -> { - if (currentGroup.getGroupName().equals(groupName)) { - System.out.println("You are in " + groupName); - } - }); - - // If the user is in a different group, prevent them from creating or joining a new group. - if (isInGroup()) { - getCurrentGroup().ifPresent(currentGroup -> { - if (!currentGroup.getGroupName().equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroup.getGroupName() - + "' to create or join another group."); - } - }); - } - - Optional group = Optional.ofNullable(groups.get(groupName)); - - if (group.isEmpty()) { - Group newGroup = new Group(groupName); - groups.put(groupName, newGroup); - System.out.println(groupName + " created."); - currentGroupName = Optional.of(groupName); - group = Optional.of(newGroup); - } - - System.out.println("You are now in " + groupName); - return group; - } - - /** - * Exits the current group. - * If the user is not in any group, it displays a message asking the user to try again. - */ - public static void exitGroup() { - if (currentGroupName.isPresent()) { - System.out.println("You have exited " + currentGroupName.get() + "."); - currentGroupName = Optional.empty(); - } else { - System.out.println("You are not currently in any group. Please try again."); - } - } - - /** - * Retrieves the current group. - * - * @return The current group, or null if the user is not in any group. + * @return Newly created group. */ - public static Optional getCurrentGroup() { - return currentGroupName.map(groups::get); - } - - /** - * Checks if the user is currently in a group. - * - * @return true if the user is in a group, false otherwise. - */ - public static boolean isInGroup() { - return currentGroupName.isPresent(); + public static Group createGroup(String groupName) { + return new Group(groupName); } /** @@ -108,18 +44,17 @@ public boolean isMember(String memberName) { * Adds a new member to the group. * * @param memberName The name of the member to add. - * @return The newly added user, or null if the user is already a member of the group. */ - public User addMember(String memberName) { + public void addMember(String memberName) { if (isMember(memberName)) { - System.out.println(memberName + " is already a member of " + groupName + "."); - return null; + System.out.println( + memberName + " is already a member of " + groupName + "."); + return; } - User newMember = new User(memberName); members.add(newMember); - System.out.println(memberName + " has been added to " + groupName + "."); - return newMember; + System.out.println( + memberName + " has been added to " + groupName + "."); } /** @@ -139,4 +74,9 @@ public String getGroupName() { public List getMembers() { return new ArrayList<>(members); } + + @Override + public String toString() { + return String.format("%s", this.groupName); + } } diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index 6ac83a914c..959c20856f 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -1,46 +1,46 @@ -package seedu.duke; - -import java.util.Optional; - -/** - * Represents a command handler for group-related operations. - * Provides static methods to create groups, add members to groups, and exit groups. - */ -public class GroupCommand { - /** - * Creates a new group or retrieves an existing group with the specified name. - * - * @param groupName the name of the group to create or retrieve - */ - public static void createGroup(String groupName) { - try { - Group.getOrCreateGroup(groupName); - } catch (IllegalStateException e) { - System.out.println(e.getMessage()); - } - } - - /** - * Adds a member with the specified name to the current group. - * If the user is not currently in a group, prints a message asking them to create or join a group first. - * - * @param memberName the name of the member to add - */ - public static void addMember(String memberName) { - Optional currentGroup = Group.getCurrentGroup(); - if (currentGroup.isEmpty()) { - System.out.println("Please create or join a group first."); - return; - } - - currentGroup.get().addMember(memberName); - } - - /** - * Exits the current group. - * If the user is not currently in a group, prints a message indicating so. - */ - public static void exitGroup() { - Group.exitGroup(); - } -} +//package seedu.duke; +// +//import java.util.Optional; +// +///** +// * Represents a command handler for group-related operations. +// * Provides static methods to create groups, add members to groups, and exit groups. +// */ +//public class GroupCommand { +// /** +// * Creates a new group or retrieves an existing group with the specified name. +// * +// * @param groupName the name of the group to create or retrieve +// */ +// public static void createGroup(String groupName) { +// try { +// Group.getOrCreateGroup(groupName); +// } catch (IllegalStateException e) { +// System.out.println(e.getMessage()); +// } +// } +// +// /** +// * Adds a member with the specified name to the current group. +// * If the user is not currently in a group, prints a message asking them to create or join a group first. +// * +// * @param memberName the name of the member to add +// */ +// public static void addMember(String memberName) { +// Optional currentGroup = Group.getCurrentGroup(); +// if (currentGroup.isEmpty()) { +// System.out.println("Please create or join a group first."); +// return; +// } +// +// currentGroup.get().addMember(memberName); +// } +// +// /** +// * Exits the current group. +// * If the user is not currently in a group, prints a message indicating so. +// */ +// public static void exitGroup() { +// Group.exitGroup(); +// } +//} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index f3473a6110..a48bf4c507 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,8 +1,6 @@ package seedu.duke; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; public class Parser { @@ -14,6 +12,8 @@ public class Parser { private static final String[] paramKeys = {"amount", "paid", "user"}; private final String userInput; + private static Map groups = new HashMap<>(); + private static Optional currentGroup = Optional.empty(); /** * First word of user input. @@ -70,7 +70,8 @@ private HashMap> createParams() { } /** - * Process the String userInput and populates corresponding fields of Parser object. + * Process the String userInput + * populates corresponding fields of Parser object. */ public void parseUserInput() { String[] tokens = userInput.split(" ", 2); @@ -91,7 +92,8 @@ public void parseUserInput() { String subCommand = subTokens[0].toLowerCase().trim(); String subArgument = subTokens[1].trim(); - if (!subArgument.isEmpty() && params.containsKey(subCommand)){ + if (!subArgument.isEmpty() && + params.containsKey(subCommand)){ params.get(subCommand).add(subArgument); } } @@ -123,7 +125,8 @@ public String toString(){ } - public void handleUserInput() throws EndProgramException, ExpensesException { + public void handleUserInput() throws EndProgramException, + ExpensesException { switch (command) { case "bye": throw new EndProgramException(); @@ -132,13 +135,39 @@ public void handleUserInput() throws EndProgramException, ExpensesException { Help.printHelp(); break; case "create": - GroupCommand.createGroup(argument); + currentGroup = Optional.of(currentGroup.map(group -> { + System.out.printf( + "Please exit %s before creating a new one%n", + group); + return group; + }).orElseGet(() -> { + String groupName = argument; + boolean isGroupCreated = groups.containsKey(groupName); + if (isGroupCreated) { + Group queriedGroup = groups.get(groupName); + System.out.println("Group already exists! " + + "You are now in " + queriedGroup); + return queriedGroup; + } + Group createdGroup = Group.createGroup(groupName); + groups.put(groupName, createdGroup); + System.out.println("Creating new group! " + + "You are now in " + createdGroup); + return createdGroup; + })); break; case "member": - GroupCommand.addMember(argument); + String memberName = argument; + currentGroup.ifPresentOrElse(group -> group.addMember(memberName), + () -> System.out.println("You are not in a group!!")); break; case "exit": - GroupCommand.exitGroup(); + currentGroup.ifPresentOrElse( + group -> { + System.out.printf("Exiting %s%n", group); + currentGroup = Optional.empty(); + }, + () -> currentGroup = Optional.empty()); break; case "expense": @@ -146,7 +175,9 @@ public void handleUserInput() throws EndProgramException, ExpensesException { String[] expenseParams = {"amount", "paid", "user"}; for(String expenseParam : expenseParams){ if(params.get(expenseParam).isEmpty()){ - String exceptionMessage = "No description for expenses! Add /" + expenseParam; + String exceptionMessage = + "No description for expenses! Add /" + + expenseParam; throw new ExpensesException(exceptionMessage); } } @@ -156,16 +187,18 @@ public void handleUserInput() throws EndProgramException, ExpensesException { try { totalAmount = Float.parseFloat(params.get("amount").get(0)); } catch (NumberFormatException e) { - String exceptionMessage = "Re-enter expense with amount as a proper number."; + String exceptionMessage = + "Re-enter expense with amount as a proper number."; throw new ExpensesException(exceptionMessage); } - // Obtain necessary information from 'params' and create new Expense + // Obtain necessary information from 'params', create new Expense ArrayList payeeList = params.get("user"); String payerName = params.get("paid").get(0); payeeList.add(0, payerName); - Expense newTransaction = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); + Expense newTransaction = new Expense(payerName, + totalAmount, payeeList.toArray(new String[0])); break; case "list": // List code here diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 5298c9137c..7fdeec4a03 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -16,22 +16,18 @@ public void testUser() { User user = new User("John"); assertEquals("John", user.getName()); } catch (Exception e) { - fail("Exception occurred while creating a User object: " + e.getMessage()); + fail("Exception occurred while creating a User object: " + + e.getMessage()); } } @Test public void testAddUserToGroup() { String groupName = "TestGroup"; - Optional group = Group.getOrCreateGroup(groupName); - if (group.isEmpty()) { - System.out.println("Group does not exist."); - return; - } - - User user = group.get().addMember("TestUser"); - - assertTrue(group.get().getMembers().contains(user), "User was not added to the group"); + Group TestGroup = Group.createGroup(groupName); + TestGroup.addMember("TestUser"); + assertTrue(TestGroup.isMember("TestUser"), + "User was not added to the group"); } } diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 7dac8cdf70..6f92bf81ce 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -1,64 +1,64 @@ -package seedu.duke; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class GroupTest { - @BeforeEach - public void setup() { - // Reset the state before each test - Group.exitGroup(); - } - - @AfterEach - public void teardown() { - // Clean up after each test - Group.exitGroup(); - } - - @Test - public void testGroupCreation() { - String expectedName = "GroupName"; - Optional group = Group.getOrCreateGroup(expectedName); - assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); - } - - @Test - public void testGetOrCreateGroup() { - String groupName = "NewGroup"; - Optional newGroup = Group.getOrCreateGroup(groupName); - - assertEquals(groupName, newGroup.get().getGroupName(), "Group name is not the expected value"); - - Group.exitGroup(); - Optional existingGroup = Group.getOrCreateGroup(groupName); - - assertEquals(newGroup.get(), existingGroup.get(), "getOrCreateGroup should return the existing group"); - assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); - } - - @Test - public void testExitGroup() { - String groupName = "ExitingGroup"; - Group.getOrCreateGroup(groupName); - Group.exitGroup(); - assertTrue(Group.getCurrentGroup().isEmpty(), "Did not successfully exit the group"); - } - - @Test - public void testGetCurrentGroup() { - String groupName = "CurrentGroup"; - Optional group = Group.getOrCreateGroup(groupName); - - assertEquals(group.get(), Group.getCurrentGroup().get(), "Current group is not the expected group"); - - Group.exitGroup(); - assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); - } -} +//package seedu.duke; +// +//import org.junit.jupiter.api.AfterEach; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +// +//import java.util.Optional; +// +//import static org.junit.jupiter.api.Assertions.assertEquals; +//import static org.junit.jupiter.api.Assertions.assertTrue; +// +//public class GroupTest { +// @BeforeEach +// public void setup() { +// // Reset the state before each test +// Group.exitGroup(); +// } +// +// @AfterEach +// public void teardown() { +// // Clean up after each test +// Group.exitGroup(); +// } +// +// @Test +// public void testGroupCreation() { +// String expectedName = "GroupName"; +// Optional group = Group.getOrCreateGroup(expectedName); +// assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); +// } +// +// @Test +// public void testGetOrCreateGroup() { +// String groupName = "NewGroup"; +// Optional newGroup = Group.getOrCreateGroup(groupName); +// +// assertEquals(groupName, newGroup.get().getGroupName(), "Group name is not the expected value"); +// +// Group.exitGroup(); +// Optional existingGroup = Group.getOrCreateGroup(groupName); +// +// assertEquals(newGroup.get(), existingGroup.get(), "getOrCreateGroup should return the existing group"); +// assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); +// } +// +// @Test +// public void testExitGroup() { +// String groupName = "ExitingGroup"; +// Group.getOrCreateGroup(groupName); +// Group.exitGroup(); +// assertTrue(Group.getCurrentGroup().isEmpty(), "Did not successfully exit the group"); +// } +// +// @Test +// public void testGetCurrentGroup() { +// String groupName = "CurrentGroup"; +// Optional group = Group.getOrCreateGroup(groupName); +// +// assertEquals(group.get(), Group.getCurrentGroup().get(), "Current group is not the expected group"); +// +// Group.exitGroup(); +// assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); +// } +//} From 6ad73f2d146d36ba3f1cee55f83a6172edd1576d Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 27 Mar 2024 01:52:52 +0800 Subject: [PATCH 083/270] Fixed Checkstyle --- src/main/java/seedu/duke/Group.java | 3 --- src/main/java/seedu/duke/Parser.java | 32 +++++++++++------------ src/test/java/seedu/duke/AddUserTest.java | 1 - 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 868ca21b80..7e29184999 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,10 +1,7 @@ package seedu.duke; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Optional; public class Group { private final String groupName; diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index a48bf4c507..a004ba34ad 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,5 +1,4 @@ package seedu.duke; - import java.util.*; public class Parser { @@ -10,10 +9,9 @@ public class Parser { * Add new Keys to extract additional user parameters for future functionality. */ private static final String[] paramKeys = {"amount", "paid", "user"}; - - private final String userInput; private static Map groups = new HashMap<>(); private static Optional currentGroup = Optional.empty(); + private final String userInput; /** * First word of user input. @@ -139,22 +137,22 @@ public void handleUserInput() throws EndProgramException, System.out.printf( "Please exit %s before creating a new one%n", group); - return group; - }).orElseGet(() -> { - String groupName = argument; - boolean isGroupCreated = groups.containsKey(groupName); - if (isGroupCreated) { - Group queriedGroup = groups.get(groupName); - System.out.println("Group already exists! " + + return group; + }).orElseGet(() -> { + String groupName = argument; + boolean isGroupCreated = groups.containsKey(groupName); + if (isGroupCreated) { + Group queriedGroup = groups.get(groupName); + System.out.println("Group already exists! " + "You are now in " + queriedGroup); - return queriedGroup; - } - Group createdGroup = Group.createGroup(groupName); - groups.put(groupName, createdGroup); - System.out.println("Creating new group! " + + return queriedGroup; + } + Group createdGroup = Group.createGroup(groupName); + groups.put(groupName, createdGroup); + System.out.println("Creating new group! " + "You are now in " + createdGroup); - return createdGroup; - })); + return createdGroup; + })); break; case "member": String memberName = argument; diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 7fdeec4a03..1dd31a54ea 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.Test; -import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; From d29429da544776d15cca5a8097f59f7a507627ae Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 27 Mar 2024 02:00:53 +0800 Subject: [PATCH 084/270] Fixed Checkstyle again --- src/main/java/seedu/duke/Parser.java | 26 ++++++++++++----------- src/test/java/seedu/duke/AddUserTest.java | 6 +++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index a004ba34ad..0223533e4a 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,4 +1,6 @@ package seedu.duke; + + import java.util.*; public class Parser { @@ -139,20 +141,20 @@ public void handleUserInput() throws EndProgramException, group); return group; }).orElseGet(() -> { - String groupName = argument; - boolean isGroupCreated = groups.containsKey(groupName); - if (isGroupCreated) { - Group queriedGroup = groups.get(groupName); - System.out.println("Group already exists! " + + String groupName = argument; + boolean isGroupCreated = groups.containsKey(groupName); + if (isGroupCreated) { + Group queriedGroup = groups.get(groupName); + System.out.println("Group already exists! " + "You are now in " + queriedGroup); - return queriedGroup; - } - Group createdGroup = Group.createGroup(groupName); - groups.put(groupName, createdGroup); - System.out.println("Creating new group! " + + return queriedGroup; + } + Group createdGroup = Group.createGroup(groupName); + groups.put(groupName, createdGroup); + System.out.println("Creating new group! " + "You are now in " + createdGroup); - return createdGroup; - })); + return createdGroup; + })); break; case "member": String memberName = argument; diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 1dd31a54ea..21aae34775 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -22,10 +22,10 @@ public void testUser() { @Test public void testAddUserToGroup() { - String groupName = "TestGroup"; + String groupName = "testGroup"; Group TestGroup = Group.createGroup(groupName); - TestGroup.addMember("TestUser"); - assertTrue(TestGroup.isMember("TestUser"), + TestGroup.addMember("testUser"); + assertTrue(TestGroup.isMember("testUser"), "User was not added to the group"); } } From 60924e6e2baa188eb8409c8f185eca038aed1dce Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 27 Mar 2024 02:03:16 +0800 Subject: [PATCH 085/270] Fixed Checkstyle again and again --- src/main/java/seedu/duke/Parser.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 0223533e4a..c7f2791ba9 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,7 +1,11 @@ package seedu.duke; -import java.util.*; +import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; public class Parser { From 6afa5bec27daf2b6153bf9e91bd879919c28c941 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 28 Mar 2024 16:17:46 +0800 Subject: [PATCH 086/270] Add member feature in DG --- docs/DeveloperGuide.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..efba9940a0 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -8,6 +8,19 @@ {Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Add Member to Group feature +#### Implementation + +The "Add Member to Group" feature is facilitated by the `Group` class. It provides methods to manage group membership and allows users to add new members to an existing group. The implementation of this feature is as follows: + +The Group class maintains a list of members as a `private List` field called `members`. + +The `addMember(String memberName)` method is responsible for adding a new member to the group. It performs the following steps: + +1. Checks if a user with the given `memberName` is already a member of the group using the `isMember(String memberName)` method. +2. If the user is not a member, creates a new `User` object with the provided `memberName`. +3. Adds the new `User` object to the `members` list. +4. Prints a success message indicating that the member has been added to the group. ## Product scope ### Target user profile @@ -20,10 +33,10 @@ ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|----------|---------------------------|-------------------------------------------------------------| +| v1.0 | new user | see usage instructions | refer to them when I forget how to use the application | +| v2.0 | user | find a to-do item by name | locate a to-do without having to go through the entire list | ## Non-Functional Requirements From d0d2be8327d06fa199748c93b4dd53cdae38a358 Mon Sep 17 00:00:00 2001 From: MonkeScripts <104839312+MonkeScripts@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:44:56 +0800 Subject: [PATCH 087/270] Revert "Refactor group, group commands into an optional context in Parser.java" --- src/main/java/seedu/duke/Group.java | 93 ++++++++++++--- src/main/java/seedu/duke/GroupCommand.java | 92 +++++++-------- src/main/java/seedu/duke/Parser.java | 61 ++-------- src/test/java/seedu/duke/AddUserTest.java | 19 +-- src/test/java/seedu/duke/GroupTest.java | 128 ++++++++++----------- 5 files changed, 212 insertions(+), 181 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 7e29184999..4293e5076b 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,9 +1,15 @@ package seedu.duke; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; public class Group { + private static final Map groups = new HashMap<>(); + private static Optional currentGroupName = Optional.empty(); + private final String groupName; private final List members; @@ -13,13 +19,74 @@ private Group(String groupName) { } /** - * Factory method to create a new Group + * Retrieves an existing group by its name or creates a new one if it does not exist. + * It ensures that a user cannot create or join a new group without exiting their current group. * * @param groupName The name of the group to get or create. - * @return Newly created group. + * @return The existing or newly created group. + * @throws IllegalStateException If trying to create or join a new group while already in another group. + */ + public static Optional getOrCreateGroup(String groupName) { + // Check if user is accessing a group they are already in + getCurrentGroup().ifPresent(currentGroup -> { + if (currentGroup.getGroupName().equals(groupName)) { + System.out.println("You are in " + groupName); + } + }); + + // If the user is in a different group, prevent them from creating or joining a new group. + if (isInGroup()) { + getCurrentGroup().ifPresent(currentGroup -> { + if (!currentGroup.getGroupName().equals(groupName)) { + throw new IllegalStateException("Please exit the current group '" + currentGroup.getGroupName() + + "' to create or join another group."); + } + }); + } + + Optional group = Optional.ofNullable(groups.get(groupName)); + + if (group.isEmpty()) { + Group newGroup = new Group(groupName); + groups.put(groupName, newGroup); + System.out.println(groupName + " created."); + currentGroupName = Optional.of(groupName); + group = Optional.of(newGroup); + } + + System.out.println("You are now in " + groupName); + return group; + } + + /** + * Exits the current group. + * If the user is not in any group, it displays a message asking the user to try again. + */ + public static void exitGroup() { + if (currentGroupName.isPresent()) { + System.out.println("You have exited " + currentGroupName.get() + "."); + currentGroupName = Optional.empty(); + } else { + System.out.println("You are not currently in any group. Please try again."); + } + } + + /** + * Retrieves the current group. + * + * @return The current group, or null if the user is not in any group. */ - public static Group createGroup(String groupName) { - return new Group(groupName); + public static Optional getCurrentGroup() { + return currentGroupName.map(groups::get); + } + + /** + * Checks if the user is currently in a group. + * + * @return true if the user is in a group, false otherwise. + */ + public static boolean isInGroup() { + return currentGroupName.isPresent(); } /** @@ -41,17 +108,18 @@ public boolean isMember(String memberName) { * Adds a new member to the group. * * @param memberName The name of the member to add. + * @return The newly added user, or null if the user is already a member of the group. */ - public void addMember(String memberName) { + public User addMember(String memberName) { if (isMember(memberName)) { - System.out.println( - memberName + " is already a member of " + groupName + "."); - return; + System.out.println(memberName + " is already a member of " + groupName + "."); + return null; } + User newMember = new User(memberName); members.add(newMember); - System.out.println( - memberName + " has been added to " + groupName + "."); + System.out.println(memberName + " has been added to " + groupName + "."); + return newMember; } /** @@ -71,9 +139,4 @@ public String getGroupName() { public List getMembers() { return new ArrayList<>(members); } - - @Override - public String toString() { - return String.format("%s", this.groupName); - } } diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index 959c20856f..6ac83a914c 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -1,46 +1,46 @@ -//package seedu.duke; -// -//import java.util.Optional; -// -///** -// * Represents a command handler for group-related operations. -// * Provides static methods to create groups, add members to groups, and exit groups. -// */ -//public class GroupCommand { -// /** -// * Creates a new group or retrieves an existing group with the specified name. -// * -// * @param groupName the name of the group to create or retrieve -// */ -// public static void createGroup(String groupName) { -// try { -// Group.getOrCreateGroup(groupName); -// } catch (IllegalStateException e) { -// System.out.println(e.getMessage()); -// } -// } -// -// /** -// * Adds a member with the specified name to the current group. -// * If the user is not currently in a group, prints a message asking them to create or join a group first. -// * -// * @param memberName the name of the member to add -// */ -// public static void addMember(String memberName) { -// Optional currentGroup = Group.getCurrentGroup(); -// if (currentGroup.isEmpty()) { -// System.out.println("Please create or join a group first."); -// return; -// } -// -// currentGroup.get().addMember(memberName); -// } -// -// /** -// * Exits the current group. -// * If the user is not currently in a group, prints a message indicating so. -// */ -// public static void exitGroup() { -// Group.exitGroup(); -// } -//} +package seedu.duke; + +import java.util.Optional; + +/** + * Represents a command handler for group-related operations. + * Provides static methods to create groups, add members to groups, and exit groups. + */ +public class GroupCommand { + /** + * Creates a new group or retrieves an existing group with the specified name. + * + * @param groupName the name of the group to create or retrieve + */ + public static void createGroup(String groupName) { + try { + Group.getOrCreateGroup(groupName); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Adds a member with the specified name to the current group. + * If the user is not currently in a group, prints a message asking them to create or join a group first. + * + * @param memberName the name of the member to add + */ + public static void addMember(String memberName) { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + System.out.println("Please create or join a group first."); + return; + } + + currentGroup.get().addMember(memberName); + } + + /** + * Exits the current group. + * If the user is not currently in a group, prints a message indicating so. + */ + public static void exitGroup() { + Group.exitGroup(); + } +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index c7f2791ba9..f3473a6110 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,11 +1,8 @@ package seedu.duke; - -import java.util.List; import java.util.ArrayList; import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.List; public class Parser { @@ -15,8 +12,7 @@ public class Parser { * Add new Keys to extract additional user parameters for future functionality. */ private static final String[] paramKeys = {"amount", "paid", "user"}; - private static Map groups = new HashMap<>(); - private static Optional currentGroup = Optional.empty(); + private final String userInput; /** @@ -74,8 +70,7 @@ private HashMap> createParams() { } /** - * Process the String userInput - * populates corresponding fields of Parser object. + * Process the String userInput and populates corresponding fields of Parser object. */ public void parseUserInput() { String[] tokens = userInput.split(" ", 2); @@ -96,8 +91,7 @@ public void parseUserInput() { String subCommand = subTokens[0].toLowerCase().trim(); String subArgument = subTokens[1].trim(); - if (!subArgument.isEmpty() && - params.containsKey(subCommand)){ + if (!subArgument.isEmpty() && params.containsKey(subCommand)){ params.get(subCommand).add(subArgument); } } @@ -129,8 +123,7 @@ public String toString(){ } - public void handleUserInput() throws EndProgramException, - ExpensesException { + public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": throw new EndProgramException(); @@ -139,39 +132,13 @@ public void handleUserInput() throws EndProgramException, Help.printHelp(); break; case "create": - currentGroup = Optional.of(currentGroup.map(group -> { - System.out.printf( - "Please exit %s before creating a new one%n", - group); - return group; - }).orElseGet(() -> { - String groupName = argument; - boolean isGroupCreated = groups.containsKey(groupName); - if (isGroupCreated) { - Group queriedGroup = groups.get(groupName); - System.out.println("Group already exists! " + - "You are now in " + queriedGroup); - return queriedGroup; - } - Group createdGroup = Group.createGroup(groupName); - groups.put(groupName, createdGroup); - System.out.println("Creating new group! " + - "You are now in " + createdGroup); - return createdGroup; - })); + GroupCommand.createGroup(argument); break; case "member": - String memberName = argument; - currentGroup.ifPresentOrElse(group -> group.addMember(memberName), - () -> System.out.println("You are not in a group!!")); + GroupCommand.addMember(argument); break; case "exit": - currentGroup.ifPresentOrElse( - group -> { - System.out.printf("Exiting %s%n", group); - currentGroup = Optional.empty(); - }, - () -> currentGroup = Optional.empty()); + GroupCommand.exitGroup(); break; case "expense": @@ -179,9 +146,7 @@ public void handleUserInput() throws EndProgramException, String[] expenseParams = {"amount", "paid", "user"}; for(String expenseParam : expenseParams){ if(params.get(expenseParam).isEmpty()){ - String exceptionMessage = - "No description for expenses! Add /" + - expenseParam; + String exceptionMessage = "No description for expenses! Add /" + expenseParam; throw new ExpensesException(exceptionMessage); } } @@ -191,18 +156,16 @@ public void handleUserInput() throws EndProgramException, try { totalAmount = Float.parseFloat(params.get("amount").get(0)); } catch (NumberFormatException e) { - String exceptionMessage = - "Re-enter expense with amount as a proper number."; + String exceptionMessage = "Re-enter expense with amount as a proper number."; throw new ExpensesException(exceptionMessage); } - // Obtain necessary information from 'params', create new Expense + // Obtain necessary information from 'params' and create new Expense ArrayList payeeList = params.get("user"); String payerName = params.get("paid").get(0); payeeList.add(0, payerName); - Expense newTransaction = new Expense(payerName, - totalAmount, payeeList.toArray(new String[0])); + Expense newTransaction = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); break; case "list": // List code here diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 21aae34775..5298c9137c 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,18 +16,22 @@ public void testUser() { User user = new User("John"); assertEquals("John", user.getName()); } catch (Exception e) { - fail("Exception occurred while creating a User object: " + - e.getMessage()); + fail("Exception occurred while creating a User object: " + e.getMessage()); } } @Test public void testAddUserToGroup() { - String groupName = "testGroup"; - Group TestGroup = Group.createGroup(groupName); - TestGroup.addMember("testUser"); - assertTrue(TestGroup.isMember("testUser"), - "User was not added to the group"); + String groupName = "TestGroup"; + Optional group = Group.getOrCreateGroup(groupName); + if (group.isEmpty()) { + System.out.println("Group does not exist."); + return; + } + + User user = group.get().addMember("TestUser"); + + assertTrue(group.get().getMembers().contains(user), "User was not added to the group"); } } diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 6f92bf81ce..7dac8cdf70 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -1,64 +1,64 @@ -//package seedu.duke; -// -//import org.junit.jupiter.api.AfterEach; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.Test; -// -//import java.util.Optional; -// -//import static org.junit.jupiter.api.Assertions.assertEquals; -//import static org.junit.jupiter.api.Assertions.assertTrue; -// -//public class GroupTest { -// @BeforeEach -// public void setup() { -// // Reset the state before each test -// Group.exitGroup(); -// } -// -// @AfterEach -// public void teardown() { -// // Clean up after each test -// Group.exitGroup(); -// } -// -// @Test -// public void testGroupCreation() { -// String expectedName = "GroupName"; -// Optional group = Group.getOrCreateGroup(expectedName); -// assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); -// } -// -// @Test -// public void testGetOrCreateGroup() { -// String groupName = "NewGroup"; -// Optional newGroup = Group.getOrCreateGroup(groupName); -// -// assertEquals(groupName, newGroup.get().getGroupName(), "Group name is not the expected value"); -// -// Group.exitGroup(); -// Optional existingGroup = Group.getOrCreateGroup(groupName); -// -// assertEquals(newGroup.get(), existingGroup.get(), "getOrCreateGroup should return the existing group"); -// assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); -// } -// -// @Test -// public void testExitGroup() { -// String groupName = "ExitingGroup"; -// Group.getOrCreateGroup(groupName); -// Group.exitGroup(); -// assertTrue(Group.getCurrentGroup().isEmpty(), "Did not successfully exit the group"); -// } -// -// @Test -// public void testGetCurrentGroup() { -// String groupName = "CurrentGroup"; -// Optional group = Group.getOrCreateGroup(groupName); -// -// assertEquals(group.get(), Group.getCurrentGroup().get(), "Current group is not the expected group"); -// -// Group.exitGroup(); -// assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); -// } -//} +package seedu.duke; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GroupTest { + @BeforeEach + public void setup() { + // Reset the state before each test + Group.exitGroup(); + } + + @AfterEach + public void teardown() { + // Clean up after each test + Group.exitGroup(); + } + + @Test + public void testGroupCreation() { + String expectedName = "GroupName"; + Optional group = Group.getOrCreateGroup(expectedName); + assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); + } + + @Test + public void testGetOrCreateGroup() { + String groupName = "NewGroup"; + Optional newGroup = Group.getOrCreateGroup(groupName); + + assertEquals(groupName, newGroup.get().getGroupName(), "Group name is not the expected value"); + + Group.exitGroup(); + Optional existingGroup = Group.getOrCreateGroup(groupName); + + assertEquals(newGroup.get(), existingGroup.get(), "getOrCreateGroup should return the existing group"); + assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); + } + + @Test + public void testExitGroup() { + String groupName = "ExitingGroup"; + Group.getOrCreateGroup(groupName); + Group.exitGroup(); + assertTrue(Group.getCurrentGroup().isEmpty(), "Did not successfully exit the group"); + } + + @Test + public void testGetCurrentGroup() { + String groupName = "CurrentGroup"; + Optional group = Group.getOrCreateGroup(groupName); + + assertEquals(group.get(), Group.getCurrentGroup().get(), "Current group is not the expected group"); + + Group.exitGroup(); + assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); + } +} From f83d95a1fa48ab0788e0b393172bc914ac721b7b Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Thu, 28 Mar 2024 17:53:23 +0800 Subject: [PATCH 088/270] Add Expense feature in implementation section and populate v1.0 user stories in developer guide --- docs/DeveloperGuide.md | 25 +++++++++++++++++++---- src/main/java/seedu/duke/Expense.java | 4 ++-- src/test/java/seedu/duke/AddUserTest.java | 6 +++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..32b8c5e389 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -8,6 +8,19 @@ {Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +This section shows important details on implementation of certain features + +### Expenses feature +#### Implementation +The Expenses feature is facilitated through the Expense class. It allows users to add a new Expense through creation of a new Expense object. Users can specify amount paid, the payee, and the members of the group involved in the transaction. +Additionally, it implements the following operations: ++ `Expenses#payer()` - Gives the name of the member who paid for the expense ++ `Expenses#totalAmount()` - Returns the total amount of the expense ++ `Expenses#payees()` - Returns all the members involved in the transaction + +These operations are exposed in the Expense class through the `getPayerName()`, `getTotalAmount()`, and `getPayees()` functions respectively. + + ## Product scope ### Target user profile @@ -20,10 +33,14 @@ ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|----------|----------------------------------------------------------------|-------------------------------------------------------------| +| v1.0 | new user | see usage instructions | refer to them when I forget how to use the application | +| v1.0 | user | add a new expense with description, amount, and users involved | split the expense equally | +| v1.0 | user | create a new group | split expenses with different groups | +| v1.0 | user | list all expenses within a group | see recent transactions | +| v1.0 | user | check how much I owe each member in the group | keep track of my debts | +| v2.0 | user | find a to-do item by name | locate a to-do without having to go through the entire list | ## Non-Functional Requirements diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 5cb8dbeb31..2173b9b721 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -16,8 +16,8 @@ public class Expense { * Constructor to create new Expense * @param payerName : The name of the user who paid for the Expense * @param totalAmount : The total amount before being divided - * @param payeeList : String array of people who owe the payer money - * (Index 0 is the payer and will not be added to the payee list) + * @param payeeList : String array of people who are involved in the transaction + * (Index 0 is the payer and will also be added to the payee list) */ Expense(String payerName, float totalAmount, String[] payeeList) { payees.addAll(Arrays.asList(payeeList)); diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java index 21aae34775..e1e55a64a0 100644 --- a/src/test/java/seedu/duke/AddUserTest.java +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -23,9 +23,9 @@ public void testUser() { @Test public void testAddUserToGroup() { String groupName = "testGroup"; - Group TestGroup = Group.createGroup(groupName); - TestGroup.addMember("testUser"); - assertTrue(TestGroup.isMember("testUser"), + Group testGroup = Group.createGroup(groupName); + testGroup.addMember("testUser"); + assertTrue(testGroup.isMember("testUser"), "User was not added to the group"); } } From c9cefa7dba276e09f8433de30db35ef54879a53d Mon Sep 17 00:00:00 2001 From: avrilgk Date: Thu, 28 Mar 2024 23:13:37 +0800 Subject: [PATCH 089/270] DG Update and name Author --- docs/DeveloperGuide.md | 34 +++++++++++++++++++--- src/main/java/seedu/duke/Group.java | 20 +++++++++++++ src/main/java/seedu/duke/GroupCommand.java | 12 ++++++++ src/main/java/seedu/duke/Parser.java | 23 ++++++++------- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index efba9940a0..44c313f45b 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,27 +2,53 @@ ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the +original source as well} ## Design & implementation {Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Group Creation feature + +#### Implementation + +The "Group Creation" feature is facilitated by the `Group` class. It provides methods to create a new group and manage +group membership. The implementation of this feature is as follows: + +The Group class maintains a list of members as a `private List` field called `members`. + +The `createGroup(String groupName)` method is responsible for creating a new group. It performs the following steps: + +1. Checks if a group with the given `groupName` already exists using the `isGroup(String groupName)` method. +2. If the group does not exist, creates a new `Group` object with the provided `groupName`. +3. Prints a success message indicating that the group has been created. +4. Adds the new `Group` object to the `groups` list. +5. Returns the newly created `Group` object. +6. If the group already exists, prints an error message indicating that the group already exists. + +# + ### Add Member to Group feature + #### Implementation -The "Add Member to Group" feature is facilitated by the `Group` class. It provides methods to manage group membership and allows users to add new members to an existing group. The implementation of this feature is as follows: +The "Add Member to Group" feature is facilitated by the `Group` class. It provides methods to manage group membership +and allows users to add new members to an existing group. The implementation of this feature is as follows: The Group class maintains a list of members as a `private List` field called `members`. -The `addMember(String memberName)` method is responsible for adding a new member to the group. It performs the following steps: +The `addMember(String memberName)` method is responsible for adding a new member to the group. It performs the following +steps: -1. Checks if a user with the given `memberName` is already a member of the group using the `isMember(String memberName)` method. +1. Checks if a user with the given `memberName` is already a member of the group using the `isMember(String memberName)` + method. 2. If the user is not a member, creates a new `User` object with the provided `memberName`. 3. Adds the new `User` object to the `members` list. 4. Prints a success message indicating that the member has been added to the group. ## Product scope + ### Target user profile {Describe the target user profile} diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 4293e5076b..974900ad84 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,3 +1,5 @@ +//@@ author avrilgk + package seedu.duke; import java.util.ArrayList; @@ -58,6 +60,24 @@ public static Optional getOrCreateGroup(String groupName) { return group; } + /** + * Enter existing group. + * + * @param groupName The name of the group to enter. + * @return The existing group. + */ + + public static Optional enterGroup(String groupName) { + Optional group = Optional.ofNullable(groups.get(groupName)); + if (group.isEmpty()) { + System.out.println("Group does not exist."); + return group; + } + currentGroupName = Optional.of(groupName); + System.out.println("You are now in " + groupName); + return group; + } + /** * Exits the current group. * If the user is not in any group, it displays a message asking the user to try again. diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index 6ac83a914c..71504e5d93 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -1,3 +1,5 @@ +//@@ author hafizuddin-a + package seedu.duke; import java.util.Optional; @@ -36,6 +38,16 @@ public static void addMember(String memberName) { currentGroup.get().addMember(memberName); } + /** + * Enters an existing group with the specified name. + * + * @param groupName the name of the group to enter + */ + + public static void enterGroup(String groupName) { + Group.enterGroup(groupName); + } + /** * Exits the current group. * If the user is not currently in a group, prints a message indicating so. diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index f3473a6110..39a9709b43 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -62,7 +62,7 @@ public Parser(String userInput) { private HashMap> createParams() { HashMap> additionalInfo = new HashMap<>(); - for(String paramKey : paramKeys){ + for (String paramKey : paramKeys) { additionalInfo.put(paramKey, new ArrayList<>()); } @@ -76,22 +76,22 @@ public void parseUserInput() { String[] tokens = userInput.split(" ", 2); this.command = tokens[0].toLowerCase().trim(); - if (tokens.length == 1){ + if (tokens.length == 1) { return; } String[] arguments = tokens[1].split("/"); this.argument = arguments[0].trim(); - for(int i = 1; i < arguments.length; i++){ + for (int i = 1; i < arguments.length; i++) { String[] subTokens = arguments[i].split(" ", 2); - if (subTokens.length == 1){ + if (subTokens.length == 1) { continue; } String subCommand = subTokens[0].toLowerCase().trim(); String subArgument = subTokens[1].trim(); - if (!subArgument.isEmpty() && params.containsKey(subCommand)){ + if (!subArgument.isEmpty() && params.containsKey(subCommand)) { params.get(subCommand).add(subArgument); } } @@ -104,16 +104,16 @@ public void parseUserInput() { * @return Contents of Parser object. */ @Override - public String toString(){ + public String toString() { StringBuilder parser = new StringBuilder(); parser.append("command: ").append(command).append("\n"); parser.append("argument: ").append(argument).append("\n"); - for(String paramKey : paramKeys){ + for (String paramKey : paramKeys) { parser.append(paramKey).append(": "); - for(String item : params.get(paramKey)){ + for (String item : params.get(paramKey)) { parser.append(item).append(" "); } parser.append("\n"); @@ -137,6 +137,9 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "member": GroupCommand.addMember(argument); break; + case "enter": + GroupCommand.enterGroup(argument); + break; case "exit": GroupCommand.exitGroup(); break; @@ -144,8 +147,8 @@ public void handleUserInput() throws EndProgramException, ExpensesException { // Checks for missing Expense Parameters String[] expenseParams = {"amount", "paid", "user"}; - for(String expenseParam : expenseParams){ - if(params.get(expenseParam).isEmpty()){ + for (String expenseParam : expenseParams) { + if (params.get(expenseParam).isEmpty()) { String exceptionMessage = "No description for expenses! Add /" + expenseParam; throw new ExpensesException(exceptionMessage); } From 2bf42b7b6e7078eb82c5d060b0260bc3967122ec Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Thu, 28 Mar 2024 23:27:28 +0800 Subject: [PATCH 090/270] edit DeveloperGuide.md --- docs/DeveloperGuide.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index efba9940a0..1fe7d99dfd 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -7,6 +7,12 @@ ## Design & implementation {Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Help menu feature +#### Implementation +The "help" feature is facilitated by the `Help` class. +It provides a static method `printHelp` to print out a guide on how to use the commands in the application. +`printHelp` can be used in the event the user issues an invalid command + ### Add Member to Group feature #### Implementation @@ -26,6 +32,8 @@ The `addMember(String memberName)` method is responsible for adding a new member ### Target user profile {Describe the target user profile} +Our target users are people who share expenses with friends, family, roommates, or colleagues. +The application gives an accurate and simple way to represent unsettled debts between users and their friends ### Value proposition From ad9c0692741215caefa66af82d5a052d26856e15 Mon Sep 17 00:00:00 2001 From: Cohii Date: Thu, 28 Mar 2024 23:37:03 +0800 Subject: [PATCH 091/270] Integrate Balance class with Group and Expense classes Add new expenseList param to Group class. Added checks if user is currently in a group to expense and balance switch statements in Parser. Made printBalance() callable from balance switch statement in Parser. Change var type from ArrayList to List in Balance methods and edited BalanceTest. --- src/main/java/seedu/duke/Balance.java | 10 ++++++--- src/main/java/seedu/duke/Group.java | 20 +++++++++++++++++ src/main/java/seedu/duke/Parser.java | 26 ++++++++++++++++++++++- src/test/java/seedu/duke/BalanceTest.java | 5 +++-- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index 4423d2bc11..447d112b13 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -1,7 +1,7 @@ package seedu.duke; -import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class Balance { @@ -13,7 +13,11 @@ public Balance(String userName, Map userList) { this.balanceList = userList; } - public Balance(String userName, ArrayList expenses, ArrayList users) { + public Balance(String userName, Group group){ + this(userName, group.getExpenseList(), group.getMembers()); + } + + public Balance(String userName, List expenses, List users) { this.userName = userName; this.balanceList = new HashMap<>(); @@ -39,7 +43,7 @@ public Map getBalanceList() { } private void addExpense(Expense expense) { - ArrayList payees = expense.getPayees(); + List payees = expense.getPayees(); int numberOfUsers = payees.size(); Float amountPerUser = expense.getTotalAmount() / numberOfUsers; diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 4293e5076b..6d9c3ec280 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -12,10 +12,12 @@ public class Group { private final String groupName; private final List members; + private final List expenseList; private Group(String groupName) { this.groupName = groupName; this.members = new ArrayList<>(); + this.expenseList = new ArrayList<>(); } /** @@ -122,6 +124,15 @@ public User addMember(String memberName) { return newMember; } + /** + * Adds a new expense to the group. + * + * @param expense The Expense object to add. + */ + public void addExpense(Expense expense) { + expenseList.add(expense); + } + /** * Retrieves the name of the group. * @@ -139,4 +150,13 @@ public String getGroupName() { public List getMembers() { return new ArrayList<>(members); } + + /** + * Retrieves the list of expenses in the group. + * + * @return The list of expenses in the group. + */ + public List getExpenseList() { + return new ArrayList<>(expenseList); + } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index f3473a6110..80be976121 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Optional; public class Parser { @@ -141,6 +142,12 @@ public void handleUserInput() throws EndProgramException, ExpensesException { GroupCommand.exitGroup(); break; case "expense": + // Checks if user is currently in a Group + Optional currentGroup = Group.getCurrentGroup(); + if(currentGroup.isEmpty()){ + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } // Checks for missing Expense Parameters String[] expenseParams = {"amount", "paid", "user"}; @@ -166,12 +173,29 @@ public void handleUserInput() throws EndProgramException, ExpensesException { payeeList.add(0, payerName); Expense newTransaction = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); + currentGroup.get().addExpense(newTransaction); + break; case "list": // List code here break; case "balance": - // Balance code here + // Checks if user is currently in a Group + // named 'currentGroup1' to prevent conflict with previous declaration + Optional currentGroup1 = Group.getCurrentGroup(); + if(currentGroup1.isEmpty()){ + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + + // Checks if user specified is in Current Group + if(!currentGroup1.get().isMember(argument)){ + String exceptionMessage = argument + " is not in current Group!"; + throw new ExpensesException(exceptionMessage); + } + + Balance balance = new Balance(argument, currentGroup1.get()); + balance.printBalance(); break; default: // Default clause diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 24fc583573..598604ad0a 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -4,16 +4,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; +import java.util.List; public class BalanceTest { @Test public void testConstructor() { - ArrayList users = new ArrayList<>(); + List users = new ArrayList<>(); users.add(new User("member1")); users.add(new User("member2")); users.add(new User("member3")); - ArrayList expenses = new ArrayList<>(); + List expenses = new ArrayList<>(); expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); From 76dab76361e143e0af5ecf84fd4dd3a991c3485d Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Fri, 29 Mar 2024 01:39:46 +0800 Subject: [PATCH 092/270] assert true == true --- src/main/java/seedu/duke/Parser.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 39a9709b43..1641ccaca4 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -130,6 +130,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "help": // Help code here Help.printHelp(); + assert(true); break; case "create": GroupCommand.createGroup(argument); From 495fffd9839372376bad261a6f1136d3570f26da Mon Sep 17 00:00:00 2001 From: Cohii Date: Fri, 29 Mar 2024 03:26:34 +0800 Subject: [PATCH 093/270] Update docs Update developer guide with Balance class functionality. --- docs/DeveloperGuide.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 4e84fe2734..3d8101bd6f 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -63,6 +63,22 @@ Additionally, it implements the following operations: These operations are exposed in the Expense class through the `getPayerName()`, `getTotalAmount()`, and `getPayees()` functions respectively. +### Balance feature + +#### Implementation + +The Balance feature is facilitated through the Balance class. +It allows a user to view a printed list of other users in the Group, and the amount that is owed by/to each user. + +Each `Balance` object contains a String of a user `userName`, +and a Map `balanceList`. This Map uses String of other users' usernames as Key, and +a Float of the amount that is owed by/to each user. + +To print a user's Balance List, perform the following steps: +1. Create a `Balance` object with the params String `userName` and the current Group `group`. +2. From the `members` and `expenseList` List items in `group`, the Map `balanceList` is populated. +3. Call method `printBalance()` to print the contents of the Map `balanceList`. + ## Product scope ### Target user profile From a6489e0de30e83b4b6e1dc337f375ecb929d5e8b Mon Sep 17 00:00:00 2001 From: avrilgk Date: Fri, 29 Mar 2024 22:55:15 +0800 Subject: [PATCH 094/270] Delete group function --- src/main/java/seedu/duke/Group.java | 5 +++-- src/main/java/seedu/duke/GroupCommand.java | 22 +++++++++++++++++++++- src/main/java/seedu/duke/Parser.java | 11 +++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 2724e41c3a..fbc43ce4f8 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -1,4 +1,4 @@ -//@@ author avrilgk +//@@author avrilgk package seedu.duke; @@ -9,7 +9,7 @@ import java.util.Optional; public class Group { - private static final Map groups = new HashMap<>(); + static final Map groups = new HashMap<>(); private static Optional currentGroupName = Optional.empty(); private final String groupName; @@ -180,3 +180,4 @@ public List getExpenseList() { return new ArrayList<>(expenseList); } } + diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index 71504e5d93..f1d548e141 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -1,4 +1,4 @@ -//@@ author hafizuddin-a +//@@author hafizuddin-a package seedu.duke; @@ -22,6 +22,25 @@ public static void createGroup(String groupName) { } } + //@@author avrilgk + + /** + * Deletes the current group. + * If the user is not currently in a group, prints a message indicating so. + * + * @param groupName the name of the group to delete + */ + public static void deleteGroup(String groupName) { + if (Group.groups.containsKey(groupName)) { + Group.groups.remove(groupName); + System.out.println("The group " + groupName + " has been deleted."); + } else { + System.out.println("The group " + groupName + " does not exist."); + } + } + + //@@author hafizuddin-a + /** * Adds a member with the specified name to the current group. * If the user is not currently in a group, prints a message asking them to create or join a group first. @@ -56,3 +75,4 @@ public static void exitGroup() { Group.exitGroup(); } } + diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 1eb086193c..74b2003166 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -131,11 +131,14 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "help": // Help code here Help.printHelp(); - assert(true); + assert (true); break; case "create": GroupCommand.createGroup(argument); break; + case "delete": + GroupCommand.deleteGroup(argument); + break; case "member": GroupCommand.addMember(argument); break; @@ -148,7 +151,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { case "expense": // Checks if user is currently in a Group Optional currentGroup = Group.getCurrentGroup(); - if(currentGroup.isEmpty()){ + if (currentGroup.isEmpty()) { String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; throw new ExpensesException(exceptionMessage); } @@ -187,13 +190,13 @@ public void handleUserInput() throws EndProgramException, ExpensesException { // Checks if user is currently in a Group // named 'currentGroup1' to prevent conflict with previous declaration Optional currentGroup1 = Group.getCurrentGroup(); - if(currentGroup1.isEmpty()){ + if (currentGroup1.isEmpty()) { String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; throw new ExpensesException(exceptionMessage); } // Checks if user specified is in Current Group - if(!currentGroup1.get().isMember(argument)){ + if (!currentGroup1.get().isMember(argument)) { String exceptionMessage = argument + " is not in current Group!"; throw new ExpensesException(exceptionMessage); } From c7fb57267f1f2f5fe131f323eed372b8966ba43e Mon Sep 17 00:00:00 2001 From: avrilgk Date: Fri, 29 Mar 2024 22:58:23 +0800 Subject: [PATCH 095/270] Delete group function --- src/test/java/seedu/duke/GroupTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 7dac8cdf70..f8318b2987 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -1,3 +1,5 @@ +//@@author avrilgk + package seedu.duke; import org.junit.jupiter.api.AfterEach; From d93c0ddaf551891129797ae151e93344e00f3a3e Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Fri, 29 Mar 2024 23:26:34 +0800 Subject: [PATCH 096/270] Add GroupStorage class for saving and loading group data Implement methods to save and load group information to/from a text file Structure the saved data with group name, members, and expenses: GroupName Members: Member1 Member2 ... Expenses: TotalAmount1,PayerName1,Payee1,Payee2,... TotalAmount2,PayerName2,Payee1,Payee2,... --- src/main/java/seedu/duke/GroupStorage.java | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/main/java/seedu/duke/GroupStorage.java diff --git a/src/main/java/seedu/duke/GroupStorage.java b/src/main/java/seedu/duke/GroupStorage.java new file mode 100644 index 0000000000..5ecc7961f1 --- /dev/null +++ b/src/main/java/seedu/duke/GroupStorage.java @@ -0,0 +1,133 @@ +package seedu.duke; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class GroupStorage { + private static final String GROUPS_DIRECTORY = "data/groups"; + private static final String GROUP_FILE_EXTENSION = ".txt"; + private static final String MEMBERS_HEADER = "Members:"; + private static final String EXPENSES_HEADER = "Expenses:"; + + /** + * Saves the group information to a file. + * + * @param group The group to save. + */ + public void saveGroup(Group group) { + String groupName = group.getGroupName(); + List members = group.getMembers(); + List expenses = group.getExpenseList(); + + try { + createGroupDirectory(); + String filePath = getGroupFilePath(groupName); + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath)); + + // Write group name + writer.write(groupName); + writer.newLine(); + + // Write members + writer.write(MEMBERS_HEADER); + writer.newLine(); + for (User member : members) { + writer.write(member.getName()); + writer.newLine(); + } + + // Write expenses + writer.write(EXPENSES_HEADER); + writer.newLine(); + for (Expense expense : expenses) { + String expenseData = String.format("%.2f,%s,%s", expense.getTotalAmount(), expense.getPayerName(), + String.join(",", expense.getPayees())); + writer.write(expenseData); + writer.newLine(); + } + + writer.close(); + System.out.println("Group information saved successfully."); + } catch (IOException e) { + System.out.println("An error occurred while saving the group information."); + } + } + + /** + * Loads the group information from a file. + * + * @param groupName The name of the group to load. + * @return The loaded group, or null if the group file does not exist. + */ + public Group loadGroup(String groupName) { + String filePath = getGroupFilePath(groupName); + Path path = Paths.get(filePath); + + if (!Files.exists(path)) { + System.out.println("Group file does not exist."); + return null; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String name = reader.readLine(); + Group group = Group.getOrCreateGroup(name).orElse(null); + + // Skip the "Members:" header + reader.readLine(); + + // Read members + String line; + while ((line = reader.readLine()) != null && !line.equals(EXPENSES_HEADER)) { + group.addMember(line); + } + + // Read expenses + while ((line = reader.readLine()) != null) { + String[] expenseData = line.split(","); + float totalAmount = Float.parseFloat(expenseData[0]); + String payerName = expenseData[1]; + String[] payeeList = expenseData[2].split(","); + Expense expense = new Expense(payerName, totalAmount, payeeList); + group.addExpense(expense); + } + + System.out.println("Group information loaded successfully."); + return group; + } catch (IOException e) { + System.out.println("An error occurred while loading the group information."); + } + + return null; + } + + /** + * Creates the groups directory if it does not exist. + */ + private void createGroupDirectory() { + Path path = Paths.get(GROUPS_DIRECTORY); + if (!Files.exists(path)) { + try { + Files.createDirectories(path); + } catch (IOException e) { + System.out.println("An error occurred while creating the groups directory."); + } + } + } + + /** + * Returns the file path for the group file. + * + * @param groupName The name of the group. + * @return The file path for the group file. + */ + private String getGroupFilePath(String groupName) { + return GROUPS_DIRECTORY + "/" + groupName + GROUP_FILE_EXTENSION; + } +} From 85b1b7f75aeebdda295139c44402cdaf1d14e2da Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 00:12:49 +0800 Subject: [PATCH 097/270] Add loading and saving group data in Group class --- src/main/java/seedu/duke/Group.java | 15 +++++++++++++-- src/main/java/seedu/duke/GroupStorage.java | 9 +++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 2724e41c3a..307c8ea9dd 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -72,8 +72,18 @@ public static Optional getOrCreateGroup(String groupName) { public static Optional enterGroup(String groupName) { Optional group = Optional.ofNullable(groups.get(groupName)); if (group.isEmpty()) { - System.out.println("Group does not exist."); - return group; + //@@ author hafizuddin-a + // If the group doesn't exist in memory, try loading it from file + Group loadedGroup = GroupStorage.loadGroup(groupName); + + if (loadedGroup != null) { + groups.put(groupName, loadedGroup); + group = Optional.of(loadedGroup); + } else { + //@@ author avrilgk + System.out.println("Group does not exist."); + return group; + } } currentGroupName = Optional.of(groupName); System.out.println("You are now in " + groupName); @@ -86,6 +96,7 @@ public static Optional enterGroup(String groupName) { */ public static void exitGroup() { if (currentGroupName.isPresent()) { + GroupStorage.saveGroup(groups.get(currentGroupName.get())); System.out.println("You have exited " + currentGroupName.get() + "."); currentGroupName = Optional.empty(); } else { diff --git a/src/main/java/seedu/duke/GroupStorage.java b/src/main/java/seedu/duke/GroupStorage.java index 5ecc7961f1..afe623ea09 100644 --- a/src/main/java/seedu/duke/GroupStorage.java +++ b/src/main/java/seedu/duke/GroupStorage.java @@ -1,3 +1,4 @@ +//@@ author hafizuddin-a package seedu.duke; import java.io.BufferedReader; @@ -21,7 +22,7 @@ public class GroupStorage { * * @param group The group to save. */ - public void saveGroup(Group group) { + public static void saveGroup(Group group) { String groupName = group.getGroupName(); List members = group.getMembers(); List expenses = group.getExpenseList(); @@ -66,7 +67,7 @@ public void saveGroup(Group group) { * @param groupName The name of the group to load. * @return The loaded group, or null if the group file does not exist. */ - public Group loadGroup(String groupName) { + public static Group loadGroup(String groupName) { String filePath = getGroupFilePath(groupName); Path path = Paths.get(filePath); @@ -110,7 +111,7 @@ public Group loadGroup(String groupName) { /** * Creates the groups directory if it does not exist. */ - private void createGroupDirectory() { + private static void createGroupDirectory() { Path path = Paths.get(GROUPS_DIRECTORY); if (!Files.exists(path)) { try { @@ -127,7 +128,7 @@ private void createGroupDirectory() { * @param groupName The name of the group. * @return The file path for the group file. */ - private String getGroupFilePath(String groupName) { + private static String getGroupFilePath(String groupName) { return GROUPS_DIRECTORY + "/" + groupName + GROUP_FILE_EXTENSION; } } From 054767d0ee2547d49ff477efdee1557502da235d Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 00:29:12 +0800 Subject: [PATCH 098/270] Add exceptions package --- .gitignore | 1 + .../java/seedu/duke/exceptions/GroupLoadException.java | 7 +++++++ .../java/seedu/duke/exceptions/GroupSaveException.java | 7 +++++++ 3 files changed, 15 insertions(+) create mode 100644 src/main/java/seedu/duke/exceptions/GroupLoadException.java create mode 100644 src/main/java/seedu/duke/exceptions/GroupSaveException.java diff --git a/.gitignore b/.gitignore index 2873e189e1..ebcd74ed30 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +data/ diff --git a/src/main/java/seedu/duke/exceptions/GroupLoadException.java b/src/main/java/seedu/duke/exceptions/GroupLoadException.java new file mode 100644 index 0000000000..63e8e89cf3 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/GroupLoadException.java @@ -0,0 +1,7 @@ +package seedu.duke.exceptions; + +public class GroupLoadException extends Exception { + public GroupLoadException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/exceptions/GroupSaveException.java b/src/main/java/seedu/duke/exceptions/GroupSaveException.java new file mode 100644 index 0000000000..5a768c9a79 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/GroupSaveException.java @@ -0,0 +1,7 @@ +package seedu.duke.exceptions; + +public class GroupSaveException extends Exception { + public GroupSaveException(String message) { + super(message); + } +} From ecdc341e45758b554fbe273dacf0af2494284b07 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 00:36:34 +0800 Subject: [PATCH 099/270] Made expense class public and add GroupFIlePath --- src/main/java/seedu/duke/Expense.java | 2 +- src/main/java/seedu/duke/Group.java | 2 ++ .../seedu/duke/storage/GroupFilePath.java | 22 +++++++++++++++++++ .../duke/{ => storage}/GroupStorage.java | 6 ++++- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/main/java/seedu/duke/storage/GroupFilePath.java rename src/main/java/seedu/duke/{ => storage}/GroupStorage.java (97%) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 2173b9b721..75ff76a788 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -19,7 +19,7 @@ public class Expense { * @param payeeList : String array of people who are involved in the transaction * (Index 0 is the payer and will also be added to the payee list) */ - Expense(String payerName, float totalAmount, String[] payeeList) { + public Expense(String payerName, float totalAmount, String[] payeeList) { payees.addAll(Arrays.asList(payeeList)); this.payerName = payerName; this.totalAmount = totalAmount; diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 307c8ea9dd..d0291b1f56 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -2,6 +2,8 @@ package seedu.duke; +import seedu.duke.storage.GroupStorage; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java new file mode 100644 index 0000000000..bf710a7422 --- /dev/null +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -0,0 +1,22 @@ +package seedu.duke.storage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class GroupFilePath { + private static final String GROUPS_DIRECTORY = "data/groups"; + private static final String GROUP_FILE_EXTENSION = ".txt"; + + public static String getFilePath(String groupName) { + return GROUPS_DIRECTORY + "/" + groupName + GROUP_FILE_EXTENSION; + } + + public static void createGroupDirectory() throws IOException { + Path path = Paths.get(GROUPS_DIRECTORY); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } +} diff --git a/src/main/java/seedu/duke/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java similarity index 97% rename from src/main/java/seedu/duke/GroupStorage.java rename to src/main/java/seedu/duke/storage/GroupStorage.java index afe623ea09..3d46e5092d 100644 --- a/src/main/java/seedu/duke/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,5 +1,9 @@ //@@ author hafizuddin-a -package seedu.duke; +package seedu.duke.storage; + +import seedu.duke.Expense; +import seedu.duke.Group; +import seedu.duke.User; import java.io.BufferedReader; import java.io.BufferedWriter; From ec58b623d2a57aea292611bccfed18e9c70a8294 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 00:45:06 +0800 Subject: [PATCH 100/270] Add File interface --- src/main/java/seedu/duke/Group.java | 4 +- src/main/java/seedu/duke/storage/FileIO.java | 10 +++ .../java/seedu/duke/storage/FileIOImpl.java | 19 +++++ .../java/seedu/duke/storage/GroupStorage.java | 81 +++++-------------- 4 files changed, 51 insertions(+), 63 deletions(-) create mode 100644 src/main/java/seedu/duke/storage/FileIO.java create mode 100644 src/main/java/seedu/duke/storage/FileIOImpl.java diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index d0291b1f56..b6d9546a30 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -76,7 +76,7 @@ public static Optional enterGroup(String groupName) { if (group.isEmpty()) { //@@ author hafizuddin-a // If the group doesn't exist in memory, try loading it from file - Group loadedGroup = GroupStorage.loadGroup(groupName); + Group loadedGroup = GroupStorage.loadGroupFromFile(groupName); if (loadedGroup != null) { groups.put(groupName, loadedGroup); @@ -98,7 +98,7 @@ public static Optional enterGroup(String groupName) { */ public static void exitGroup() { if (currentGroupName.isPresent()) { - GroupStorage.saveGroup(groups.get(currentGroupName.get())); + GroupStorage.saveGroupToFile(groups.get(currentGroupName.get())); System.out.println("You have exited " + currentGroupName.get() + "."); currentGroupName = Optional.empty(); } else { diff --git a/src/main/java/seedu/duke/storage/FileIO.java b/src/main/java/seedu/duke/storage/FileIO.java new file mode 100644 index 0000000000..c9188ec8a9 --- /dev/null +++ b/src/main/java/seedu/duke/storage/FileIO.java @@ -0,0 +1,10 @@ +package seedu.duke.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; + +public interface FileIO { + BufferedReader getFileReader(String filePath) throws IOException; + BufferedWriter getFileWriter(String filePath) throws IOException; +} diff --git a/src/main/java/seedu/duke/storage/FileIOImpl.java b/src/main/java/seedu/duke/storage/FileIOImpl.java new file mode 100644 index 0000000000..eaa3b543f7 --- /dev/null +++ b/src/main/java/seedu/duke/storage/FileIOImpl.java @@ -0,0 +1,19 @@ +package seedu.duke.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +public class FileIOImpl implements FileIO { + @Override + public BufferedReader getFileReader(String filePath) throws IOException { + return new BufferedReader(new FileReader(filePath)); + } + + @Override + public BufferedWriter getFileWriter(String filePath) throws IOException { + return new BufferedWriter(new FileWriter(filePath)); + } +} diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 3d46e5092d..6c758323e0 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -4,37 +4,33 @@ import seedu.duke.Expense; import seedu.duke.Group; import seedu.duke.User; +import seedu.duke.exceptions.GroupLoadException; +import seedu.duke.exceptions.GroupSaveException; import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; public class GroupStorage { - private static final String GROUPS_DIRECTORY = "data/groups"; - private static final String GROUP_FILE_EXTENSION = ".txt"; private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; - /** - * Saves the group information to a file. - * - * @param group The group to save. - */ - public static void saveGroup(Group group) { + private static FileIO fileIO; + + public GroupStorage(FileIO fileIO) { + this.fileIO = fileIO; + } + + public static void saveGroupToFile(Group group) throws GroupSaveException { String groupName = group.getGroupName(); List members = group.getMembers(); List expenses = group.getExpenseList(); try { - createGroupDirectory(); - String filePath = getGroupFilePath(groupName); - BufferedWriter writer = new BufferedWriter(new FileWriter(filePath)); + GroupFilePath.createGroupDirectory(); + String filePath = GroupFilePath.getFilePath(groupName); + BufferedWriter writer = fileIO.getFileWriter(filePath); // Write group name writer.write(groupName); @@ -59,28 +55,17 @@ public static void saveGroup(Group group) { } writer.close(); - System.out.println("Group information saved successfully."); } catch (IOException e) { - System.out.println("An error occurred while saving the group information."); + throw new GroupSaveException("An error occurred while saving the group information."); } } - /** - * Loads the group information from a file. - * - * @param groupName The name of the group to load. - * @return The loaded group, or null if the group file does not exist. - */ - public static Group loadGroup(String groupName) { - String filePath = getGroupFilePath(groupName); - Path path = Paths.get(filePath); - - if (!Files.exists(path)) { - System.out.println("Group file does not exist."); - return null; - } + public static Group loadGroupFromFile(String groupName) throws GroupLoadException { + String filePath = GroupFilePath.getFilePath(groupName); + + try { + BufferedReader reader = fileIO.getFileReader(filePath); - try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String name = reader.readLine(); Group group = Group.getOrCreateGroup(name).orElse(null); @@ -103,36 +88,10 @@ public static Group loadGroup(String groupName) { group.addExpense(expense); } - System.out.println("Group information loaded successfully."); + reader.close(); return group; } catch (IOException e) { - System.out.println("An error occurred while loading the group information."); + throw new GroupLoadException("An error occurred while loading the group information."); } - - return null; - } - - /** - * Creates the groups directory if it does not exist. - */ - private static void createGroupDirectory() { - Path path = Paths.get(GROUPS_DIRECTORY); - if (!Files.exists(path)) { - try { - Files.createDirectories(path); - } catch (IOException e) { - System.out.println("An error occurred while creating the groups directory."); - } - } - } - - /** - * Returns the file path for the group file. - * - * @param groupName The name of the group. - * @return The file path for the group file. - */ - private static String getGroupFilePath(String groupName) { - return GROUPS_DIRECTORY + "/" + groupName + GROUP_FILE_EXTENSION; } } From 3e428b6ef4ccf7077ad9b66f7b1a04cf60b75c54 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 00:54:05 +0800 Subject: [PATCH 101/270] Handle storage exceptions in Group --- src/main/java/seedu/duke/Group.java | 40 ++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index b6d9546a30..54368cdd87 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -2,7 +2,10 @@ package seedu.duke; +import seedu.duke.exceptions.GroupLoadException; +import seedu.duke.exceptions.GroupSaveException; import seedu.duke.storage.GroupStorage; +import seedu.duke.storage.FileIOImpl; import java.util.ArrayList; import java.util.HashMap; @@ -13,6 +16,7 @@ public class Group { private static final Map groups = new HashMap<>(); private static Optional currentGroupName = Optional.empty(); + private static final GroupStorage groupStorage = new GroupStorage(new FileIOImpl()); private final String groupName; private final List members; @@ -70,23 +74,28 @@ public static Optional getOrCreateGroup(String groupName) { * @param groupName The name of the group to enter. * @return The existing group. */ - public static Optional enterGroup(String groupName) { Optional group = Optional.ofNullable(groups.get(groupName)); if (group.isEmpty()) { //@@ author hafizuddin-a - // If the group doesn't exist in memory, try loading it from file - Group loadedGroup = GroupStorage.loadGroupFromFile(groupName); - - if (loadedGroup != null) { - groups.put(groupName, loadedGroup); - group = Optional.of(loadedGroup); - } else { - //@@ author avrilgk - System.out.println("Group does not exist."); - return group; + try { + // If the group doesn't exist in memory, try loading it from file + Group loadedGroup = groupStorage.loadGroupFromFile(groupName); + if (loadedGroup != null) { + groups.put(groupName, loadedGroup); + group = Optional.of(loadedGroup); + } else { + //@@ author avrilgk + System.out.println("Group does not exist."); + return Optional.empty(); + } + // @@ author hafizuddin-a + } catch (GroupLoadException e) { + System.out.println("Error loading group: " + e.getMessage()); + return Optional.empty(); } } + //@@ author avrilgk currentGroupName = Optional.of(groupName); System.out.println("You are now in " + groupName); return group; @@ -98,7 +107,14 @@ public static Optional enterGroup(String groupName) { */ public static void exitGroup() { if (currentGroupName.isPresent()) { - GroupStorage.saveGroupToFile(groups.get(currentGroupName.get())); + //@@ author hafizuddin-a + try { + groupStorage.saveGroupToFile(groups.get(currentGroupName.get())); + System.out.println("Group data saved successfully."); + } catch (GroupSaveException e) { + System.out.println("Error saving group: " + e.getMessage()); + } + //@@ author avrilgk System.out.println("You have exited " + currentGroupName.get() + "."); currentGroupName = Optional.empty(); } else { From 7509d8ce45c91b020394b3401f5b9626e489e12e Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 01:13:45 +0800 Subject: [PATCH 102/270] Add Javadoc --- .../duke/exceptions/GroupLoadException.java | 9 +++++++++ .../duke/exceptions/GroupSaveException.java | 9 +++++++++ src/main/java/seedu/duke/storage/FileIO.java | 19 +++++++++++++++++++ .../java/seedu/duke/storage/FileIOImpl.java | 18 ++++++++++++++++++ .../seedu/duke/storage/GroupFilePath.java | 15 +++++++++++++++ .../java/seedu/duke/storage/GroupStorage.java | 17 +++++++++++++++++ .../seedu/duke/storage/GroupStorageTest.java | 5 +++++ 7 files changed, 92 insertions(+) create mode 100644 src/test/java/seedu/duke/storage/GroupStorageTest.java diff --git a/src/main/java/seedu/duke/exceptions/GroupLoadException.java b/src/main/java/seedu/duke/exceptions/GroupLoadException.java index 63e8e89cf3..6a2625da39 100644 --- a/src/main/java/seedu/duke/exceptions/GroupLoadException.java +++ b/src/main/java/seedu/duke/exceptions/GroupLoadException.java @@ -1,6 +1,15 @@ package seedu.duke.exceptions; +/** + * Represents an exception that occurs when loading group information fails. + * This exception is thrown when an error occurs while loading a group from a file. + */ public class GroupLoadException extends Exception { + /** + * Constructs a new GroupLoadException with the specified detail message. + * + * @param message The detail message. + */ public GroupLoadException(String message) { super(message); } diff --git a/src/main/java/seedu/duke/exceptions/GroupSaveException.java b/src/main/java/seedu/duke/exceptions/GroupSaveException.java index 5a768c9a79..47deb1246d 100644 --- a/src/main/java/seedu/duke/exceptions/GroupSaveException.java +++ b/src/main/java/seedu/duke/exceptions/GroupSaveException.java @@ -1,6 +1,15 @@ package seedu.duke.exceptions; +/** + * Represents an exception that occurs when saving group information fails. + * This exception is thrown when an error occurs while saving a group to a file. + */ public class GroupSaveException extends Exception { + /** + * Constructs a new GroupSaveException with the specified detail message. + * + * @param message The detail message. + */ public GroupSaveException(String message) { super(message); } diff --git a/src/main/java/seedu/duke/storage/FileIO.java b/src/main/java/seedu/duke/storage/FileIO.java index c9188ec8a9..e86f5d7a88 100644 --- a/src/main/java/seedu/duke/storage/FileIO.java +++ b/src/main/java/seedu/duke/storage/FileIO.java @@ -4,7 +4,26 @@ import java.io.BufferedWriter; import java.io.IOException; +/** + * Represents the file I/O operations. + * Defines methods for reading from and writing to files. + */ public interface FileIO { + /** + * Returns a BufferedReader for reading from the specified file. + * + * @param filePath The path of the file to read from. + * @return A BufferedReader for reading from the file. + * @throws IOException If an I/O error occurs while creating the reader. + */ BufferedReader getFileReader(String filePath) throws IOException; + + /** + * Returns a BufferedWriter for writing to the specified file. + * + * @param filePath The path of the file to write to. + * @return A BufferedWriter for writing to the file. + * @throws IOException If an I/O error occurs while creating the writer. + */ BufferedWriter getFileWriter(String filePath) throws IOException; } diff --git a/src/main/java/seedu/duke/storage/FileIOImpl.java b/src/main/java/seedu/duke/storage/FileIOImpl.java index eaa3b543f7..cacdab685f 100644 --- a/src/main/java/seedu/duke/storage/FileIOImpl.java +++ b/src/main/java/seedu/duke/storage/FileIOImpl.java @@ -6,12 +6,30 @@ import java.io.FileWriter; import java.io.IOException; +/** + * Implements the FileIO interface. + * Provides concrete implementations for file I/O operations using BufferedReader and BufferedWriter. + */ public class FileIOImpl implements FileIO { + /** + * Returns a BufferedReader for reading from the specified file. + * + * @param filePath The path of the file to read from. + * @return A BufferedReader for reading from the file. + * @throws IOException If an I/O error occurs while creating the reader. + */ @Override public BufferedReader getFileReader(String filePath) throws IOException { return new BufferedReader(new FileReader(filePath)); } + /** + * Returns a BufferedWriter for writing to the specified file. + * + * @param filePath The path of the file to write to. + * @return A BufferedWriter for writing to the file. + * @throws IOException If an I/O error occurs while creating the writer. + */ @Override public BufferedWriter getFileWriter(String filePath) throws IOException { return new BufferedWriter(new FileWriter(filePath)); diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java index bf710a7422..091c301a34 100644 --- a/src/main/java/seedu/duke/storage/GroupFilePath.java +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -5,14 +5,29 @@ import java.nio.file.Path; import java.nio.file.Paths; +/** + * Represents the file path management for group files. + * Provides methods to get the file path for a group and create the groups directory. + */ public class GroupFilePath { private static final String GROUPS_DIRECTORY = "data/groups"; private static final String GROUP_FILE_EXTENSION = ".txt"; + /** + * Returns the file path for the group file. + * + * @param groupName The name of the group. + * @return The file path for the group file. + */ public static String getFilePath(String groupName) { return GROUPS_DIRECTORY + "/" + groupName + GROUP_FILE_EXTENSION; } + /** + * Creates the groups directory if it does not exist. + * + * @throws IOException If an I/O error occurs while creating the directory. + */ public static void createGroupDirectory() throws IOException { Path path = Paths.get(GROUPS_DIRECTORY); if (!Files.exists(path)) { diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 6c758323e0..e97b27112a 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -12,6 +12,10 @@ import java.io.IOException; import java.util.List; +/** + * Represents the storage manager for group data. + * Handles the saving and loading of group information to and from files. + */ public class GroupStorage { private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; @@ -22,6 +26,12 @@ public GroupStorage(FileIO fileIO) { this.fileIO = fileIO; } + /** + * Saves the group information to a file. + * + * @param group The group to save. + * @throws GroupSaveException If an error occurs while saving the group information. + */ public static void saveGroupToFile(Group group) throws GroupSaveException { String groupName = group.getGroupName(); List members = group.getMembers(); @@ -60,6 +70,13 @@ public static void saveGroupToFile(Group group) throws GroupSaveException { } } + /** + * Loads the group information from a file. + * + * @param groupName The name of the group to load. + * @return The loaded group, or null if the group file does not exist. + * @throws GroupLoadException If an error occurs while loading the group information. + */ public static Group loadGroupFromFile(String groupName) throws GroupLoadException { String filePath = GroupFilePath.getFilePath(groupName); diff --git a/src/test/java/seedu/duke/storage/GroupStorageTest.java b/src/test/java/seedu/duke/storage/GroupStorageTest.java new file mode 100644 index 0000000000..870c26e204 --- /dev/null +++ b/src/test/java/seedu/duke/storage/GroupStorageTest.java @@ -0,0 +1,5 @@ +package seedu.duke.storage; + +class GroupStorageTest { + +} From 519f800e5a241fe747ff6789b624195f9b41f79e Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 01:28:20 +0800 Subject: [PATCH 103/270] Add authors for expense class --- src/main/java/seedu/duke/Expense.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 75ff76a788..774a156245 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -1,6 +1,6 @@ +//@@ author mukund1403 package seedu.duke; - import java.util.ArrayList; import java.util.Arrays; @@ -32,6 +32,7 @@ public Expense(String payerName, float totalAmount, String[] payeeList) { System.out.println(); } + //@@ author Cohii2 public String getPayerName() { return payerName; } From e5f4af075f577cb9a77fe8c8ce7a09fa865bb8d6 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 01:42:33 +0800 Subject: [PATCH 104/270] Update authors - no space after @@ --- src/main/java/seedu/duke/Expense.java | 4 ++-- src/main/java/seedu/duke/Group.java | 10 +++++----- src/main/java/seedu/duke/storage/GroupStorage.java | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 774a156245..f3a3dc2027 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -1,4 +1,4 @@ -//@@ author mukund1403 +//@@author mukund1403 package seedu.duke; import java.util.ArrayList; @@ -32,7 +32,7 @@ public Expense(String payerName, float totalAmount, String[] payeeList) { System.out.println(); } - //@@ author Cohii2 + //@@author Cohii2 public String getPayerName() { return payerName; } diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 54368cdd87..63f25df231 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -77,7 +77,7 @@ public static Optional getOrCreateGroup(String groupName) { public static Optional enterGroup(String groupName) { Optional group = Optional.ofNullable(groups.get(groupName)); if (group.isEmpty()) { - //@@ author hafizuddin-a + //@@author hafizuddin-a try { // If the group doesn't exist in memory, try loading it from file Group loadedGroup = groupStorage.loadGroupFromFile(groupName); @@ -89,13 +89,13 @@ public static Optional enterGroup(String groupName) { System.out.println("Group does not exist."); return Optional.empty(); } - // @@ author hafizuddin-a + // @@author hafizuddin-a } catch (GroupLoadException e) { System.out.println("Error loading group: " + e.getMessage()); return Optional.empty(); } } - //@@ author avrilgk + //@@author avrilgk currentGroupName = Optional.of(groupName); System.out.println("You are now in " + groupName); return group; @@ -107,14 +107,14 @@ public static Optional enterGroup(String groupName) { */ public static void exitGroup() { if (currentGroupName.isPresent()) { - //@@ author hafizuddin-a + //@@author hafizuddin-a try { groupStorage.saveGroupToFile(groups.get(currentGroupName.get())); System.out.println("Group data saved successfully."); } catch (GroupSaveException e) { System.out.println("Error saving group: " + e.getMessage()); } - //@@ author avrilgk + //@@author avrilgk System.out.println("You have exited " + currentGroupName.get() + "."); currentGroupName = Optional.empty(); } else { diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index e97b27112a..317378b9e3 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,4 +1,4 @@ -//@@ author hafizuddin-a +//@@author hafizuddin-a package seedu.duke.storage; import seedu.duke.Expense; From adcb2384aeb128cd4a428d3a7498631c71172cf3 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sat, 30 Mar 2024 02:37:43 +0800 Subject: [PATCH 105/270] Add currency exchange functionality --- .../java/seedu/duke/CurrencyConversions.java | 39 +++++++++++ src/main/java/seedu/duke/Money.java | 64 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/main/java/seedu/duke/CurrencyConversions.java create mode 100644 src/main/java/seedu/duke/Money.java diff --git a/src/main/java/seedu/duke/CurrencyConversions.java b/src/main/java/seedu/duke/CurrencyConversions.java new file mode 100644 index 0000000000..2c7d08bc29 --- /dev/null +++ b/src/main/java/seedu/duke/CurrencyConversions.java @@ -0,0 +1,39 @@ +package seedu.duke; + +//all rates are relative to 1 SGD +public enum CurrencyConversions { + USD("USD", 0.74), + RMB("RMB", 5.35), + EUR("EUR", 0.687), + JPY("JPY", 112.12), + AUD("AUD", 1.12), + MYR("MYR", 3.50), + SGD("USD", 1.00), + + ; + + private final String name; + private final double rate; + + CurrencyConversions(String name, double rate) { + this.name = name; + this.rate = rate; + } + + String getName() { + return this.name; + } + + double getRate() { + return this.rate; + } + + double getInverseRate() { + return 1.00 / this.rate; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/src/main/java/seedu/duke/Money.java b/src/main/java/seedu/duke/Money.java new file mode 100644 index 0000000000..1c660ef7ee --- /dev/null +++ b/src/main/java/seedu/duke/Money.java @@ -0,0 +1,64 @@ +package seedu.duke; + +public class Money { + private final double amount; + private final CurrencyConversions currency; + + public Money(double amount, CurrencyConversions currency) { + this.amount = amount; + this.currency = currency; + } + + Money convertToSGD() { + double amountInSGD = this.amount * this.currency.getInverseRate(); + return new Money(amountInSGD, CurrencyConversions.SGD); + } + + Money convertToOther(CurrencyConversions resultCurrency) { + double amountInSGD = this.amount * this.currency.getInverseRate(); + double foreignAmount = amountInSGD * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money addition(Money other, CurrencyConversions resultCurrency) { + double amountInSGD = this.convertToSGD().getAmount(); + double otherAmountInSGD = other.convertToSGD().getAmount(); + double foreignAmount = (amountInSGD + otherAmountInSGD) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money subtraction(Money other, CurrencyConversions resultCurrency) { + double amountInSGD = this.convertToSGD().getAmount(); + double otherAmountInSGD = other.convertToSGD().getAmount(); + double foreignAmount = (amountInSGD - otherAmountInSGD) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money multiplication(Money other, CurrencyConversions resultCurrency) { + double amountInSGD = this.convertToSGD().getAmount(); + double otherAmountInSGD = other.convertToSGD().getAmount(); + double foreignAmount = (amountInSGD * otherAmountInSGD) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money division(double constant, CurrencyConversions resultCurrency) { + double amountInSGD = this.convertToSGD().getAmount(); + double foreignAmount = (amountInSGD / constant) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + double getAmount() { + return this.amount; + } + CurrencyConversions getCurrency() { + return this.currency; + } + @Override + public String toString() { + return String.format("%.2f%s", this.amount, this.currency); + } +} From 4c39c85d19a9ccfd296526782cacf6be97f294d0 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sat, 30 Mar 2024 02:46:20 +0800 Subject: [PATCH 106/270] Add authorship --- src/main/java/seedu/duke/CurrencyConversions.java | 2 +- src/main/java/seedu/duke/Money.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/CurrencyConversions.java b/src/main/java/seedu/duke/CurrencyConversions.java index 2c7d08bc29..8664868265 100644 --- a/src/main/java/seedu/duke/CurrencyConversions.java +++ b/src/main/java/seedu/duke/CurrencyConversions.java @@ -1,5 +1,5 @@ package seedu.duke; - +//@@author MonkeScripts //all rates are relative to 1 SGD public enum CurrencyConversions { USD("USD", 0.74), diff --git a/src/main/java/seedu/duke/Money.java b/src/main/java/seedu/duke/Money.java index 1c660ef7ee..ad8a75e799 100644 --- a/src/main/java/seedu/duke/Money.java +++ b/src/main/java/seedu/duke/Money.java @@ -1,5 +1,5 @@ package seedu.duke; - +//@@author MonkeScripts public class Money { private final double amount; private final CurrencyConversions currency; From 0c513719990d6d8d94a84d1dd70409650e75d225 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sat, 30 Mar 2024 02:52:58 +0800 Subject: [PATCH 107/270] Add UniversalExceptions.java --- .../java/seedu/duke/UniversalExceptions.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/seedu/duke/UniversalExceptions.java diff --git a/src/main/java/seedu/duke/UniversalExceptions.java b/src/main/java/seedu/duke/UniversalExceptions.java new file mode 100644 index 0000000000..b25a68f05b --- /dev/null +++ b/src/main/java/seedu/duke/UniversalExceptions.java @@ -0,0 +1,17 @@ +package seedu.duke; + +public class UniversalExceptions extends Exception { + private final String errorMessage; + UniversalExceptions(String errorMessage) { + this.errorMessage = errorMessage; + } + + String getErrorMessage() { + return this.errorMessage; + } + + @Override + public String toString() { + return this.errorMessage; + } +} From 4e5f332ba8fcc1ba55f126f86c99d01cbc8d2847 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 10:28:04 +0800 Subject: [PATCH 108/270] ArrayList for payees --- src/main/java/seedu/duke/storage/GroupStorage.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 317378b9e3..7afe2f161e 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -10,6 +10,7 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -97,11 +98,17 @@ public static Group loadGroupFromFile(String groupName) throws GroupLoadExceptio // Read expenses while ((line = reader.readLine()) != null) { - String[] expenseData = line.split(","); + String[] expenseData = line.split(",", 3); float totalAmount = Float.parseFloat(expenseData[0]); String payerName = expenseData[1]; - String[] payeeList = expenseData[2].split(","); - Expense expense = new Expense(payerName, totalAmount, payeeList); + String[] payeeNames = expenseData[2].split(","); + + List payeeList = new ArrayList<>(); + for (String payeeName : payeeNames) { + payeeList.add(payeeName); + } + + Expense expense = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); group.addExpense(expense); } From 60392a24d6ef5e428f4095cd76831d29244c5f63 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sat, 30 Mar 2024 10:54:55 +0800 Subject: [PATCH 109/270] Add description to expenses --- src/main/java/seedu/duke/Expense.java | 15 ++++++++++++--- src/main/java/seedu/duke/Parser.java | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 2173b9b721..31e47536bf 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -12,6 +12,8 @@ public class Expense { private float totalAmount; private ArrayList payees = new ArrayList<>(); + private String description; + /** * Constructor to create new Expense * @param payerName : The name of the user who paid for the Expense @@ -19,13 +21,14 @@ public class Expense { * @param payeeList : String array of people who are involved in the transaction * (Index 0 is the payer and will also be added to the payee list) */ - Expense(String payerName, float totalAmount, String[] payeeList) { + Expense(String payerName, String description, float totalAmount, String[] payeeList) { payees.addAll(Arrays.asList(payeeList)); this.payerName = payerName; this.totalAmount = totalAmount; + this.description = description; - System.out.printf("Added new expense %.2f paid by %s and split between:" - ,this.totalAmount,this.payerName); + System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:", + this.description,this.totalAmount,this.payerName); for(String payee : payees) { System.out.print(payee + ", "); } @@ -46,4 +49,10 @@ public float getTotalAmount() { public ArrayList getPayees() { return payees; } + + public String getDescription(){ + return description; + } } + + diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 74b2003166..388853fcbe 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -179,7 +179,9 @@ public void handleUserInput() throws EndProgramException, ExpensesException { String payerName = params.get("paid").get(0); payeeList.add(0, payerName); - Expense newTransaction = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); + System.out.println(this.argument); + + Expense newTransaction = new Expense(payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); currentGroup.get().addExpense(newTransaction); break; @@ -200,7 +202,6 @@ public void handleUserInput() throws EndProgramException, ExpensesException { String exceptionMessage = argument + " is not in current Group!"; throw new ExpensesException(exceptionMessage); } - Balance balance = new Balance(argument, currentGroup1.get()); balance.printBalance(); break; From a7f7cfbb3ba1f4181f24439ff7312865eb353a18 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 11:31:13 +0800 Subject: [PATCH 110/270] Add Group test cases --- .../seedu/duke/storage/GroupFilePath.java | 10 ++- .../seedu/duke/storage/GroupStorageTest.java | 76 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java index 091c301a34..41a9470448 100644 --- a/src/main/java/seedu/duke/storage/GroupFilePath.java +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -10,7 +10,7 @@ * Provides methods to get the file path for a group and create the groups directory. */ public class GroupFilePath { - private static final String GROUPS_DIRECTORY = "data/groups"; + private static String groupsDirectory = "data/groups"; private static final String GROUP_FILE_EXTENSION = ".txt"; /** @@ -20,7 +20,7 @@ public class GroupFilePath { * @return The file path for the group file. */ public static String getFilePath(String groupName) { - return GROUPS_DIRECTORY + "/" + groupName + GROUP_FILE_EXTENSION; + return groupsDirectory + "/" + groupName + GROUP_FILE_EXTENSION; } /** @@ -29,9 +29,13 @@ public static String getFilePath(String groupName) { * @throws IOException If an I/O error occurs while creating the directory. */ public static void createGroupDirectory() throws IOException { - Path path = Paths.get(GROUPS_DIRECTORY); + Path path = Paths.get(groupsDirectory); if (!Files.exists(path)) { Files.createDirectories(path); } } + + public static void setGroupsDirectory(String directory) { + groupsDirectory = directory; + } } diff --git a/src/test/java/seedu/duke/storage/GroupStorageTest.java b/src/test/java/seedu/duke/storage/GroupStorageTest.java index 870c26e204..e24c4bb04e 100644 --- a/src/test/java/seedu/duke/storage/GroupStorageTest.java +++ b/src/test/java/seedu/duke/storage/GroupStorageTest.java @@ -1,5 +1,81 @@ package seedu.duke.storage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.GroupLoadException; +import seedu.duke.exceptions.GroupSaveException; +import seedu.duke.Group; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + class GroupStorageTest { + private static final String TEST_GROUPS_DIRECTORY = "src/test/data/groups"; + + private GroupStorage groupStorage; + private FileIO fileIO; + + @BeforeAll + static void setUpTestDirectory() { + GroupFilePath.setGroupsDirectory(TEST_GROUPS_DIRECTORY); + } + + @BeforeEach + void setUp() throws IOException { + fileIO = new FileIOImpl(); + groupStorage = new GroupStorage(fileIO); + + // Create the test groups directory if it doesn't exist + Files.createDirectories(Path.of(TEST_GROUPS_DIRECTORY)); + } + + @Test + void saveGroupToFile_validGroup_successfullySaves() throws GroupSaveException { + Group group = createSampleGroup(); + + groupStorage.saveGroupToFile(group); + + Path filePath = Path.of(TEST_GROUPS_DIRECTORY, "sample_group.txt"); + assertTrue(Files.exists(filePath)); + } + + @Test + void loadGroupFromFile_validFile_successfullyLoads() throws IOException, GroupLoadException { + createSampleGroupFile(); + + Group loadedGroup = groupStorage.loadGroupFromFile("sample_group"); + + assertNotNull(loadedGroup); + assertEquals("sample_group", loadedGroup.getGroupName()); + assertEquals(2, loadedGroup.getMembers().size()); + } + + @Test + void loadGroupFromFile_nonExistentFile_throwsGroupLoadException() { + assertThrows(GroupLoadException.class, () -> groupStorage.loadGroupFromFile("nonexistent_group")); + } + + private Group createSampleGroup() { + Group group = Group.getOrCreateGroup("sample_group").get(); + group.addMember("user1"); + group.addMember("user2"); + return group; + } + private void createSampleGroupFile() throws IOException { + Path filePath = Path.of(TEST_GROUPS_DIRECTORY, "sample_group.txt"); + String fileContent = "sample_group\n" + + "Members:\n" + + "user1\n" + + "user2\n" + + "Expenses:\n"; + Files.writeString(filePath, fileContent); + } } From 9db45b91cd0643b9e2572554eb128d92d63eb262 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 11:50:31 +0800 Subject: [PATCH 111/270] Add assertions --- src/main/java/seedu/duke/storage/GroupFilePath.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java index 41a9470448..ce6c5b3cb2 100644 --- a/src/main/java/seedu/duke/storage/GroupFilePath.java +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -20,6 +20,7 @@ public class GroupFilePath { * @return The file path for the group file. */ public static String getFilePath(String groupName) { + assert groupName != null && !groupName.isEmpty() : "Group name cannot be null or empty"; return groupsDirectory + "/" + groupName + GROUP_FILE_EXTENSION; } @@ -36,6 +37,7 @@ public static void createGroupDirectory() throws IOException { } public static void setGroupsDirectory(String directory) { + assert directory != null && !directory.isEmpty() : "Groups directory cannot be null or empty"; groupsDirectory = directory; } } From f24cebf3359cf9e2dec606c419f615ffaa008044 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 11:56:12 +0800 Subject: [PATCH 112/270] Javadoc for setGroupsDirectory --- src/main/java/seedu/duke/storage/GroupFilePath.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java index ce6c5b3cb2..f313e4baef 100644 --- a/src/main/java/seedu/duke/storage/GroupFilePath.java +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -36,6 +36,18 @@ public static void createGroupDirectory() throws IOException { } } + /** + * Sets the directory where group files are stored. + *

+ * This method allows changing the default groups directory to a custom directory. + * It is useful for testing purposes or when the groups need to be stored in a different location. + *

+ * Assertions are used to check that the provided directory is not null or empty. + * If the assertion fails, an {@code AssertionError} will be thrown, indicating a programming error. + * + * @param directory the directory where group files should be stored + * @throws AssertionError if the provided directory is null or empty + */ public static void setGroupsDirectory(String directory) { assert directory != null && !directory.isEmpty() : "Groups directory cannot be null or empty"; groupsDirectory = directory; From 50d66a00094296163eef8c17035405b93e7eccd8 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 12:12:44 +0800 Subject: [PATCH 113/270] Extract more methods from load and save --- .../java/seedu/duke/storage/GroupStorage.java | 187 ++++++++++++------ 1 file changed, 123 insertions(+), 64 deletions(-) diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 7afe2f161e..56d91ee5ad 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,4 +1,3 @@ -//@@author hafizuddin-a package seedu.duke.storage; import seedu.duke.Expense; @@ -11,6 +10,7 @@ import java.io.BufferedWriter; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -20,9 +20,15 @@ public class GroupStorage { private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; + private static final String EXPENSE_DELIMITER = ","; - private static FileIO fileIO; + private final FileIO fileIO; + /** + * Constructs a GroupStorage object with the specified FileIO dependency. + * + * @param fileIO the FileIO instance for file input/output operations + */ public GroupStorage(FileIO fileIO) { this.fileIO = fileIO; } @@ -30,40 +36,18 @@ public GroupStorage(FileIO fileIO) { /** * Saves the group information to a file. * - * @param group The group to save. - * @throws GroupSaveException If an error occurs while saving the group information. + * @param group the group to save + * @throws GroupSaveException if an error occurs while saving the group information */ - public static void saveGroupToFile(Group group) throws GroupSaveException { - String groupName = group.getGroupName(); - List members = group.getMembers(); - List expenses = group.getExpenseList(); - + public void saveGroupToFile(Group group) throws GroupSaveException { try { GroupFilePath.createGroupDirectory(); - String filePath = GroupFilePath.getFilePath(groupName); + String filePath = GroupFilePath.getFilePath(group.getGroupName()); BufferedWriter writer = fileIO.getFileWriter(filePath); - // Write group name - writer.write(groupName); - writer.newLine(); - - // Write members - writer.write(MEMBERS_HEADER); - writer.newLine(); - for (User member : members) { - writer.write(member.getName()); - writer.newLine(); - } - - // Write expenses - writer.write(EXPENSES_HEADER); - writer.newLine(); - for (Expense expense : expenses) { - String expenseData = String.format("%.2f,%s,%s", expense.getTotalAmount(), expense.getPayerName(), - String.join(",", expense.getPayees())); - writer.write(expenseData); - writer.newLine(); - } + saveGroupName(writer, group.getGroupName()); + saveMembers(writer, group.getMembers()); + saveExpenses(writer, group.getExpenseList()); writer.close(); } catch (IOException e) { @@ -72,45 +56,68 @@ public static void saveGroupToFile(Group group) throws GroupSaveException { } /** - * Loads the group information from a file. + * Saves the group name to the file. * - * @param groupName The name of the group to load. - * @return The loaded group, or null if the group file does not exist. - * @throws GroupLoadException If an error occurs while loading the group information. + * @param writer the BufferedWriter for writing to the file + * @param groupName the name of the group + * @throws IOException if an I/O error occurs while writing to the file */ - public static Group loadGroupFromFile(String groupName) throws GroupLoadException { - String filePath = GroupFilePath.getFilePath(groupName); - - try { - BufferedReader reader = fileIO.getFileReader(filePath); - - String name = reader.readLine(); - Group group = Group.getOrCreateGroup(name).orElse(null); - - // Skip the "Members:" header - reader.readLine(); + private void saveGroupName(BufferedWriter writer, String groupName) throws IOException { + writer.write(groupName); + writer.newLine(); + } - // Read members - String line; - while ((line = reader.readLine()) != null && !line.equals(EXPENSES_HEADER)) { - group.addMember(line); - } + /** + * Saves the group members to the file. + * + * @param writer the BufferedWriter for writing to the file + * @param members the list of members in the group + * @throws IOException if an I/O error occurs while writing to the file + */ + private void saveMembers(BufferedWriter writer, List members) throws IOException { + writer.write(MEMBERS_HEADER); + writer.newLine(); + for (User member : members) { + writer.write(member.getName()); + writer.newLine(); + } + } - // Read expenses - while ((line = reader.readLine()) != null) { - String[] expenseData = line.split(",", 3); - float totalAmount = Float.parseFloat(expenseData[0]); - String payerName = expenseData[1]; - String[] payeeNames = expenseData[2].split(","); + /** + * Saves the group expenses to the file. + * + * @param writer the BufferedWriter for writing to the file + * @param expenses the list of expenses in the group + * @throws IOException if an I/O error occurs while writing to the file + */ + private void saveExpenses(BufferedWriter writer, List expenses) throws IOException { + writer.write(EXPENSES_HEADER); + writer.newLine(); + for (Expense expense : expenses) { + String expenseData = String.format("%.2f%s%s%s%s", + expense.getTotalAmount(), EXPENSE_DELIMITER, + expense.getPayerName(), EXPENSE_DELIMITER, + String.join(EXPENSE_DELIMITER, expense.getPayees())); + writer.write(expenseData); + writer.newLine(); + } + } - List payeeList = new ArrayList<>(); - for (String payeeName : payeeNames) { - payeeList.add(payeeName); - } + /** + * Loads the group information from a file. + * + * @param groupName the name of the group to load + * @return the loaded group + * @throws GroupLoadException if an error occurs while loading the group information + */ + public Group loadGroupFromFile(String groupName) throws GroupLoadException { + try { + String filePath = GroupFilePath.getFilePath(groupName); + BufferedReader reader = fileIO.getFileReader(filePath); - Expense expense = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); - group.addExpense(expense); - } + Group group = loadGroupName(reader); + loadMembers(reader, group); + loadExpenses(reader, group); reader.close(); return group; @@ -118,4 +125,56 @@ public static Group loadGroupFromFile(String groupName) throws GroupLoadExceptio throw new GroupLoadException("An error occurred while loading the group information."); } } + + /** + * Loads the group name from the file. + * + * @param reader the BufferedReader for reading from the file + * @return the loaded group + * @throws IOException if an I/O error occurs while reading from the file + */ + private Group loadGroupName(BufferedReader reader) throws IOException { + String name = reader.readLine(); + return Group.getOrCreateGroup(name).orElse(null); + } + + /** + * Loads the group members from the file. + * + * @param reader the BufferedReader for reading from the file + * @param group the group to add the loaded members to + * @throws IOException if an I/O error occurs while reading from the file + */ + private void loadMembers(BufferedReader reader, Group group) throws IOException { + // Skip the "Members:" header + reader.readLine(); + + String line; + while ((line = reader.readLine()) != null && !line.equals(EXPENSES_HEADER)) { + group.addMember(line); + } + } + + /** + * Loads the group expenses from the file. + * + * @param reader the BufferedReader for reading from the file + * @param group the group to add the loaded expenses to + * @throws IOException if an I/O error occurs while reading from the file + */ + private void loadExpenses(BufferedReader reader, Group group) throws IOException { + String line; + while ((line = reader.readLine()) != null) { + String[] expenseData = line.split(EXPENSE_DELIMITER, 3); + float totalAmount = Float.parseFloat(expenseData[0]); + String payerName = expenseData[1]; + String[] payeeNames = expenseData[2].split(EXPENSE_DELIMITER); + + List payeeList = new ArrayList<>(); + Collections.addAll(payeeList, payeeNames); + + Expense expense = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); + group.addExpense(expense); + } + } } From 72b4bf9a32bc92f0e1287670943bb07e4240acc6 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 12:33:27 +0800 Subject: [PATCH 114/270] Update GroupStorageTest --- src/test/java/seedu/duke/storage/GroupStorageTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/seedu/duke/storage/GroupStorageTest.java b/src/test/java/seedu/duke/storage/GroupStorageTest.java index e24c4bb04e..3a9bdd1171 100644 --- a/src/test/java/seedu/duke/storage/GroupStorageTest.java +++ b/src/test/java/seedu/duke/storage/GroupStorageTest.java @@ -20,7 +20,6 @@ class GroupStorageTest { private static final String TEST_GROUPS_DIRECTORY = "src/test/data/groups"; private GroupStorage groupStorage; - private FileIO fileIO; @BeforeAll static void setUpTestDirectory() { @@ -29,7 +28,7 @@ static void setUpTestDirectory() { @BeforeEach void setUp() throws IOException { - fileIO = new FileIOImpl(); + FileIO fileIO = new FileIOImpl(); groupStorage = new GroupStorage(fileIO); // Create the test groups directory if it doesn't exist From 0ba59488eec2598289954aa2e63dcf7006478ba5 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 12:58:41 +0800 Subject: [PATCH 115/270] Null check for adding members --- src/main/java/seedu/duke/Group.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 3f7de6dc18..9768b1e581 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -162,6 +162,11 @@ public boolean isMember(String memberName) { * @return The newly added user, or null if the user is already a member of the group. */ public User addMember(String memberName) { + if (memberName == null || memberName.isEmpty()) { + System.out.println("Please provide a valid member name."); + return null; + } + if (isMember(memberName)) { System.out.println(memberName + " is already a member of " + groupName + "."); return null; From 68097ee252e348f042f6a284c0eca9b87c34373a Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 13:20:03 +0800 Subject: [PATCH 116/270] Add loadGroupNames to load at the start - I just realised that groups should be loaded at the start of the app and not when you enter a group - Also need handle case of empty group data --- .../seedu/duke/storage/GroupFilePath.java | 4 +++ .../java/seedu/duke/storage/GroupStorage.java | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java index f313e4baef..858eb1afe7 100644 --- a/src/main/java/seedu/duke/storage/GroupFilePath.java +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -52,4 +52,8 @@ public static void setGroupsDirectory(String directory) { assert directory != null && !directory.isEmpty() : "Groups directory cannot be null or empty"; groupsDirectory = directory; } + + public static String getGroupsDirectory() { + return groupsDirectory; + } } diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 56d91ee5ad..cddd4eb579 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -9,6 +9,10 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -177,4 +181,27 @@ private void loadExpenses(BufferedReader reader, Group group) throws IOException group.addExpense(expense); } } + + /** + * Loads all the group names from the saved files. + * + * @return a list of group names + */ + public List loadGroupNames() { + List groupNames = new ArrayList<>(); + try { + GroupFilePath.createGroupDirectory(); + Path groupsDirectory = Paths.get(GroupFilePath.getGroupsDirectory()); + try (DirectoryStream stream = Files.newDirectoryStream(groupsDirectory, "*.txt")) { + for (Path file : stream) { + String fileName = file.getFileName().toString(); + String groupName = fileName.substring(0, fileName.lastIndexOf('.')); + groupNames.add(groupName); + } + } + } catch (IOException e) { + System.out.println("An error occurred while loading group names."); + } + return groupNames; + } } From ee2a289dbb6cb101352bb8007721d20f0468ecc9 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sat, 30 Mar 2024 13:56:38 +0800 Subject: [PATCH 117/270] add unequal split feature for expenses --- src/main/java/seedu/duke/Balance.java | 160 +++++++++++----------- src/main/java/seedu/duke/Expense.java | 66 +++++++-- src/main/java/seedu/duke/Help.java | 5 +- src/main/java/seedu/duke/Parser.java | 54 ++++---- src/test/java/seedu/duke/BalanceTest.java | 78 +++++------ src/test/java/seedu/duke/ExpenseTest.java | 9 +- 6 files changed, 217 insertions(+), 155 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index 447d112b13..fdea8b100c 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -1,80 +1,80 @@ -package seedu.duke; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class Balance { - protected String userName; - protected Map balanceList; - - public Balance(String userName, Map userList) { - this.userName = userName; - this.balanceList = userList; - } - - public Balance(String userName, Group group){ - this(userName, group.getExpenseList(), group.getMembers()); - } - - public Balance(String userName, List expenses, List users) { - this.userName = userName; - this.balanceList = new HashMap<>(); - - // Populate balanceList with other Users from Group - for (User user : users) { - if(!user.getName().equals(userName)) { - balanceList.put(user.getName(), 0f); - } - } - - // Add Expenses to balanceList - for (Expense expense : expenses) { - addExpense(expense); - } - } - - public String getUserName() { - return userName; - } - - public Map getBalanceList() { - return balanceList; - } - - private void addExpense(Expense expense) { - List payees = expense.getPayees(); - int numberOfUsers = payees.size(); - Float amountPerUser = expense.getTotalAmount() / numberOfUsers; - - if(expense.getPayerName().equals(userName)) { - for(String payee : payees) { - if(payee.equals(userName)){ - continue; - } - Float currentOwed = balanceList.get(payee); - Float newOwed = currentOwed + amountPerUser; - - balanceList.put(payee, newOwed); - } - } else if (expense.getPayees().contains(userName)) { - String payerName = expense.getPayerName(); - Float currentOwed = balanceList.get(payerName); - Float newOwed = currentOwed - amountPerUser; - - balanceList.put(payerName, newOwed); - } - } - - public void printBalance() { - String firstLine = String.format("User %s's Balance List:", userName); - System.out.println(firstLine); - - for (Map.Entry entry : balanceList.entrySet()) { - String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); - System.out.println(balanceLine); - } - - System.out.println("End of Balance List"); - } -} +//package seedu.duke; +// +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; +// +//public class Balance { +// protected String userName; +// protected Map balanceList; +// +// public Balance(String userName, Map userList) { +// this.userName = userName; +// this.balanceList = userList; +// } +// +// public Balance(String userName, Group group){ +// this(userName, group.getExpenseList(), group.getMembers()); +// } +// +// public Balance(String userName, List expenses, List users) { +// this.userName = userName; +// this.balanceList = new HashMap<>(); +// +// // Populate balanceList with other Users from Group +// for (User user : users) { +// if(!user.getName().equals(userName)) { +// balanceList.put(user.getName(), 0f); +// } +// } +// +// // Add Expenses to balanceList +// for (Expense expense : expenses) { +// addExpense(expense); +// } +// } +// +// public String getUserName() { +// return userName; +// } +// +// public Map getBalanceList() { +// return balanceList; +// } +// +// private void addExpense(Expense expense) { +// List payees = expense.getPayees(); +// int numberOfUsers = payees.size(); +// Float amountPerUser = expense.getTotalAmount() / numberOfUsers; +// +// if(expense.getPayerName().equals(userName)) { +// for(String payee : payees) { +// if(payee.equals(userName)){ +// continue; +// } +// Float currentOwed = balanceList.get(payee); +// Float newOwed = currentOwed + amountPerUser; +// +// balanceList.put(payee, newOwed); +// } +// } else if (expense.getPayees().contains(userName)) { +// String payerName = expense.getPayerName(); +// Float currentOwed = balanceList.get(payerName); +// Float newOwed = currentOwed - amountPerUser; +// +// balanceList.put(payerName, newOwed); +// } +// } +// +// public void printBalance() { +// String firstLine = String.format("User %s's Balance List:", userName); +// System.out.println(firstLine); +// +// for (Map.Entry entry : balanceList.entrySet()) { +// String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); +// System.out.println(balanceLine); +// } +// +// System.out.println("End of Balance List"); +// } +//} diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 31e47536bf..e79d58d480 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -1,6 +1,9 @@ +//@@author mukund1403 package seedu.duke; +import javafx.util.Pair; + import java.util.ArrayList; import java.util.Arrays; @@ -10,27 +13,63 @@ public class Expense { private String payerName; private float totalAmount; - private ArrayList payees = new ArrayList<>(); + private ArrayList> payees = new ArrayList<>(); private String description; /** * Constructor to create new Expense + * @param isUnequal : Boolean showing whether expense is split unequally or not * @param payerName : The name of the user who paid for the Expense + * @param description : Description of the expense * @param totalAmount : The total amount before being divided * @param payeeList : String array of people who are involved in the transaction - * (Index 0 is the payer and will also be added to the payee list) + * (Index 0 is the payer and will also be added to the payees but as last index) */ - Expense(String payerName, String description, float totalAmount, String[] payeeList) { - payees.addAll(Arrays.asList(payeeList)); + Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) throws ExpensesException { + if(isUnequal){ + float amountDueByPayees = 0; + for(String payee : payeeList){ + String[] payeeInfo = payee.split(" "); + String payeeName = payeeInfo[0].trim(); + if(payeeName.equals(payerName)){ + continue; + } + try { + float amountDue = Float.parseFloat(payeeInfo[1].trim()); + amountDueByPayees += amountDue; + payees.add(new Pair<>(payeeName,amountDue)); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter amount due for payee with name " + + payeeName + " as a proper number."; + throw new ExpensesException(exceptionMessage); + } catch (ArrayIndexOutOfBoundsException e) { + String exceptionMessage = "Amount due for payee with name " + + payeeName + " is empty. Enter it and try again"; + throw new ExpensesException(exceptionMessage); + } + } + if(amountDueByPayees > totalAmount){ + String exceptionMessage = "The amount split between users is greater than total amount. Try again."; + throw new ExpensesException(exceptionMessage); + } + payees.add(new Pair<>(payerName,totalAmount-amountDueByPayees)); + } + else { + Float amountDue = totalAmount/payeeList.length; + for(String payee : payeeList){ + payees.add(new Pair<>(payee,amountDue)); + } + } + this.payerName = payerName; this.totalAmount = totalAmount; this.description = description; - System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:", + System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:\n", this.description,this.totalAmount,this.payerName); - for(String payee : payees) { - System.out.print(payee + ", "); + for(Pair payee : payees) { + System.out.printf("%s who owes %.2f\n", payee.getKey(),payee.getValue()); } System.out.println(); } @@ -46,13 +85,24 @@ public float getTotalAmount() { return totalAmount; } - public ArrayList getPayees() { + public ArrayList> getPayees() { return payees; } public String getDescription(){ return description; } + + @Override + public String toString(){ + String expensesDetails = ""; + expensesDetails += "description " + description + " and amount " + totalAmount + + " paid by " + payerName + " and split between:\n"; + for(Pair payee : payees) { + expensesDetails += payee.getKey() + " who owes " + String.format("%.2f",payee.getValue()) + "\n"; + } + return expensesDetails; + } } diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index ec194b83ce..e43c3cfd32 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -7,7 +7,10 @@ public class Help { "create : Create a group.\n" + "exit : Exit current group.\n" + "member : Add a member to the group.\n" + - "expense /amount /paid /user /user ...: Add an expense.\n" + + "expense /amount /paid /user /user ...: " + + "Add an expense SPLIT EQUALLY.\n" + + "expense /unequal /amount /paid " + + "/user /user ...: Add an expense SPLIT UNEQUALLY.\n" + "list: List all expenses in the group.\n" + "balance : Show user's balance."; diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 388853fcbe..b33d2f8c2b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -6,7 +6,6 @@ import java.util.Optional; public class Parser { - /** * List of parameters to extract from user input. * For example, "/amount (amount)". @@ -123,7 +122,6 @@ public String toString() { return parser.toString(); } - public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": @@ -155,12 +153,11 @@ public void handleUserInput() throws EndProgramException, ExpensesException { String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; throw new ExpensesException(exceptionMessage); } - // Checks for missing Expense Parameters String[] expenseParams = {"amount", "paid", "user"}; for (String expenseParam : expenseParams) { if (params.get(expenseParam).isEmpty()) { - String exceptionMessage = "No description for expenses! Add /" + expenseParam; + String exceptionMessage = "No " + expenseParam + " for expenses! Add /" + expenseParam; throw new ExpensesException(exceptionMessage); } } @@ -178,33 +175,42 @@ public void handleUserInput() throws EndProgramException, ExpensesException { ArrayList payeeList = params.get("user"); String payerName = params.get("paid").get(0); payeeList.add(0, payerName); + if(this.argument.isEmpty()){ + System.out.println("Warning! Empty description"); + } - System.out.println(this.argument); - - Expense newTransaction = new Expense(payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); + //Adding expenses split equally and unequally + Expense newTransaction; + if(userInput.contains("/unequal")){ + newTransaction = new Expense(true, payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); + } + else { + newTransaction = new Expense(false, payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); + } currentGroup.get().addExpense(newTransaction); + break; case "list": // List code here break; - case "balance": - // Checks if user is currently in a Group - // named 'currentGroup1' to prevent conflict with previous declaration - Optional currentGroup1 = Group.getCurrentGroup(); - if (currentGroup1.isEmpty()) { - String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; - throw new ExpensesException(exceptionMessage); - } - - // Checks if user specified is in Current Group - if (!currentGroup1.get().isMember(argument)) { - String exceptionMessage = argument + " is not in current Group!"; - throw new ExpensesException(exceptionMessage); - } - Balance balance = new Balance(argument, currentGroup1.get()); - balance.printBalance(); - break; +// case "balance": +// // Checks if user is currently in a Group +// // named 'currentGroup1' to prevent conflict with previous declaration +// Optional currentGroup1 = Group.getCurrentGroup(); +// if (currentGroup1.isEmpty()) { +// String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; +// throw new ExpensesException(exceptionMessage); +// } +// +// // Checks if user specified is in Current Group +// if (!currentGroup1.get().isMember(argument)) { +// String exceptionMessage = argument + " is not in current Group!"; +// throw new ExpensesException(exceptionMessage); +// } +// Balance balance = new Balance(argument, currentGroup1.get()); +// balance.printBalance(); +// break; default: // Default clause System.out.println("That is not a command. " + diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 598604ad0a..0d5d1fb971 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -1,39 +1,39 @@ -package seedu.duke; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.ArrayList; -import java.util.List; - -public class BalanceTest { - @Test - public void testConstructor() { - List users = new ArrayList<>(); - users.add(new User("member1")); - users.add(new User("member2")); - users.add(new User("member3")); - - List expenses = new ArrayList<>(); - expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); - expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); - expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); - - Balance member1Balance = new Balance("member1", expenses, users); - member1Balance.printBalance(); - Balance member2Balance = new Balance("member2", expenses, users); - member2Balance.printBalance(); - Balance member3Balance = new Balance("member3", expenses, users); - member3Balance.printBalance(); - - assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); - assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); - - assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); - assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); - - assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); - assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); - } - -} +//package seedu.duke; +// +//import org.junit.jupiter.api.Test; +//import static org.junit.jupiter.api.Assertions.assertEquals; +// +//import java.util.ArrayList; +//import java.util.List; +// +//public class BalanceTest { +// @Test +// public void testConstructor() { +// List users = new ArrayList<>(); +// users.add(new User("member1")); +// users.add(new User("member2")); +// users.add(new User("member3")); +// +// List expenses = new ArrayList<>(); +// expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); +// expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); +// expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); +// +// Balance member1Balance = new Balance("member1", expenses, users); +// member1Balance.printBalance(); +// Balance member2Balance = new Balance("member2", expenses, users); +// member2Balance.printBalance(); +// Balance member3Balance = new Balance("member3", expenses, users); +// member3Balance.printBalance(); +// +// assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); +// assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); +// +// assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); +// assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); +// +// assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); +// assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); +// } +// +//} diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java index b0d1e0b51a..e4c6fdffd6 100644 --- a/src/test/java/seedu/duke/ExpenseTest.java +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -7,8 +7,11 @@ class ExpenseTest{ @Test - public void newExpenseTest() { - Expense testExpense = new Expense("Mukund",10, new String[]{"Mukund", " JX", "hehe"}); - assertEquals((float) 10, testExpense.getTotalAmount()); + public void newExpenseTest() throws ExpensesException { + Expense testExpense = new Expense(true,"mukund","disneyland", + 10, new String[]{"cohii 2", "shao 3.2", "avril 1", "hafiz 2"}); + assertEquals("description disneyland and amount 10.0 paid by mukund " + + "and split between:\ncohii who owes 2.00\nshao who owes 3.20\navril who owes 1.00" + + "\nhafiz who owes 2.00\nmukund who owes 1.80\n",testExpense.toString()); } } From f5d291de93ed8b83d76a09ad5f3f0d420fdfcfb6 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sat, 30 Mar 2024 14:10:43 +0800 Subject: [PATCH 118/270] Fix bug with SGD to USD conversions --- src/main/java/seedu/duke/CurrencyConversions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/CurrencyConversions.java b/src/main/java/seedu/duke/CurrencyConversions.java index 8664868265..96e75fadde 100644 --- a/src/main/java/seedu/duke/CurrencyConversions.java +++ b/src/main/java/seedu/duke/CurrencyConversions.java @@ -8,7 +8,7 @@ public enum CurrencyConversions { JPY("JPY", 112.12), AUD("AUD", 1.12), MYR("MYR", 3.50), - SGD("USD", 1.00), + SGD("SGD", 1.00), ; From d8df33bf9efb822972507189c2c210164f13106a Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sat, 30 Mar 2024 14:19:30 +0800 Subject: [PATCH 119/270] fix checkstyle errors in Parser and Expenses class --- build.gradle | 6 ++++++ src/main/java/seedu/duke/Expense.java | 7 +++---- src/main/java/seedu/duke/Parser.java | 9 +++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 593bf8299e..fd3c53444d 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'application' id 'checkstyle' id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'org.openjfx.javafxplugin' version '0.1.0' } repositories { @@ -45,3 +46,8 @@ run{ standardInput = System.in enableAssertions = true } + +javafx { + version = "21" + modules = [ 'javafx.controls' ] +} diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index e79d58d480..70c29f1941 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -5,7 +5,6 @@ import javafx.util.Pair; import java.util.ArrayList; -import java.util.Arrays; /** * A class to add a new expense @@ -26,7 +25,8 @@ public class Expense { * @param payeeList : String array of people who are involved in the transaction * (Index 0 is the payer and will also be added to the payees but as last index) */ - Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) throws ExpensesException { + Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) + throws ExpensesException { if(isUnequal){ float amountDueByPayees = 0; for(String payee : payeeList){ @@ -54,8 +54,7 @@ public class Expense { throw new ExpensesException(exceptionMessage); } payees.add(new Pair<>(payerName,totalAmount-amountDueByPayees)); - } - else { + } else { Float amountDue = totalAmount/payeeList.length; for(String payee : payeeList){ payees.add(new Pair<>(payee,amountDue)); diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index b33d2f8c2b..a466bbca91 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -182,10 +182,11 @@ public void handleUserInput() throws EndProgramException, ExpensesException { //Adding expenses split equally and unequally Expense newTransaction; if(userInput.contains("/unequal")){ - newTransaction = new Expense(true, payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); - } - else { - newTransaction = new Expense(false, payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); + newTransaction = new Expense(true, payerName, this.argument, + totalAmount, payeeList.toArray(new String[0])); + } else { + newTransaction = new Expense(false, payerName, this.argument, + totalAmount, payeeList.toArray(new String[0])); } currentGroup.get().addExpense(newTransaction); From eaee78717241c7d14f1ded6861a7a09d16a4c690 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sat, 30 Mar 2024 16:31:24 +0800 Subject: [PATCH 120/270] fix issues found during code review --- src/main/java/seedu/duke/Expense.java | 83 ++++++++++++++++----------- src/main/java/seedu/duke/Parser.java | 9 +-- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 70c29f1941..91071c48e5 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -27,50 +27,49 @@ public class Expense { */ Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) throws ExpensesException { - if(isUnequal){ - float amountDueByPayees = 0; - for(String payee : payeeList){ - String[] payeeInfo = payee.split(" "); - String payeeName = payeeInfo[0].trim(); - if(payeeName.equals(payerName)){ - continue; - } - try { - float amountDue = Float.parseFloat(payeeInfo[1].trim()); - amountDueByPayees += amountDue; - payees.add(new Pair<>(payeeName,amountDue)); - } catch (NumberFormatException e) { - String exceptionMessage = "Re-enter amount due for payee with name " - + payeeName + " as a proper number."; - throw new ExpensesException(exceptionMessage); - } catch (ArrayIndexOutOfBoundsException e) { - String exceptionMessage = "Amount due for payee with name " - + payeeName + " is empty. Enter it and try again"; - throw new ExpensesException(exceptionMessage); - } - } - if(amountDueByPayees > totalAmount){ - String exceptionMessage = "The amount split between users is greater than total amount. Try again."; + float amountDueByPayees = 0; + for (String payee : payeeList) { + String[] payeeInfo = payee.split(" "); + if (payeeInfo.length < 2) { + String exceptionMessage = "Amount due for payee with name " + + payeeInfo[0] + " is empty. Enter it and try again"; throw new ExpensesException(exceptionMessage); } - payees.add(new Pair<>(payerName,totalAmount-amountDueByPayees)); - } else { - Float amountDue = totalAmount/payeeList.length; - for(String payee : payeeList){ - payees.add(new Pair<>(payee,amountDue)); + String payeeName = mergeBack(payeeInfo); + try { + float amountDue = Float.parseFloat(payeeInfo[payeeInfo.length - 1]); + amountDueByPayees += amountDue; + payees.add(new Pair<>(payeeName, amountDue)); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter amount due for payee with name " + + payeeName + " as a proper number."; + throw new ExpensesException(exceptionMessage); } } + if (amountDueByPayees > totalAmount) { + String exceptionMessage = "The amount split between users is greater than total amount. Try again."; + throw new ExpensesException(exceptionMessage); + } + payees.add(new Pair<>(payerName, totalAmount - amountDueByPayees)); + this.payerName = payerName; this.totalAmount = totalAmount; this.description = description; + printSuccessMessage(); + } - System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:\n", - this.description,this.totalAmount,this.payerName); - for(Pair payee : payees) { - System.out.printf("%s who owes %.2f\n", payee.getKey(),payee.getValue()); + Expense(String payerName, String description, float totalAmount, String[] payeeList){ + Float amountDue = totalAmount / payeeList.length; + for (String payee : payeeList) { + payees.add(new Pair<>(payee, amountDue)); } - System.out.println(); + payees.add(new Pair<>(payerName, amountDue)); + + this.payerName = payerName; + this.totalAmount = totalAmount; + this.description = description; + printSuccessMessage(); } public String getPayerName() { @@ -102,6 +101,22 @@ public String toString(){ } return expensesDetails; } + private void printSuccessMessage(){ + System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:\n", + this.description, this.totalAmount, this.payerName); + for (Pair payee : payees) { + System.out.printf("%s who owes %.2f\n", payee.getKey(), payee.getValue()); + } + System.out.println(); + } + private String mergeBack(String[] splitArray){ + String mergedString = ""; + for(int i = 0; i < splitArray.length-2; i++){ + mergedString += splitArray[i].trim() + " "; + } + mergedString += splitArray[splitArray.length-2]; + return mergedString; + } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index a466bbca91..6ee0dd4acf 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -174,7 +174,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { // Obtain necessary information from 'params' and create new Expense ArrayList payeeList = params.get("user"); String payerName = params.get("paid").get(0); - payeeList.add(0, payerName); + //payeeList.add(0, payerName); if(this.argument.isEmpty()){ System.out.println("Warning! Empty description"); } @@ -184,9 +184,8 @@ public void handleUserInput() throws EndProgramException, ExpensesException { if(userInput.contains("/unequal")){ newTransaction = new Expense(true, payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); - } else { - newTransaction = new Expense(false, payerName, this.argument, - totalAmount, payeeList.toArray(new String[0])); + } else{ + newTransaction = new Expense(payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); } currentGroup.get().addExpense(newTransaction); @@ -194,6 +193,8 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; case "list": // List code here + + break; // case "balance": // // Checks if user is currently in a Group From 64cbad942a622f4c3c1fef5dcbdc8283616f25c1 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sat, 30 Mar 2024 16:43:16 +0800 Subject: [PATCH 121/270] Create UserInterface Class Create UserInterface Class, MessageType Enum for use with class, and UserInterfaceTest --- src/main/java/seedu/duke/MessageType.java | 6 +++ src/main/java/seedu/duke/UserInterface.java | 46 +++++++++++++++++++ .../java/seedu/duke/UserInterfaceTest.java | 13 ++++++ 3 files changed, 65 insertions(+) create mode 100644 src/main/java/seedu/duke/MessageType.java create mode 100644 src/main/java/seedu/duke/UserInterface.java create mode 100644 src/test/java/seedu/duke/UserInterfaceTest.java diff --git a/src/main/java/seedu/duke/MessageType.java b/src/main/java/seedu/duke/MessageType.java new file mode 100644 index 0000000000..02adedde71 --- /dev/null +++ b/src/main/java/seedu/duke/MessageType.java @@ -0,0 +1,6 @@ +package seedu.duke; + +public enum MessageType { + SUCCESS, + ERROR; +} diff --git a/src/main/java/seedu/duke/UserInterface.java b/src/main/java/seedu/duke/UserInterface.java new file mode 100644 index 0000000000..30216665fa --- /dev/null +++ b/src/main/java/seedu/duke/UserInterface.java @@ -0,0 +1,46 @@ +package seedu.duke; + +import java.io.PrintStream; + +public class UserInterface { + + private static final String SUCCESS_BORDER = "<----------SUCCESS----------->"; + private static final String ERROR_BORDER = "<-----------ERROR------------>"; + private static final String DEFAULT_BORDER = "<---------------------------->"; + private static final String HAPPY_CAT = + " /\\_/\\\n" + + " ( ^.^ )\n" + + " > ^ <"; + private static final String GRUMPY_CAT = + " /\\_/\\\n" + + " ( >_< )\n" + + " > ^ <"; + + private static final String SAD_CAT = + " /\\_/\\\n" + + " ( ._. )\n" + + " > ^ <"; + + public static void printMessage(String message, MessageType type) { + switch(type) { + case SUCCESS: + System.out.println(HAPPY_CAT); + System.out.println(SUCCESS_BORDER); + break; + case ERROR: + System.out.println(GRUMPY_CAT); + System.out.println(ERROR_BORDER); + break; + } + + System.out.println(message); + System.out.println(DEFAULT_BORDER); + } + + public static void printMessage(String message) { + System.out.println(SAD_CAT); + System.out.println(DEFAULT_BORDER); + System.out.println(message); + System.out.println(DEFAULT_BORDER); + } +} diff --git a/src/test/java/seedu/duke/UserInterfaceTest.java b/src/test/java/seedu/duke/UserInterfaceTest.java new file mode 100644 index 0000000000..fd9f6e4889 --- /dev/null +++ b/src/test/java/seedu/duke/UserInterfaceTest.java @@ -0,0 +1,13 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +public class UserInterfaceTest { + @Test + public void printTest() { + UserInterface.printMessage("Success", MessageType.SUCCESS); + UserInterface.printMessage("Message"); + UserInterface.printMessage("Error", MessageType.ERROR); + + } +} From 66c19246f9b70a86ff2222213345386ab8929288 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sat, 30 Mar 2024 22:16:44 +0800 Subject: [PATCH 122/270] Add test code --- src/main/java/seedu/duke/Money.java | 5 +- .../seedu/duke/CurrencyConversionsTest.java | 24 ++++++++ src/test/java/seedu/duke/MoneyTest.java | 58 +++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/test/java/seedu/duke/CurrencyConversionsTest.java create mode 100644 src/test/java/seedu/duke/MoneyTest.java diff --git a/src/main/java/seedu/duke/Money.java b/src/main/java/seedu/duke/Money.java index ad8a75e799..2ed9378802 100644 --- a/src/main/java/seedu/duke/Money.java +++ b/src/main/java/seedu/duke/Money.java @@ -36,10 +36,9 @@ Money subtraction(Money other, CurrencyConversions resultCurrency) { return new Money(foreignAmount, resultCurrency); } - Money multiplication(Money other, CurrencyConversions resultCurrency) { + Money multiplication(double constant, CurrencyConversions resultCurrency) { double amountInSGD = this.convertToSGD().getAmount(); - double otherAmountInSGD = other.convertToSGD().getAmount(); - double foreignAmount = (amountInSGD * otherAmountInSGD) + double foreignAmount = (amountInSGD * constant) * resultCurrency.getRate(); return new Money(foreignAmount, resultCurrency); } diff --git a/src/test/java/seedu/duke/CurrencyConversionsTest.java b/src/test/java/seedu/duke/CurrencyConversionsTest.java new file mode 100644 index 0000000000..d516a36bb7 --- /dev/null +++ b/src/test/java/seedu/duke/CurrencyConversionsTest.java @@ -0,0 +1,24 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +public class CurrencyConversionsTest { + @Test + void testCurrencyConversions() { + assert (CurrencyConversions.AUD.getName().equals("AUD")); + assert (CurrencyConversions.AUD.getRate() == 1.12); + assert (CurrencyConversions.AUD.getInverseRate() == 1.00 / 1.12); + assert (CurrencyConversions.USD.getName().equals("USD")); + assert (CurrencyConversions.USD.getRate() == 0.74); + assert (CurrencyConversions.RMB.getName().equals("RMB")); + assert (CurrencyConversions.RMB.getRate() == 5.35); + assert (CurrencyConversions.EUR.getName().equals("EUR")); + assert (CurrencyConversions.EUR.getRate() == 0.687); + assert (CurrencyConversions.JPY.getName().equals("JPY")); + assert (CurrencyConversions.JPY.getRate() == 112.12); + assert (CurrencyConversions.MYR.getName().equals("MYR")); + assert (CurrencyConversions.MYR.getRate() == 3.50); + assert (CurrencyConversions.SGD.getName().equals("SGD")); + assert (CurrencyConversions.SGD.getRate() == 1.00); + } +} diff --git a/src/test/java/seedu/duke/MoneyTest.java b/src/test/java/seedu/duke/MoneyTest.java new file mode 100644 index 0000000000..d13fd804db --- /dev/null +++ b/src/test/java/seedu/duke/MoneyTest.java @@ -0,0 +1,58 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +public class MoneyTest { + @Test + void testMoney() { + Money a = new Money(10.00, CurrencyConversions.MYR); + assert(a.getAmount() == 10.00); + assert(a.getCurrency().equals(CurrencyConversions.MYR)) ; + } + + @Test + void testAddition() { + Money sg = new Money(10.00, CurrencyConversions.SGD); + Money malaysia = new Money(10.00, CurrencyConversions.MYR); + //total in SGD + Money total = sg.addition(malaysia, CurrencyConversions.SGD); + System.out.println(total.getAmount()); + assert(total.getAmount() == 10.00 * CurrencyConversions.SGD.getRate() + + 10.00 * CurrencyConversions.MYR.getInverseRate()); + assert(total.getCurrency().equals(CurrencyConversions.SGD)); + assert(total.getAmount() == sg.convertToSGD().getAmount() + + malaysia.convertToSGD().getAmount()); + assert(total.getCurrency().equals(CurrencyConversions.SGD)); + //total in MYR + total = sg.addition(malaysia, CurrencyConversions.MYR); + assert(total.getAmount() == 10.00 * CurrencyConversions.MYR.getRate() + + 10.00); + assert(total.getAmount() == + sg.convertToOther(CurrencyConversions.MYR).getAmount() + malaysia.getAmount()); + assert(total.getCurrency().equals(CurrencyConversions.MYR)); + } + + @Test + void testMultiplication() { + Money jap = new Money(10000.00, CurrencyConversions.JPY); + //multiplied by 3 fold and then converted to euro + Money multiplied = jap.multiplication(3, CurrencyConversions.EUR); + assert(multiplied.getAmount() == new Money( + 30000.00, CurrencyConversions.JPY). + convertToOther(CurrencyConversions.EUR).getAmount()); + assert(multiplied.getCurrency().equals(CurrencyConversions.EUR)); + } + + @Test + void testAdditionAndMultiplication() { + Money sg = new Money(10.00, CurrencyConversions.SGD); + Money malaysia = new Money(10.00, CurrencyConversions.MYR); + //compute total = sg + 3 * malaysia, converted to euro; + Money total = sg.addition(malaysia.multiplication( + 3, CurrencyConversions.MYR), CurrencyConversions.EUR); + assert(total.getAmount() == + sg.addition(new Money(30.00, CurrencyConversions.MYR), + CurrencyConversions.EUR).getAmount()); + } + +} From cb3faae878154c3391721c80101876e170a87475 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sat, 30 Mar 2024 22:26:01 +0800 Subject: [PATCH 123/270] Fix checkstyle --- src/main/java/seedu/duke/Parser.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 1eb086193c..b5d20e6c7b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -208,6 +208,5 @@ public void handleUserInput() throws EndProgramException, ExpensesException { Help.printHelp(); break; } - } } From a458e118cd11a8167085d00ed633a19e08d947fe Mon Sep 17 00:00:00 2001 From: Cohii Date: Sat, 30 Mar 2024 23:03:39 +0800 Subject: [PATCH 124/270] Create Pair class Copied CS2030 utility function Pair class for use with Expense and Balance class. --- build.gradle | 10 +- src/main/java/seedu/duke/Balance.java | 172 ++++++++++++---------- src/main/java/seedu/duke/Expense.java | 3 - src/main/java/seedu/duke/Pair.java | 56 +++++++ src/test/java/seedu/duke/BalanceTest.java | 79 +++++----- 5 files changed, 193 insertions(+), 127 deletions(-) create mode 100644 src/main/java/seedu/duke/Pair.java diff --git a/build.gradle b/build.gradle index fd3c53444d..3b28db3ca4 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'application' id 'checkstyle' id 'com.github.johnrengelman.shadow' version '7.1.2' - id 'org.openjfx.javafxplugin' version '0.1.0' + // id 'org.openjfx.javafxplugin' version '0.1.0' } repositories { @@ -47,7 +47,7 @@ run{ enableAssertions = true } -javafx { - version = "21" - modules = [ 'javafx.controls' ] -} +//javafx { +// version = "21" +// modules = [ 'javafx.controls' ] +//} diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index fdea8b100c..d09242bc4d 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -1,80 +1,92 @@ -//package seedu.duke; -// -//import java.util.HashMap; -//import java.util.List; -//import java.util.Map; -// -//public class Balance { -// protected String userName; -// protected Map balanceList; -// -// public Balance(String userName, Map userList) { -// this.userName = userName; -// this.balanceList = userList; -// } -// -// public Balance(String userName, Group group){ -// this(userName, group.getExpenseList(), group.getMembers()); -// } -// -// public Balance(String userName, List expenses, List users) { -// this.userName = userName; -// this.balanceList = new HashMap<>(); -// -// // Populate balanceList with other Users from Group -// for (User user : users) { -// if(!user.getName().equals(userName)) { -// balanceList.put(user.getName(), 0f); -// } -// } -// -// // Add Expenses to balanceList -// for (Expense expense : expenses) { -// addExpense(expense); -// } -// } -// -// public String getUserName() { -// return userName; -// } -// -// public Map getBalanceList() { -// return balanceList; -// } -// -// private void addExpense(Expense expense) { -// List payees = expense.getPayees(); -// int numberOfUsers = payees.size(); -// Float amountPerUser = expense.getTotalAmount() / numberOfUsers; -// -// if(expense.getPayerName().equals(userName)) { -// for(String payee : payees) { -// if(payee.equals(userName)){ -// continue; -// } -// Float currentOwed = balanceList.get(payee); -// Float newOwed = currentOwed + amountPerUser; -// -// balanceList.put(payee, newOwed); -// } -// } else if (expense.getPayees().contains(userName)) { -// String payerName = expense.getPayerName(); -// Float currentOwed = balanceList.get(payerName); -// Float newOwed = currentOwed - amountPerUser; -// -// balanceList.put(payerName, newOwed); -// } -// } -// -// public void printBalance() { -// String firstLine = String.format("User %s's Balance List:", userName); -// System.out.println(firstLine); -// -// for (Map.Entry entry : balanceList.entrySet()) { -// String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); -// System.out.println(balanceLine); -// } -// -// System.out.println("End of Balance List"); -// } -//} +package seedu.duke; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Balance { + protected String userName; + protected Map balanceList; + + public Balance(String userName, Map userList) { + this.userName = userName; + this.balanceList = userList; + } + + public Balance(String userName, Group group){ + this(userName, group.getExpenseList(), group.getMembers()); + } + + public Balance(String userName, List expenses, List users) { + this.userName = userName; + this.balanceList = new HashMap<>(); + + // Populate balanceList with other Users from Group + for (User user : users) { + if(!user.getName().equals(userName)) { + balanceList.put(user.getName(), 0f); + } + } + + // Add Expenses to balanceList + for (Expense expense : expenses) { + addExpense(expense); + } + } + + public String getUserName() { + return userName; + } + + public Map getBalanceList() { + return balanceList; + } + + private void addExpense(Expense expense) { + String payerName = expense.getPayerName(); + List> payees = expense.getPayees(); + + if(payerName.equals(userName)) { + for(Pair payee : payees) { + String payeeName = payee.getKey(); + Float payeeAmount = payee.getValue(); + + if(payeeName.equals(userName)){ + continue; + } + + Float currentOwed = balanceList.get(payeeName); + Float newOwed = currentOwed + payeeAmount; + + balanceList.put(payeeName, newOwed); + } + } else { + for(Pair payee : payees) { + String payeeName = payee.getKey(); + Float payeeAmount = payee.getValue(); + + if (!payeeName.equals(userName)) { + continue; + } + + Float currentOwed = balanceList.get(payerName); + Float newOwed = currentOwed - payeeAmount; + + balanceList.put(payerName, newOwed); + break; + } + } + } + + public void printBalance() { + String firstLine = String.format("User %s's Balance List:", userName); + System.out.println(firstLine); + + for (Map.Entry entry : balanceList.entrySet()) { + String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); + System.out.println(balanceLine); + } + + System.out.println("End of Balance List"); + } +} diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 91071c48e5..f4915f8642 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -1,9 +1,6 @@ //@@author mukund1403 package seedu.duke; - -import javafx.util.Pair; - import java.util.ArrayList; /** diff --git a/src/main/java/seedu/duke/Pair.java b/src/main/java/seedu/duke/Pair.java new file mode 100644 index 0000000000..7a3407113f --- /dev/null +++ b/src/main/java/seedu/duke/Pair.java @@ -0,0 +1,56 @@ +package seedu.duke; + + /** + * This utility class stores two items together in a pair. + * It could be used, for instance, to faciliate returning of + * two values in a function. + * + * @author cs2030 + * @param the type of the first element + * @param the type of the second element + **/ +public class Pair { + private final T t; + private final U u; + + /** + * Creates a {@code Pair} of items. + * + * @param t first item of the pair + * @param u second item of the pair + **/ + public Pair(T t, U u) { + this.t = t; + this.u = u; + } + + /** + * Returns the first item of the pair. + * + * @return the first item of the pair + */ + public T getKey() { + return this.t; + } + + /** + * Returns the second item of the pair. + * + * @return the second item of the pair + */ + public U getValue() { + return this.u; + } + + /** + * Returns a string representation of this pair enclosed in ({@code "()"}). + * The two elements are separated by the characters {@code ", "} (comma and space). + * Elements are converted to strings as by {@link String#valueOf(Object)}. + * + * @return a string representation of this list + */ + @Override + public String toString() { + return "(" + this.t + ", " + this.u + ")"; + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 0d5d1fb971..78019a5f4e 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -1,39 +1,40 @@ -//package seedu.duke; -// -//import org.junit.jupiter.api.Test; -//import static org.junit.jupiter.api.Assertions.assertEquals; -// -//import java.util.ArrayList; -//import java.util.List; -// -//public class BalanceTest { -// @Test -// public void testConstructor() { -// List users = new ArrayList<>(); -// users.add(new User("member1")); -// users.add(new User("member2")); -// users.add(new User("member3")); -// -// List expenses = new ArrayList<>(); -// expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); -// expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); -// expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); -// -// Balance member1Balance = new Balance("member1", expenses, users); -// member1Balance.printBalance(); -// Balance member2Balance = new Balance("member2", expenses, users); -// member2Balance.printBalance(); -// Balance member3Balance = new Balance("member3", expenses, users); -// member3Balance.printBalance(); -// -// assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); -// assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); -// -// assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); -// assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); -// -// assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); -// assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); -// } -// -//} +package seedu.duke; + +import org.junit.jupiter.api.Disabled; +// import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +public class BalanceTest { + @Disabled + public void testConstructor() { + List users = new ArrayList<>(); + users.add(new User("member1")); + users.add(new User("member2")); + users.add(new User("member3")); + + List expenses = new ArrayList<>(); + // expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); + // expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); + // expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); + + Balance member1Balance = new Balance("member1", expenses, users); + member1Balance.printBalance(); + Balance member2Balance = new Balance("member2", expenses, users); + member2Balance.printBalance(); + Balance member3Balance = new Balance("member3", expenses, users); + member3Balance.printBalance(); + + assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); + assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); + + assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); + assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); + + assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); + assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); + } + +} From 52bd875ef0f21ce080cbcd4756693689987f3594 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sat, 30 Mar 2024 23:08:09 +0800 Subject: [PATCH 125/270] Fix checkstyle errors --- src/main/java/seedu/duke/Pair.java | 20 ++++++++-------- src/main/java/seedu/duke/Parser.java | 34 ++++++++++++++-------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/seedu/duke/Pair.java b/src/main/java/seedu/duke/Pair.java index 7a3407113f..1fb6a41899 100644 --- a/src/main/java/seedu/duke/Pair.java +++ b/src/main/java/seedu/duke/Pair.java @@ -1,14 +1,14 @@ package seedu.duke; - /** - * This utility class stores two items together in a pair. - * It could be used, for instance, to faciliate returning of - * two values in a function. - * - * @author cs2030 - * @param the type of the first element - * @param the type of the second element - **/ +/** + * This utility class stores two items together in a pair. + * It could be used, for instance, to faciliate returning of + * two values in a function. + * + * @author cs2030 + * @param the type of the first element + * @param the type of the second element + **/ public class Pair { private final T t; private final U u; @@ -53,4 +53,4 @@ public U getValue() { public String toString() { return "(" + this.t + ", " + this.u + ")"; } -} \ No newline at end of file +} diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 6ee0dd4acf..3ef53fa773 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -196,23 +196,23 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; -// case "balance": -// // Checks if user is currently in a Group -// // named 'currentGroup1' to prevent conflict with previous declaration -// Optional currentGroup1 = Group.getCurrentGroup(); -// if (currentGroup1.isEmpty()) { -// String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; -// throw new ExpensesException(exceptionMessage); -// } -// -// // Checks if user specified is in Current Group -// if (!currentGroup1.get().isMember(argument)) { -// String exceptionMessage = argument + " is not in current Group!"; -// throw new ExpensesException(exceptionMessage); -// } -// Balance balance = new Balance(argument, currentGroup1.get()); -// balance.printBalance(); -// break; + case "balance": + // Checks if user is currently in a Group + // named 'currentGroup1' to prevent conflict with previous declaration + Optional currentGroup1 = Group.getCurrentGroup(); + if (currentGroup1.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + + // Checks if user specified is in Current Group + if (!currentGroup1.get().isMember(argument)) { + String exceptionMessage = argument + " is not in current Group!"; + throw new ExpensesException(exceptionMessage); + } + Balance balance = new Balance(argument, currentGroup1.get()); + balance.printBalance(); + break; default: // Default clause System.out.println("That is not a command. " + From 9c88ef7e4495937bec7656c72fb79e458630dff4 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sat, 30 Mar 2024 23:11:21 +0800 Subject: [PATCH 126/270] Fix IO tests --- text-ui-test/EXPECTED.TXT | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 0de20b7ef1..0c1dfb3b97 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -13,7 +13,8 @@ help: Access help menu. create : Create a group. exit : Exit current group. member : Add a member to the group. -expense /amount /paid /user /user ...: Add an expense. +expense /amount /paid /user /user ...: Add an expense SPLIT EQUALLY. +expense /unequal /amount /paid /user /user ...: Add an expense SPLIT UNEQUALLY. list: List all expenses in the group. balance : Show user's balance. Goodbye! From 1414676c54fb6d9476a2910e5d8883a1bff2b5f4 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 23:29:18 +0800 Subject: [PATCH 127/270] Update GroupStorage --- .../java/seedu/duke/storage/GroupStorage.java | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index cddd4eb579..a87cf24e76 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,6 +1,8 @@ package seedu.duke.storage; +import javafx.util.Pair; import seedu.duke.Expense; +import seedu.duke.ExpensesException; import seedu.duke.Group; import seedu.duke.User; import seedu.duke.exceptions.GroupLoadException; @@ -14,7 +16,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -25,6 +26,8 @@ public class GroupStorage { private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; private static final String EXPENSE_DELIMITER = ","; + private static final String PAYEE_DELIMITER = ":"; + private static final String PAYEE_DATA_DELIMITER = ","; private final FileIO fileIO; @@ -98,15 +101,21 @@ private void saveExpenses(BufferedWriter writer, List expenses) throws writer.write(EXPENSES_HEADER); writer.newLine(); for (Expense expense : expenses) { - String expenseData = String.format("%.2f%s%s%s%s", - expense.getTotalAmount(), EXPENSE_DELIMITER, - expense.getPayerName(), EXPENSE_DELIMITER, - String.join(EXPENSE_DELIMITER, expense.getPayees())); - writer.write(expenseData); + StringBuilder expenseData = new StringBuilder(); + expenseData.append(expense.getTotalAmount()).append(EXPENSE_DELIMITER) + .append(expense.getPayerName()).append(EXPENSE_DELIMITER) + .append(expense.getDescription()).append(EXPENSE_DELIMITER); + + List payeeData = new ArrayList<>(); + for (Pair payee : expense.getPayees()) { + payeeData.add(payee.getKey() + PAYEE_DELIMITER + payee.getValue()); + } + expenseData.append(String.join(PAYEE_DATA_DELIMITER, payeeData)); + + writer.write(expenseData.toString()); writer.newLine(); } } - /** * Loads the group information from a file. * @@ -169,16 +178,27 @@ private void loadMembers(BufferedReader reader, Group group) throws IOException private void loadExpenses(BufferedReader reader, Group group) throws IOException { String line; while ((line = reader.readLine()) != null) { - String[] expenseData = line.split(EXPENSE_DELIMITER, 3); + String[] expenseData = line.split(EXPENSE_DELIMITER, 4); float totalAmount = Float.parseFloat(expenseData[0]); String payerName = expenseData[1]; - String[] payeeNames = expenseData[2].split(EXPENSE_DELIMITER); + String description = expenseData[2]; + String[] payeeData = expenseData[3].split(PAYEE_DATA_DELIMITER); List payeeList = new ArrayList<>(); - Collections.addAll(payeeList, payeeNames); + for (String payee : payeeData) { + String[] payeeInfo = payee.split(PAYEE_DELIMITER); + String payeeName = payeeInfo[0]; + float amountDue = Float.parseFloat(payeeInfo[1]); + payeeList.add(payeeName + " " + amountDue); + } - Expense expense = new Expense(payerName, totalAmount, payeeList.toArray(new String[0])); - group.addExpense(expense); + try { + Expense expense = new Expense(false, payerName, description, totalAmount, + payeeList.toArray(new String[0])); + group.addExpense(expense); + } catch (ExpensesException e) { + System.out.println("Error loading expense: " + e.getMessage()); + } } } From 2e8ba85ce67864705d44aca3a810af1f9edb754b Mon Sep 17 00:00:00 2001 From: Cohii Date: Sat, 30 Mar 2024 23:39:14 +0800 Subject: [PATCH 128/270] Edit comments --- src/main/java/seedu/duke/Pair.java | 32 ------------------------------ 1 file changed, 32 deletions(-) diff --git a/src/main/java/seedu/duke/Pair.java b/src/main/java/seedu/duke/Pair.java index 1fb6a41899..d360996e0e 100644 --- a/src/main/java/seedu/duke/Pair.java +++ b/src/main/java/seedu/duke/Pair.java @@ -1,54 +1,22 @@ package seedu.duke; -/** - * This utility class stores two items together in a pair. - * It could be used, for instance, to faciliate returning of - * two values in a function. - * - * @author cs2030 - * @param the type of the first element - * @param the type of the second element - **/ public class Pair { private final T t; private final U u; - /** - * Creates a {@code Pair} of items. - * - * @param t first item of the pair - * @param u second item of the pair - **/ public Pair(T t, U u) { this.t = t; this.u = u; } - /** - * Returns the first item of the pair. - * - * @return the first item of the pair - */ public T getKey() { return this.t; } - /** - * Returns the second item of the pair. - * - * @return the second item of the pair - */ public U getValue() { return this.u; } - /** - * Returns a string representation of this pair enclosed in ({@code "()"}). - * The two elements are separated by the characters {@code ", "} (comma and space). - * Elements are converted to strings as by {@link String#valueOf(Object)}. - * - * @return a string representation of this list - */ @Override public String toString() { return "(" + this.t + ", " + this.u + ")"; From a8f5370256ead7bdcf34ef0303ba4437a0f9a1bd Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 30 Mar 2024 23:40:09 +0800 Subject: [PATCH 129/270] Fix checkstyle --- src/main/java/seedu/duke/Parser.java | 36 +++++++++++++--------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 6ee0dd4acf..478fa49dfc 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -193,26 +193,24 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; case "list": // List code here - - break; -// case "balance": -// // Checks if user is currently in a Group -// // named 'currentGroup1' to prevent conflict with previous declaration -// Optional currentGroup1 = Group.getCurrentGroup(); -// if (currentGroup1.isEmpty()) { -// String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; -// throw new ExpensesException(exceptionMessage); -// } -// -// // Checks if user specified is in Current Group -// if (!currentGroup1.get().isMember(argument)) { -// String exceptionMessage = argument + " is not in current Group!"; -// throw new ExpensesException(exceptionMessage); -// } -// Balance balance = new Balance(argument, currentGroup1.get()); -// balance.printBalance(); -// break; + // case "balance": + // // Checks if user is currently in a Group + // // named 'currentGroup1' to prevent conflict with previous declaration + // Optional currentGroup1 = Group.getCurrentGroup(); + // if (currentGroup1.isEmpty()) { + // String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + // throw new ExpensesException(exceptionMessage); + // } + // + // // Checks if user specified is in Current Group + // if (!currentGroup1.get().isMember(argument)) { + // String exceptionMessage = argument + " is not in current Group!"; + // throw new ExpensesException(exceptionMessage); + // } + // Balance balance = new Balance(argument, currentGroup1.get()); + // balance.printBalance(); + // break; default: // Default clause System.out.println("That is not a command. " + From e1653317ff8ff02cd1470b5bf7867b7c0ea697bb Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sun, 31 Mar 2024 13:09:01 +0800 Subject: [PATCH 130/270] Gambling feature --- src/main/java/seedu/duke/Expense.java | 1 - src/main/java/seedu/duke/Gambling.java | 69 +++++++++++ src/main/java/seedu/duke/Help.java | 3 +- src/main/java/seedu/duke/Parser.java | 4 + src/main/java/seedu/duke/SlotMachine.java | 114 ++++++++++++++++++ .../java/seedu/duke/storage/GroupStorage.java | 7 +- src/main/java/seedu/duke/storage/Slot.java | 10 ++ 7 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 src/main/java/seedu/duke/Gambling.java create mode 100644 src/main/java/seedu/duke/SlotMachine.java create mode 100644 src/main/java/seedu/duke/storage/Slot.java diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 66df42bbaa..306b76e43a 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -2,7 +2,6 @@ package seedu.duke; -import javafx.util.Pair; import java.util.ArrayList; /** diff --git a/src/main/java/seedu/duke/Gambling.java b/src/main/java/seedu/duke/Gambling.java new file mode 100644 index 0000000000..55488af6c6 --- /dev/null +++ b/src/main/java/seedu/duke/Gambling.java @@ -0,0 +1,69 @@ +package seedu.duke; + +import java.util.Scanner; + +public class Gambling { + private static final String icon = + " .=*+::. \n" + + " =*=-:::. \n" + + " .:=+**#***####*+=-:. \n" + + " :+#%%%%###%########%%%%*=: \n" + + " .=#%%%####################%%%#=. \n" + + " +#%%%####%%%###%%%###%%%%#*###%%#= \n" + + " :##%%########%%###%%###%%#########%%#: \n" + + " .##%#*#%%%%#######*##*#######%%%##*#%%#: \n" + + " *%%%##%%###%%%############%%%%%%%%##%%%#. \n" + + " .%#*****+*****++*++*****+**++***********%= \n" + + " :%*-::.-=-:::-==:==-:::-=:-===---==-.::+%+ \n" + + " :%*=:.+@@@%%@@@*=@@@#%%@@%-@@@@%@@@@-.-+#+ .=-.. \n" + + " :%*=.=%+*=+***@-%%+*=+##*@=*%++==**+%::+#=.##--- \n" + + " :%*=.#%-++=--*%-@#-++=--*@+=%-+*=--*@=:+#+ -::-: \n" + + " :%*=.#@%@%=-*@%-@@%@%=-#@@+=@%@%=-#@@=:+#+ -+: \n" + + " -%*=:=@@%---%@@:%@@%---%@@=*@@%---%@@::+#+ =*: \n" + + " -%*=:.#@@###@@@*+@@@###@@@-@@@@###@@=.-+#+ -+: \n" + + " -%*=:.:*##**+***:********+:***=+=**-..:+%*--+*: \n" + + " :%*+=====-==========-=-===============+*%*---- \n" + + " +##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##*. \n" + + " =******************************************* \n" + + " =++=++++++++++++++++++++++++++++++++++++++++ \n" + + " =*++++++++++++++++++++++++++++++++++++++++*+ \n" + + " :----------------------------------------::. "; + private static final String welcome = + "Gamble Gamble Gamble! Crazy Slots!!!\n" + + "Win if all 3 middle slots are the same!!"; + static void printWelcome() { + System.out.println(icon); + System.out.println(welcome); + } + static void startGambling() { + SlotMachine slotMachine = new SlotMachine(9, 9); + System.out.println(slotMachine); + Scanner in = new Scanner(System.in); + System.out.println("/exit to leave. /reroll to roll again"); + while (in.hasNextLine()) { + String userInput = in.nextLine(); + switch (userInput) { + case "/exit": + System.out.println("leaving the gambling den"); + return; + case "/reroll": + System.out.println("/exit to leave. /reroll to roll again"); + slotMachine.reroll(); + System.out.println(slotMachine); + if (slotMachine.isWin()) { + System.out.println("All debts clear!!"); + } else { + System.out.println("Poor, reroll again to win"); + } + break; + default: + break; + } + } + } + + + + + +} diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index e43c3cfd32..7ac5c5d244 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -12,7 +12,8 @@ public class Help { "expense /unequal /amount /paid " + "/user /user ...: Add an expense SPLIT UNEQUALLY.\n" + "list: List all expenses in the group.\n" + - "balance : Show user's balance."; + "balance : Show user's balance.\n" + + "gamble : gamble away your debts"; static void printHelp() { System.out.println(prompt); diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index ca7a4f720e..baf6494963 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -190,6 +190,10 @@ public void handleUserInput() throws EndProgramException, ExpensesException { currentGroup.get().addExpense(newTransaction); + break; + case "gamble": + Gambling.printWelcome(); + Gambling.startGambling(); break; case "list": // List code here diff --git a/src/main/java/seedu/duke/SlotMachine.java b/src/main/java/seedu/duke/SlotMachine.java new file mode 100644 index 0000000000..eb4a1f2879 --- /dev/null +++ b/src/main/java/seedu/duke/SlotMachine.java @@ -0,0 +1,114 @@ +package seedu.duke; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +public class SlotMachine { + private static final String slotOutputs = "%^#@$*!~"; + private List> slotMachine; + + SlotMachine(int numRows, int numCols) { + // create slot machine matrix + slotMachine = new ArrayList<>(numRows); + for (int row = 0; row < numRows; row++) { + List currentRow = new ArrayList<>(numCols); + for (int j = 0; j < numCols; j++) { + // Initialize each cell with empty char + currentRow.add(' '); + } + slotMachine.add(currentRow); + } + // randomise each slot + fillSlots(); + } + + // Fill each slot [3x3] with random characters + private void fillSlots() { + Random rand = ThreadLocalRandom.current(); + for (int row = 0; row < slotMachine.size(); row += 3) { + for (int col = 0; col < slotMachine.get(0).size(); col += 3) { + char randomChar = slotOutputs.charAt(rand.nextInt(slotOutputs.length())); + //one char in each slot + fillSlot(row, col, randomChar); + } + } + } + + // Fill one slot with the same random character + private void fillSlot(int startRow, int startCol, char character) { + for (int i = startRow; i < startRow + 3; i++) { + for (int j = startCol; j < startCol + 3; j++) { + slotMachine.get(i).set(j, character); + } + } + } + + // randomise characters + void reroll() { + fillSlots(); + } + + // Override toString method to print the slot machine + @Override + public String toString() { + StringBuilder matrix = new StringBuilder(); + + // Draw the slot machine + for (int i = 0; i < slotMachine.size(); i += 3) { + // Draw the top of the boxes + for (int j = 0; j < slotMachine.get(0).size(); j += 3) { + matrix.append("┌───┐"); + } + matrix.append("\n"); + // Draw the content of each slot + for (int row = i; row < i + 3; row++) { + for (int j = 0; j < slotMachine.get(0).size(); j += 3) { + matrix.append("│"); + for (int col = j; col < j + 3; col++) { + matrix.append(slotMachine.get(row).get(col)); + } + matrix.append("│"); + } + matrix.append("\n"); + } + // Draw the bottom of the boxes + for (int j = 0; j < slotMachine.get(0).size(); j += 3) { + matrix.append("└───┘"); + } + matrix.append("\n"); + } + return matrix.toString(); + } + // Check if all characters in the middle rows are the same + boolean isWin() { + for (int i = slotMachine.size() / 2 - 1; i <= slotMachine.size() / 2 + 1; i++) { + char firstChar = slotMachine.get(i).get(0); + for (int j = 1; j < slotMachine.get(i).size(); j++) { + if (slotMachine.get(i).get(j) != firstChar) { + return false; + } + } + } + return true; + } + +// public static void main(String[] args) { +// int rows = 9; // Total rows in the slot machine +// int cols = 9; // Total columns in the slot machine +// SlotMachine slotMachine = new SlotMachine(rows, cols); +// System.out.println("Initial Slot Machine:"); +// System.out.println(slotMachine); +// +// slotMachine.reroll(); +// System.out.println("After Reroll"); +// System.out.println(slotMachine); +// // Check if all characters in the middle rows are the same +// if (slotMachine.checkMiddleRows()) { +// System.out.println("All characters in the middle rows are the same."); +// } else { +// System.out.println("Not all characters in the middle rows are the same."); +// } +// } +} diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index a87cf24e76..dee50f8af9 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,6 +1,5 @@ package seedu.duke.storage; -import javafx.util.Pair; import seedu.duke.Expense; import seedu.duke.ExpensesException; import seedu.duke.Group; @@ -107,9 +106,9 @@ private void saveExpenses(BufferedWriter writer, List expenses) throws .append(expense.getDescription()).append(EXPENSE_DELIMITER); List payeeData = new ArrayList<>(); - for (Pair payee : expense.getPayees()) { - payeeData.add(payee.getKey() + PAYEE_DELIMITER + payee.getValue()); - } +// for (Pair payee : expense.getPayees()) { +// payeeData.add(payee.getKey() + PAYEE_DELIMITER + payee.getValue()); +// } expenseData.append(String.join(PAYEE_DATA_DELIMITER, payeeData)); writer.write(expenseData.toString()); diff --git a/src/main/java/seedu/duke/storage/Slot.java b/src/main/java/seedu/duke/storage/Slot.java new file mode 100644 index 0000000000..ab637f6067 --- /dev/null +++ b/src/main/java/seedu/duke/storage/Slot.java @@ -0,0 +1,10 @@ +package seedu.duke.storage; + +public class Slot { + private final char randomChar; + + public Slot(char randomChar) { + this.randomChar = randomChar; + } + +} From 21925354509e693b661deb383c87386ec58e75dc Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sun, 31 Mar 2024 13:12:31 +0800 Subject: [PATCH 131/270] update pair from javafx to inbuilt class and fix issue with equal split --- src/main/java/seedu/duke/Expense.java | 4 ++-- src/main/java/seedu/duke/storage/GroupStorage.java | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 66df42bbaa..f144c5d501 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -2,7 +2,6 @@ package seedu.duke; -import javafx.util.Pair; import java.util.ArrayList; /** @@ -57,7 +56,7 @@ public Expense(boolean isUnequal, String payerName, String description, float to } Expense(String payerName, String description, float totalAmount, String[] payeeList){ - Float amountDue = totalAmount / payeeList.length; + Float amountDue = totalAmount / (payeeList.length + 1); for (String payee : payeeList) { payees.add(new Pair<>(payee, amountDue)); } @@ -85,6 +84,7 @@ public ArrayList> getPayees() { return payees; } + //@@author mukund1403 public String getDescription(){ return description; } diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index a87cf24e76..f532759d96 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,10 +1,6 @@ package seedu.duke.storage; -import javafx.util.Pair; -import seedu.duke.Expense; -import seedu.duke.ExpensesException; -import seedu.duke.Group; -import seedu.duke.User; +import seedu.duke.*; import seedu.duke.exceptions.GroupLoadException; import seedu.duke.exceptions.GroupSaveException; From 984b55d1fda99a739d940761c909a83ca52f8477 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sun, 31 Mar 2024 13:12:53 +0800 Subject: [PATCH 132/270] Clean out comments --- src/main/java/seedu/duke/SlotMachine.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/seedu/duke/SlotMachine.java b/src/main/java/seedu/duke/SlotMachine.java index eb4a1f2879..31f1f3d17b 100644 --- a/src/main/java/seedu/duke/SlotMachine.java +++ b/src/main/java/seedu/duke/SlotMachine.java @@ -93,22 +93,4 @@ boolean isWin() { } return true; } - -// public static void main(String[] args) { -// int rows = 9; // Total rows in the slot machine -// int cols = 9; // Total columns in the slot machine -// SlotMachine slotMachine = new SlotMachine(rows, cols); -// System.out.println("Initial Slot Machine:"); -// System.out.println(slotMachine); -// -// slotMachine.reroll(); -// System.out.println("After Reroll"); -// System.out.println(slotMachine); -// // Check if all characters in the middle rows are the same -// if (slotMachine.checkMiddleRows()) { -// System.out.println("All characters in the middle rows are the same."); -// } else { -// System.out.println("Not all characters in the middle rows are the same."); -// } -// } } From 1266dd665c4e276a551abb585c589d71dba65be7 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 31 Mar 2024 13:44:17 +0800 Subject: [PATCH 133/270] Add citation Add citation for Pair class. --- src/main/java/seedu/duke/Pair.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/seedu/duke/Pair.java b/src/main/java/seedu/duke/Pair.java index d360996e0e..4ce900436e 100644 --- a/src/main/java/seedu/duke/Pair.java +++ b/src/main/java/seedu/duke/Pair.java @@ -1,5 +1,7 @@ package seedu.duke; +//@@author cs2030-reused +//Utility class for Java Pair given in cs2030 module public class Pair { private final T t; private final U u; @@ -22,3 +24,4 @@ public String toString() { return "(" + this.t + ", " + this.u + ")"; } } +//@@author From b89385da291ad3681f8c6486076faf090ae2a1f0 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sun, 31 Mar 2024 13:46:34 +0800 Subject: [PATCH 134/270] Clean out checkstyle bugs --- src/main/java/seedu/duke/Help.java | 2 +- src/main/java/seedu/duke/{Gambling.java => Luck.java} | 2 +- src/main/java/seedu/duke/Parser.java | 6 +++--- src/main/java/seedu/duke/storage/GroupStorage.java | 8 ++++---- src/main/java/seedu/duke/storage/Slot.java | 10 ---------- 5 files changed, 9 insertions(+), 19 deletions(-) rename src/main/java/seedu/duke/{Gambling.java => Luck.java} (99%) delete mode 100644 src/main/java/seedu/duke/storage/Slot.java diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index 7ac5c5d244..b0a2706c35 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -13,7 +13,7 @@ public class Help { "/user /user ...: Add an expense SPLIT UNEQUALLY.\n" + "list: List all expenses in the group.\n" + "balance : Show user's balance.\n" + - "gamble : gamble away your debts"; + "luck : luck is in the air tonight"; static void printHelp() { System.out.println(prompt); diff --git a/src/main/java/seedu/duke/Gambling.java b/src/main/java/seedu/duke/Luck.java similarity index 99% rename from src/main/java/seedu/duke/Gambling.java rename to src/main/java/seedu/duke/Luck.java index 55488af6c6..31fc1cfd62 100644 --- a/src/main/java/seedu/duke/Gambling.java +++ b/src/main/java/seedu/duke/Luck.java @@ -2,7 +2,7 @@ import java.util.Scanner; -public class Gambling { +public class Luck { private static final String icon = " .=*+::. \n" + " =*=-:::. \n" + diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index baf6494963..7d4dbdcdd4 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -191,9 +191,9 @@ public void handleUserInput() throws EndProgramException, ExpensesException { break; - case "gamble": - Gambling.printWelcome(); - Gambling.startGambling(); + case "luck": + Luck.printWelcome(); + Luck.startGambling(); break; case "list": // List code here diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index dee50f8af9..df69f6afc7 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,5 +1,5 @@ package seedu.duke.storage; - +import seedu.duke.Pair; import seedu.duke.Expense; import seedu.duke.ExpensesException; import seedu.duke.Group; @@ -106,9 +106,9 @@ private void saveExpenses(BufferedWriter writer, List expenses) throws .append(expense.getDescription()).append(EXPENSE_DELIMITER); List payeeData = new ArrayList<>(); -// for (Pair payee : expense.getPayees()) { -// payeeData.add(payee.getKey() + PAYEE_DELIMITER + payee.getValue()); -// } + for (Pair payee : expense.getPayees()) { + payeeData.add(payee.getKey() + PAYEE_DELIMITER + payee.getValue()); + } expenseData.append(String.join(PAYEE_DATA_DELIMITER, payeeData)); writer.write(expenseData.toString()); diff --git a/src/main/java/seedu/duke/storage/Slot.java b/src/main/java/seedu/duke/storage/Slot.java deleted file mode 100644 index ab637f6067..0000000000 --- a/src/main/java/seedu/duke/storage/Slot.java +++ /dev/null @@ -1,10 +0,0 @@ -package seedu.duke.storage; - -public class Slot { - private final char randomChar; - - public Slot(char randomChar) { - this.randomChar = randomChar; - } - -} From a486c1bd114cab8ca8612b28c7c148dfce9897c4 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sun, 31 Mar 2024 13:50:52 +0800 Subject: [PATCH 135/270] Fix helpTest --- src/test/java/seedu/duke/HelpTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/test/java/seedu/duke/HelpTest.java b/src/test/java/seedu/duke/HelpTest.java index 7d0727f910..80ed5367b8 100644 --- a/src/test/java/seedu/duke/HelpTest.java +++ b/src/test/java/seedu/duke/HelpTest.java @@ -7,13 +7,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class HelpTest { private static final String prompt = - "Welcome\n" + + "Welcome, here is a list of commands:\n" + "help: Access help menu.\n" + - "group : Create or enter a group.\n" + - "member : Add a member to the group.\n" + - "expense /amount /paid /user /user ...: Add an expense.\n" + + "create : Create a group.\n" + + "exit : Exit current group.\n" + + "member : Add a member to the group.\n" + + "expense /amount /paid /user /user ...: " + + "Add an expense SPLIT EQUALLY.\n" + + "expense /unequal /amount /paid " + + "/user /user ...: Add an expense SPLIT UNEQUALLY.\n" + "list: List all expenses in the group.\n" + - "balance : Show user's balance.\n"; + "balance : Show user's balance.\n" + + "luck : luck is in the air tonight"; @Test public void dummyTest() { From e5a705c0ef70b46e755a5b5795d7715f3c45d2b7 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Sun, 31 Mar 2024 13:55:32 +0800 Subject: [PATCH 136/270] Change EXPECTED.TXT for new help --- text-ui-test/EXPECTED.TXT | 1 + 1 file changed, 1 insertion(+) diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 0c1dfb3b97..fb7cbf22f2 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -17,4 +17,5 @@ expense /amount /paid /user /user /unequal /amount /paid /user /user ...: Add an expense SPLIT UNEQUALLY. list: List all expenses in the group. balance : Show user's balance. +luck : luck is in the air tonight Goodbye! From 7eee70c9a7bd3053cb75bc66b46c147089e14ce3 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 31 Mar 2024 14:00:06 +0800 Subject: [PATCH 137/270] Update Balance Unit Tests --- src/main/java/seedu/duke/Expense.java | 1 - .../java/seedu/duke/storage/GroupStorage.java | 6 +--- src/test/java/seedu/duke/BalanceTest.java | 29 ++++++++++--------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 66df42bbaa..306b76e43a 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -2,7 +2,6 @@ package seedu.duke; -import javafx.util.Pair; import java.util.ArrayList; /** diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index a87cf24e76..f532759d96 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,10 +1,6 @@ package seedu.duke.storage; -import javafx.util.Pair; -import seedu.duke.Expense; -import seedu.duke.ExpensesException; -import seedu.duke.Group; -import seedu.duke.User; +import seedu.duke.*; import seedu.duke.exceptions.GroupLoadException; import seedu.duke.exceptions.GroupSaveException; diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 78019a5f4e..72339ce8d7 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -1,24 +1,27 @@ package seedu.duke; -import org.junit.jupiter.api.Disabled; -// import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; import java.util.List; public class BalanceTest { - @Disabled - public void testConstructor() { + @Test + public void testConstructor() throws ExpensesException { List users = new ArrayList<>(); users.add(new User("member1")); users.add(new User("member2")); users.add(new User("member3")); List expenses = new ArrayList<>(); - // expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); - // expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); - // expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); + // Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) + expenses.add(new Expense(true, "member1", "expense1", 20f, + new String[]{"member2 5", "member3 10"})); + expenses.add(new Expense("member2", "expense2", 30f, + new String[]{"member1", "member3"})); + expenses.add(new Expense("member3", "expense3", 100f, + new String[]{"member1"})); Balance member1Balance = new Balance("member1", expenses, users); member1Balance.printBalance(); @@ -27,14 +30,14 @@ public void testConstructor() { Balance member3Balance = new Balance("member3", expenses, users); member3Balance.printBalance(); - assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); - assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); + assertEquals(-10.0f, member1Balance.getBalanceList().get("member2")); + assertEquals(-90.0f, member1Balance.getBalanceList().get("member3")); - assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); - assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); + assertEquals(10.0f, member2Balance.getBalanceList().get("member1")); + assertEquals(15.0f, member2Balance.getBalanceList().get("member3")); - assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); - assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); + assertEquals(90.0f, member3Balance.getBalanceList().get("member1")); + assertEquals(-15.0f, member3Balance.getBalanceList().get("member2")); } } From f853f231ceac61c4f3fda25ff6aa8c7cf9b170d9 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 31 Mar 2024 14:06:10 +0800 Subject: [PATCH 138/270] Revert "Update Balance Unit Tests" This reverts commit 7eee70c9a7bd3053cb75bc66b46c147089e14ce3. --- src/main/java/seedu/duke/Expense.java | 1 + .../java/seedu/duke/storage/GroupStorage.java | 6 +++- src/test/java/seedu/duke/BalanceTest.java | 29 +++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 306b76e43a..66df42bbaa 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -2,6 +2,7 @@ package seedu.duke; +import javafx.util.Pair; import java.util.ArrayList; /** diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index f532759d96..a87cf24e76 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -1,6 +1,10 @@ package seedu.duke.storage; -import seedu.duke.*; +import javafx.util.Pair; +import seedu.duke.Expense; +import seedu.duke.ExpensesException; +import seedu.duke.Group; +import seedu.duke.User; import seedu.duke.exceptions.GroupLoadException; import seedu.duke.exceptions.GroupSaveException; diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 72339ce8d7..78019a5f4e 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -1,27 +1,24 @@ package seedu.duke; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +// import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; import java.util.List; public class BalanceTest { - @Test - public void testConstructor() throws ExpensesException { + @Disabled + public void testConstructor() { List users = new ArrayList<>(); users.add(new User("member1")); users.add(new User("member2")); users.add(new User("member3")); List expenses = new ArrayList<>(); - // Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) - expenses.add(new Expense(true, "member1", "expense1", 20f, - new String[]{"member2 5", "member3 10"})); - expenses.add(new Expense("member2", "expense2", 30f, - new String[]{"member1", "member3"})); - expenses.add(new Expense("member3", "expense3", 100f, - new String[]{"member1"})); + // expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); + // expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); + // expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); Balance member1Balance = new Balance("member1", expenses, users); member1Balance.printBalance(); @@ -30,14 +27,14 @@ public void testConstructor() throws ExpensesException { Balance member3Balance = new Balance("member3", expenses, users); member3Balance.printBalance(); - assertEquals(-10.0f, member1Balance.getBalanceList().get("member2")); - assertEquals(-90.0f, member1Balance.getBalanceList().get("member3")); + assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); + assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); - assertEquals(10.0f, member2Balance.getBalanceList().get("member1")); - assertEquals(15.0f, member2Balance.getBalanceList().get("member3")); + assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); + assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); - assertEquals(90.0f, member3Balance.getBalanceList().get("member1")); - assertEquals(-15.0f, member3Balance.getBalanceList().get("member2")); + assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); + assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); } } From cd84180b098a8ae442de6e7a484fb48c3895283f Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 31 Mar 2024 14:12:18 +0800 Subject: [PATCH 139/270] Update Balance Unit Test --- src/test/java/seedu/duke/BalanceTest.java | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 78019a5f4e..11508287ce 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -1,24 +1,26 @@ package seedu.duke; -import org.junit.jupiter.api.Disabled; -// import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; import java.util.List; public class BalanceTest { - @Disabled - public void testConstructor() { + @Test + public void testConstructor() throws ExpensesException { List users = new ArrayList<>(); users.add(new User("member1")); users.add(new User("member2")); users.add(new User("member3")); List expenses = new ArrayList<>(); - // expenses.add(new Expense("member1", 15f, new String[]{"member1", "member2", "member3"})); - // expenses.add(new Expense("member2", 30f, new String[]{"member2", "member1", "member3"})); - // expenses.add(new Expense("member3", 100f, new String[]{"member3", "member1"})); + expenses.add(new Expense(true, "member1", "expense1", 20f, + new String[]{"member2 5", "member3 10"})); + expenses.add(new Expense("member2", "expense2", 30f, + new String[]{"member1", "member3"})); + expenses.add(new Expense("member3", "expense3", 100f, + new String[]{"member1"})); Balance member1Balance = new Balance("member1", expenses, users); member1Balance.printBalance(); @@ -27,14 +29,13 @@ public void testConstructor() { Balance member3Balance = new Balance("member3", expenses, users); member3Balance.printBalance(); - assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); - assertEquals(-45.0f, member1Balance.getBalanceList().get("member3")); + assertEquals(-10.0f, member1Balance.getBalanceList().get("member2")); + assertEquals(-90.0f, member1Balance.getBalanceList().get("member3")); - assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); - assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); + assertEquals(10.0f, member2Balance.getBalanceList().get("member1")); + assertEquals(15.0f, member2Balance.getBalanceList().get("member3")); - assertEquals(45.0f, member3Balance.getBalanceList().get("member1")); - assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); + assertEquals(90.0f, member3Balance.getBalanceList().get("member1")); + assertEquals(-15.0f, member3Balance.getBalanceList().get("member2")); } - } From a91d01763edc323cba2af531c27293fc91f578b1 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 31 Mar 2024 17:47:15 +0800 Subject: [PATCH 140/270] Settle up function --- src/main/java/seedu/duke/Expense.java | 41 +++++++----- src/main/java/seedu/duke/Group.java | 55 +++++++++++++++- src/main/java/seedu/duke/Parser.java | 19 +++++- src/main/java/seedu/duke/Settle.java | 70 +++++++++++++++++++++ src/main/java/seedu/duke/UserInterface.java | 18 +++--- src/test/java/seedu/duke/SettleTest.java | 4 ++ 6 files changed, 178 insertions(+), 29 deletions(-) create mode 100644 src/main/java/seedu/duke/Settle.java create mode 100644 src/test/java/seedu/duke/SettleTest.java diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index f144c5d501..7b944002b9 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -10,18 +10,19 @@ public class Expense { private String payerName; private float totalAmount; - private ArrayList> payees = new ArrayList<>(); + private ArrayList> payees = new ArrayList<>(); private String description; /** * Constructor to create new Expense - * @param isUnequal : Boolean showing whether expense is split unequally or not - * @param payerName : The name of the user who paid for the Expense + * + * @param isUnequal : Boolean showing whether expense is split unequally or not + * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided - * @param payeeList : String array of people who are involved in the transaction - * (Index 0 is the payer and will also be added to the payees but as last index) + * @param payeeList : String array of people who are involved in the transaction + * (Index 0 is the payer and will also be added to the payees but as last index) */ public Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) throws ExpensesException { @@ -55,7 +56,7 @@ public Expense(boolean isUnequal, String payerName, String description, float to printSuccessMessage(); } - Expense(String payerName, String description, float totalAmount, String[] payeeList){ + Expense(String payerName, String description, float totalAmount, String[] payeeList) { Float amountDue = totalAmount / (payeeList.length + 1); for (String payee : payeeList) { payees.add(new Pair<>(payee, amountDue)); @@ -68,6 +69,9 @@ public Expense(boolean isUnequal, String payerName, String description, float to printSuccessMessage(); } + public Expense(User payer, double amount) { + } + //@@author Cohii2 public String getPayerName() { return payerName; @@ -80,26 +84,27 @@ public float getTotalAmount() { return totalAmount; } - public ArrayList> getPayees() { + public ArrayList> getPayees() { return payees; } //@@author mukund1403 - public String getDescription(){ + public String getDescription() { return description; } @Override - public String toString(){ + public String toString() { String expensesDetails = ""; expensesDetails += "description " + description + " and amount " + totalAmount + " paid by " + payerName + " and split between:\n"; - for(Pair payee : payees) { - expensesDetails += payee.getKey() + " who owes " + String.format("%.2f",payee.getValue()) + "\n"; + for (Pair payee : payees) { + expensesDetails += payee.getKey() + " who owes " + String.format("%.2f", payee.getValue()) + "\n"; } return expensesDetails; } - private void printSuccessMessage(){ + + void printSuccessMessage() { System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:\n", this.description, this.totalAmount, this.payerName); for (Pair payee : payees) { @@ -107,14 +112,20 @@ private void printSuccessMessage(){ } System.out.println(); } - private String mergeBack(String[] splitArray){ + + private String mergeBack(String[] splitArray) { String mergedString = ""; - for(int i = 0; i < splitArray.length-2; i++){ + for (int i = 0; i < splitArray.length - 2; i++) { mergedString += splitArray[i].trim() + " "; } - mergedString += splitArray[splitArray.length-2]; + mergedString += splitArray[splitArray.length - 2]; return mergedString; } + + public String getPayer() { + return payerName; + } } + diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 9768b1e581..47695b0159 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -14,7 +14,7 @@ import java.util.Optional; public class Group { - static final Map groups = new HashMap<>(); + static Map groups = new HashMap<>(); private static Optional currentGroupName = Optional.empty(); private static final GroupStorage groupStorage = new GroupStorage(new FileIOImpl()); @@ -37,6 +37,13 @@ private Group(String groupName) { * @throws IllegalStateException If trying to create or join a new group while already in another group. */ public static Optional getOrCreateGroup(String groupName) { + + // Check if group name is empty + if (groupName == null || groupName.trim().isEmpty()) { + System.out.println("Group name cannot be empty. Please try again."); + return Optional.empty(); + } + // Check if user is accessing a group they are already in getCurrentGroup().ifPresent(currentGroup -> { if (currentGroup.getGroupName().equals(groupName)) { @@ -56,6 +63,7 @@ public static Optional getOrCreateGroup(String groupName) { Optional group = Optional.ofNullable(groups.get(groupName)); + // Create a new group if it doesn't exist if (group.isEmpty()) { Group newGroup = new Group(groupName); groups.put(groupName, newGroup); @@ -89,7 +97,7 @@ public static Optional enterGroup(String groupName) { System.out.println("Group does not exist."); return Optional.empty(); } - // @@author hafizuddin-a + // @@author hafizuddin-a } catch (GroupLoadException e) { System.out.println("Error loading group: " + e.getMessage()); return Optional.empty(); @@ -213,5 +221,48 @@ public List getMembers() { public List getExpenseList() { return new ArrayList<>(expenseList); } + + public void settle(String payerName, String payeeName) { + User payer = findUser(payerName); + User payee = findUser(payeeName); + + if (payer == null || payee == null) { + System.out.println("User not found."); + return; + } + + double amount = calculateOutstandingAmount(payee, payer); + + if (amount > 0) { + Settle settle = new Settle(payer, payee, amount); + addExpense(settle); + System.out.println("Settled! " + payerName + " should pay " + payeeName + String.format("%.2f", amount)); + } else { + System.out.println(payerName + " does not owe " + payeeName + " any money."); + } + } + + private User findUser(String userName) { + for (User user : members) { + if (user.getName().equals(userName)) { + return user; + } + } + return null; + } + + private double calculateOutstandingAmount(User payer, User payee) { + double totalAmount = 0; + for (Expense expense : expenseList) { + if (expense.getPayer().equals(payer.getName())) { + for (Pair user : expense.getPayees()) { + if (user.getKey().equals(payee.getName())) { + totalAmount += user.getValue(); + } + } + } + } + return totalAmount; + } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 7d4dbdcdd4..b4cf3e2fa3 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -175,21 +175,34 @@ public void handleUserInput() throws EndProgramException, ExpensesException { ArrayList payeeList = params.get("user"); String payerName = params.get("paid").get(0); //payeeList.add(0, payerName); - if(this.argument.isEmpty()){ + if (this.argument.isEmpty()) { System.out.println("Warning! Empty description"); } //Adding expenses split equally and unequally Expense newTransaction; - if(userInput.contains("/unequal")){ + if (userInput.contains("/unequal")) { newTransaction = new Expense(true, payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); - } else{ + } else { newTransaction = new Expense(payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); } currentGroup.get().addExpense(newTransaction); + break; + case "settle": + String[] commandParts = userInput.split(" "); + if (commandParts.length < 4 || !commandParts[2].equals("/user")) { + System.out.println("Invalid command. Syntax: settle payerName /user payeeName"); + return; + } + + String payer = commandParts[1]; + String payee = commandParts[3]; + + Group.getCurrentGroup().ifPresent(group -> group.settle(payer, payee)); + break; case "luck": Luck.printWelcome(); diff --git a/src/main/java/seedu/duke/Settle.java b/src/main/java/seedu/duke/Settle.java new file mode 100644 index 0000000000..0fe2570416 --- /dev/null +++ b/src/main/java/seedu/duke/Settle.java @@ -0,0 +1,70 @@ +//@@author avrilgk +package seedu.duke; + + +/** + * The Settle class represents a transaction between two users. + * It extends the Expense class and has a payer, payee and amount. + *

+ * Each Settle object represents a single transaction where one user (the payer) + * pays another user (the payee) a certain amount. + */ + +public class Settle extends Expense { + private final User payer; + private final User payee; + private final double amount; + + /** + * Constructs a new Settle object. + * + * @param payer The user who is making the payment + * @param payee The user who is receiving the payment + * @param amount The amount of the payment + */ + public Settle(User payer, User payee, double amount) { + super(payer, amount); + this.payer = payer; + this.payee = payee; + this.amount = amount; + } + + /** + * Returns the user who is making the payment. + * + * @return The user who is making the payment + */ + + public String getPayer() { + return payer.getName(); + } + + /** + * Returns the user who is receiving the payment. + * + * @return The user who is receiving the payment + */ + public String getPayee() { + return payee.getName(); + } + + /** + * Returns the amount of the payment. + * + * @return The amount of the payment + */ + public double getAmount() { + return amount; + } + + /** + * Returns a string representation of the Settle object. + * The returned string is in the format "payerName paid payeeName amount". + * + * @return A string representation of the Settle object + */ + @Override + public String toString() { + return payer.getName() + " paid " + payee.getName() + " " + amount; + } +} diff --git a/src/main/java/seedu/duke/UserInterface.java b/src/main/java/seedu/duke/UserInterface.java index 30216665fa..27e8caa434 100644 --- a/src/main/java/seedu/duke/UserInterface.java +++ b/src/main/java/seedu/duke/UserInterface.java @@ -1,7 +1,5 @@ package seedu.duke; -import java.io.PrintStream; - public class UserInterface { private static final String SUCCESS_BORDER = "<----------SUCCESS----------->"; @@ -9,20 +7,20 @@ public class UserInterface { private static final String DEFAULT_BORDER = "<---------------------------->"; private static final String HAPPY_CAT = " /\\_/\\\n" + - " ( ^.^ )\n" + - " > ^ <"; + " ( ^.^ )\n" + + " > ^ <"; private static final String GRUMPY_CAT = " /\\_/\\\n" + - " ( >_< )\n" + - " > ^ <"; + " ( >_< )\n" + + " > ^ <"; private static final String SAD_CAT = " /\\_/\\\n" + - " ( ._. )\n" + - " > ^ <"; + " ( ._. )\n" + + " > ^ <"; public static void printMessage(String message, MessageType type) { - switch(type) { + switch (type) { case SUCCESS: System.out.println(HAPPY_CAT); System.out.println(SUCCESS_BORDER); @@ -31,6 +29,8 @@ public static void printMessage(String message, MessageType type) { System.out.println(GRUMPY_CAT); System.out.println(ERROR_BORDER); break; + default: + break; } System.out.println(message); diff --git a/src/test/java/seedu/duke/SettleTest.java b/src/test/java/seedu/duke/SettleTest.java new file mode 100644 index 0000000000..d964eff2d3 --- /dev/null +++ b/src/test/java/seedu/duke/SettleTest.java @@ -0,0 +1,4 @@ +package seedu.duke; + +public class SettleTest { +} From 980abdf57636a5b594c0bdd2c7c3fa39ba80b51f Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 31 Mar 2024 17:51:42 +0800 Subject: [PATCH 141/270] Fix test fails --- src/test/java/seedu/duke/DukeTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java index 2dda5fd651..9df986a996 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/duke/DukeTest.java @@ -1,12 +1,12 @@ package seedu.duke; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; -class DukeTest { +import static org.junit.jupiter.api.Assertions.*; + +public class DukeTest { @Test - public void sampleTest() { - assertTrue(true); + public void groupTest() { + // test code here } -} +} \ No newline at end of file From 894c21411c8432659071c508ec9368be71069bab Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 31 Mar 2024 17:56:21 +0800 Subject: [PATCH 142/270] Fix test fails --- src/test/java/seedu/duke/DukeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java index 9df986a996..74c7ea070b 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/duke/DukeTest.java @@ -2,11 +2,11 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; public class DukeTest { @Test public void groupTest() { // test code here } -} \ No newline at end of file +} From 5030a653f8b3cbe3b822deeed6d3301a896fba6e Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 31 Mar 2024 17:58:20 +0800 Subject: [PATCH 143/270] Fix test fails --- src/test/java/seedu/duke/DukeTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java index 74c7ea070b..f3d4ed2e55 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/duke/DukeTest.java @@ -2,8 +2,6 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class DukeTest { @Test public void groupTest() { From 5763375064479c1f1c69bdcc83c34d4b4ae171d4 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 31 Mar 2024 18:23:43 +0800 Subject: [PATCH 144/270] Updated settle function --- src/main/java/seedu/duke/Group.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 47695b0159..09d44ae117 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -236,7 +236,7 @@ public void settle(String payerName, String payeeName) { if (amount > 0) { Settle settle = new Settle(payer, payee, amount); addExpense(settle); - System.out.println("Settled! " + payerName + " should pay " + payeeName + String.format("%.2f", amount)); + System.out.println(payerName + " should pay " + payeeName + " " + String.format("%.2f", amount)); } else { System.out.println(payerName + " does not owe " + payeeName + " any money."); } @@ -260,6 +260,12 @@ private double calculateOutstandingAmount(User payer, User payee) { totalAmount += user.getValue(); } } + } else if (expense.getPayer().equals(payee.getName())) { + for (Pair user : expense.getPayees()) { + if (user.getKey().equals(payer.getName())) { + totalAmount -= user.getValue(); + } + } } } return totalAmount; From df07df3880510975589b58d0b5d53a03ed3e1ff9 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Mon, 1 Apr 2024 22:22:35 +0800 Subject: [PATCH 145/270] add expense command class, list command class, commands package --- src/main/java/seedu/duke/Duke.java | 2 + src/main/java/seedu/duke/Expense.java | 52 ++------- src/main/java/seedu/duke/Parser.java | 52 ++------- src/main/java/seedu/duke/UserInterface.java | 3 +- .../seedu/duke/commands/ExpenseCommand.java | 110 ++++++++++++++++++ .../java/seedu/duke/commands/ListCommand.java | 25 ++++ .../{ => exceptions}/ExpensesException.java | 2 +- .../java/seedu/duke/storage/GroupStorage.java | 10 +- src/test/java/seedu/duke/BalanceTest.java | 3 + src/test/java/seedu/duke/ExpenseTest.java | 1 + 10 files changed, 167 insertions(+), 93 deletions(-) create mode 100644 src/main/java/seedu/duke/commands/ExpenseCommand.java create mode 100644 src/main/java/seedu/duke/commands/ListCommand.java rename src/main/java/seedu/duke/{ => exceptions}/ExpensesException.java (86%) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 1ab7b9d88e..8b899cc60f 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,5 +1,7 @@ package seedu.duke; +import seedu.duke.exceptions.ExpensesException; + import java.util.Scanner; public class Duke { diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index f144c5d501..ecbff6c323 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -2,8 +2,11 @@ package seedu.duke; +import seedu.duke.exceptions.ExpensesException; + import java.util.ArrayList; + /** * A class to add a new expense */ @@ -20,48 +23,21 @@ public class Expense { * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided - * @param payeeList : String array of people who are involved in the transaction - * (Index 0 is the payer and will also be added to the payees but as last index) + * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and + * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) */ - public Expense(boolean isUnequal, String payerName, String description, float totalAmount, String[] payeeList) + public Expense(boolean isUnequal, String payerName, String description, + float totalAmount, ArrayList> payees) throws ExpensesException { - float amountDueByPayees = 0; - for (String payee : payeeList) { - String[] payeeInfo = payee.split(" "); - if (payeeInfo.length < 2) { - String exceptionMessage = "Amount due for payee with name " - + payeeInfo[0] + " is empty. Enter it and try again"; - throw new ExpensesException(exceptionMessage); - } - String payeeName = mergeBack(payeeInfo); - try { - float amountDue = Float.parseFloat(payeeInfo[payeeInfo.length - 1]); - amountDueByPayees += amountDue; - payees.add(new Pair<>(payeeName, amountDue)); - } catch (NumberFormatException e) { - String exceptionMessage = "Re-enter amount due for payee with name " - + payeeName + " as a proper number."; - throw new ExpensesException(exceptionMessage); - } - } - if (amountDueByPayees > totalAmount) { - String exceptionMessage = "The amount split between users is greater than total amount. Try again."; - throw new ExpensesException(exceptionMessage); - } - payees.add(new Pair<>(payerName, totalAmount - amountDueByPayees)); + this.payees = payees; this.payerName = payerName; this.totalAmount = totalAmount; this.description = description; printSuccessMessage(); } - Expense(String payerName, String description, float totalAmount, String[] payeeList){ - Float amountDue = totalAmount / (payeeList.length + 1); - for (String payee : payeeList) { - payees.add(new Pair<>(payee, amountDue)); - } - payees.add(new Pair<>(payerName, amountDue)); - + public Expense(String payerName, String description, float totalAmount, ArrayList> payees){ + this.payees = payees; this.payerName = payerName; this.totalAmount = totalAmount; this.description = description; @@ -107,14 +83,6 @@ private void printSuccessMessage(){ } System.out.println(); } - private String mergeBack(String[] splitArray){ - String mergedString = ""; - for(int i = 0; i < splitArray.length-2; i++){ - mergedString += splitArray[i].trim() + " "; - } - mergedString += splitArray[splitArray.length-2]; - return mergedString; - } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 7d4dbdcdd4..7e92379734 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,5 +1,9 @@ package seedu.duke; +import seedu.duke.commands.ExpenseCommand; +import seedu.duke.commands.ListCommand; +import seedu.duke.exceptions.ExpensesException; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -147,56 +151,14 @@ public void handleUserInput() throws EndProgramException, ExpensesException { GroupCommand.exitGroup(); break; case "expense": - // Checks if user is currently in a Group - Optional currentGroup = Group.getCurrentGroup(); - if (currentGroup.isEmpty()) { - String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; - throw new ExpensesException(exceptionMessage); - } - // Checks for missing Expense Parameters - String[] expenseParams = {"amount", "paid", "user"}; - for (String expenseParam : expenseParams) { - if (params.get(expenseParam).isEmpty()) { - String exceptionMessage = "No " + expenseParam + " for expenses! Add /" + expenseParam; - throw new ExpensesException(exceptionMessage); - } - } - - // Checks if amount is a valid Float - float totalAmount; - try { - totalAmount = Float.parseFloat(params.get("amount").get(0)); - } catch (NumberFormatException e) { - String exceptionMessage = "Re-enter expense with amount as a proper number."; - throw new ExpensesException(exceptionMessage); - } - - // Obtain necessary information from 'params' and create new Expense - ArrayList payeeList = params.get("user"); - String payerName = params.get("paid").get(0); - //payeeList.add(0, payerName); - if(this.argument.isEmpty()){ - System.out.println("Warning! Empty description"); - } - - //Adding expenses split equally and unequally - Expense newTransaction; - if(userInput.contains("/unequal")){ - newTransaction = new Expense(true, payerName, this.argument, - totalAmount, payeeList.toArray(new String[0])); - } else{ - newTransaction = new Expense(payerName, this.argument, totalAmount, payeeList.toArray(new String[0])); - } - currentGroup.get().addExpense(newTransaction); - - + ExpenseCommand.addExpense(params, argument, userInput); break; case "luck": Luck.printWelcome(); Luck.startGambling(); break; case "list": - // List code here + ListCommand.printList(); break; case "balance": // Checks if user is currently in a Group @@ -216,11 +178,11 @@ public void handleUserInput() throws EndProgramException, ExpensesException { balance.printBalance(); break; default: - // Default clause System.out.println("That is not a command. " + "Please use one of the commands given here"); Help.printHelp(); break; } } + } diff --git a/src/main/java/seedu/duke/UserInterface.java b/src/main/java/seedu/duke/UserInterface.java index 30216665fa..03cdfb726f 100644 --- a/src/main/java/seedu/duke/UserInterface.java +++ b/src/main/java/seedu/duke/UserInterface.java @@ -1,6 +1,5 @@ package seedu.duke; -import java.io.PrintStream; public class UserInterface { @@ -31,6 +30,8 @@ public static void printMessage(String message, MessageType type) { System.out.println(GRUMPY_CAT); System.out.println(ERROR_BORDER); break; + default: + break; } System.out.println(message); diff --git a/src/main/java/seedu/duke/commands/ExpenseCommand.java b/src/main/java/seedu/duke/commands/ExpenseCommand.java new file mode 100644 index 0000000000..e3b5552173 --- /dev/null +++ b/src/main/java/seedu/duke/commands/ExpenseCommand.java @@ -0,0 +1,110 @@ +package seedu.duke.commands; + +import seedu.duke.Expense; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.Group; +import seedu.duke.Pair; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Optional; + +public class ExpenseCommand { + + //@@author Cohii2 + public static void addExpense(HashMap > params,String argument, String userInput) + throws ExpensesException { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + + String[] expenseParams = {"amount", "paid", "user"}; + for (String expenseParam : expenseParams) { + if (params.get(expenseParam).isEmpty()) { + String exceptionMessage = "No " + expenseParam + " for expenses! Add /" + expenseParam; + throw new ExpensesException(exceptionMessage); + } + } + + float totalAmount = checkTotal(params); + + // Obtain necessary information from 'params' and create new Expense + ArrayList payeeList = params.get("user"); + String payerName = params.get("paid").get(0); + if(argument.isEmpty()){ + System.out.println("Warning! Empty description"); + } + + Expense newTransaction; + ArrayList> payees = new ArrayList<>(); + if(userInput.contains("/unequal")){ + newTransaction = addUnequalExpense(payeeList, payees, totalAmount, payerName, argument); + } else { + newTransaction = addEqualExpense(payeeList, payees, totalAmount, payerName, argument); + } + currentGroup.get().addExpense(newTransaction); + } + + //@@author mukund1403 + private static Float checkTotal(HashMap > params) throws ExpensesException { + float totalAmount; + try { + totalAmount = Float.parseFloat(params.get("amount").get(0)); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter expense with amount as a proper number."; + throw new ExpensesException(exceptionMessage); + } + return totalAmount; + } + private static Expense addUnequalExpense(ArrayList payeeList,ArrayList> payees, + float totalAmount,String payerName,String argument) throws ExpensesException{ + float amountDueByPayees = 0; + int payeeInfoMinLength = 2; + for (String payee : payeeList) { + String[] payeeInfo = payee.split(" "); + + if (payeeInfo.length < payeeInfoMinLength) { + String exceptionMessage = "Amount due for payee with name " + + payeeInfo[0] + " is empty. Enter it and try again"; + throw new ExpensesException(exceptionMessage); + } + String payeeName = mergeBack(payeeInfo); + try { + float amountDue = Float.parseFloat(payeeInfo[payeeInfo.length - 1]); + amountDueByPayees += amountDue; + payees.add(new Pair<>(payeeName, amountDue)); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter amount due for payee with name " + + payeeName + " as a proper number."; + throw new ExpensesException(exceptionMessage); + } + } + if (amountDueByPayees > totalAmount) { + String exceptionMessage = "The amount split between users is greater than total amount. Try again."; + throw new ExpensesException(exceptionMessage); + } + payees.add(new Pair<>(payerName, totalAmount - amountDueByPayees)); + return new Expense(true, payerName, argument, totalAmount, payees); + } + + private static Expense addEqualExpense(ArrayList payeeList, ArrayList> payees, + float totalAmount, String payerName, String argument){ + Float amountDue = totalAmount / (payeeList.size() + 1); + for (String payee : payeeList) { + payees.add(new Pair<>(payee, amountDue)); + } + payees.add(new Pair<>(payerName, amountDue)); + return new Expense(payerName, argument, totalAmount, payees); + } + + private static String mergeBack(String[] splitArray){ + String mergedString = ""; + for(int i = 0; i < splitArray.length-2; i++){ + mergedString += splitArray[i].trim() + " "; + } + mergedString += splitArray[splitArray.length-2]; + return mergedString; + } +} diff --git a/src/main/java/seedu/duke/commands/ListCommand.java b/src/main/java/seedu/duke/commands/ListCommand.java new file mode 100644 index 0000000000..88e36005fa --- /dev/null +++ b/src/main/java/seedu/duke/commands/ListCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.commands; +//@@author mukund1403 +import seedu.duke.Expense; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.Group; + +import java.util.List; +import java.util.Optional; + +public class ListCommand { + public static void printList() throws ExpensesException { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + List expenses = currentGroup.get().getExpenseList(); + System.out.println("The expenses for this group are:\n"); + int i = 1; + for(Expense expense : expenses){ + System.out.println(i + ". " + expense.toString()); + i++; + } + } +} diff --git a/src/main/java/seedu/duke/ExpensesException.java b/src/main/java/seedu/duke/exceptions/ExpensesException.java similarity index 86% rename from src/main/java/seedu/duke/ExpensesException.java rename to src/main/java/seedu/duke/exceptions/ExpensesException.java index 5fee696bb3..50c0daa3ca 100644 --- a/src/main/java/seedu/duke/ExpensesException.java +++ b/src/main/java/seedu/duke/exceptions/ExpensesException.java @@ -1,4 +1,4 @@ -package seedu.duke; +package seedu.duke.exceptions; public class ExpensesException extends Exception { public ExpensesException(String s, Throwable err){ diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index aa3238f45e..3e4fc9eeaa 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -2,7 +2,7 @@ import seedu.duke.Pair; import seedu.duke.Expense; -import seedu.duke.ExpensesException; +import seedu.duke.exceptions.ExpensesException; import seedu.duke.Group; import seedu.duke.User; import seedu.duke.exceptions.GroupLoadException; @@ -184,17 +184,19 @@ private void loadExpenses(BufferedReader reader, Group group) throws IOException String description = expenseData[2]; String[] payeeData = expenseData[3].split(PAYEE_DATA_DELIMITER); - List payeeList = new ArrayList<>(); + //List payeeList = new ArrayList<>(); + ArrayList> payeeList = new ArrayList<>(); for (String payee : payeeData) { String[] payeeInfo = payee.split(PAYEE_DELIMITER); String payeeName = payeeInfo[0]; float amountDue = Float.parseFloat(payeeInfo[1]); - payeeList.add(payeeName + " " + amountDue); + //payeeList.add(payeeName + " " + amountDue); + payeeList.add(new Pair<>(payeeName,amountDue)); } try { Expense expense = new Expense(false, payerName, description, totalAmount, - payeeList.toArray(new String[0])); + payeeList); group.addExpense(expense); } catch (ExpensesException e) { System.out.println("Error loading expense: " + e.getMessage()); diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 11508287ce..6f1a94a0b1 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -1,11 +1,14 @@ package seedu.duke; import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ExpensesException; + import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; import java.util.List; +//@@author Cohii2 public class BalanceTest { @Test public void testConstructor() throws ExpensesException { diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java index e4c6fdffd6..a6149651d3 100644 --- a/src/test/java/seedu/duke/ExpenseTest.java +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -1,6 +1,7 @@ package seedu.duke; import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ExpensesException; import static org.junit.jupiter.api.Assertions.assertEquals; From 08034e128b0dd1ae2db722e968c7804a4eb01c03 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Tue, 2 Apr 2024 00:45:34 +0800 Subject: [PATCH 146/270] Updated DG for settle function --- docs/DeveloperGuide.md | 38 ++++++++++++++++++++++------ src/main/java/seedu/duke/Help.java | 1 + src/main/java/seedu/duke/Settle.java | 21 +++------------ 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 3d8101bd6f..d21ceec446 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -8,13 +8,15 @@ original source as well} ## Design & implementation {Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} + ### Help menu feature + #### Implementation -The "help" feature is facilitated by the `Help` class. + +The "help" feature is facilitated by the `Help` class. It provides a static method `printHelp` to print out a guide on how to use the commands in the application. `printHelp` can be used in the event the user issues an invalid command - ### Group Creation feature #### Implementation @@ -55,30 +57,52 @@ steps: #### Implementation -The Expenses feature is facilitated through the Expense class. It allows users to add a new Expense through creation of a new Expense object. Users can specify amount paid, the payee, and the members of the group involved in the transaction. +The Expenses feature is facilitated through the Expense class. It allows users to add a new Expense through creation of +a new Expense object. Users can specify amount paid, the payee, and the members of the group involved in the +transaction. Additionally, it implements the following operations: + + `Expenses#payer()` - Gives the name of the member who paid for the expense + `Expenses#totalAmount()` - Returns the total amount of the expense + `Expenses#payees()` - Returns all the members involved in the transaction -These operations are exposed in the Expense class through the `getPayerName()`, `getTotalAmount()`, and `getPayees()` functions respectively. +These operations are exposed in the Expense class through the `getPayerName()`, `getTotalAmount()`, and `getPayees()` +functions respectively. ### Balance feature #### Implementation -The Balance feature is facilitated through the Balance class. +The Balance feature is facilitated through the Balance class. It allows a user to view a printed list of other users in the Group, and the amount that is owed by/to each user. Each `Balance` object contains a String of a user `userName`, and a Map `balanceList`. This Map uses String of other users' usernames as Key, and -a Float of the amount that is owed by/to each user. +a Float of the amount that is owed by/to each user. To print a user's Balance List, perform the following steps: + 1. Create a `Balance` object with the params String `userName` and the current Group `group`. 2. From the `members` and `expenseList` List items in `group`, the Map `balanceList` is populated. 3. Call method `printBalance()` to print the contents of the Map `balanceList`. +### Settle feature + +#### Implementation + +The Settle feature is facilitated through the Settle class. +It allows a user to settle the debts between two users in a Group. + +The `Settle` class contains a `settleDebt(String userName1, String userName2)` method. +This method takes in two Strings `userName1` and `userName2` as parameters, representing the two users to settle the +debt between. +The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed +by `userName2` to `userName1`. +It then prints out the total amount that is owed between the two users, and prompts the user to enter the amount to +settle the debt. +The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed +by `userName2` to `userName1` after the settlement. + ## Product scope ### Target user profile @@ -93,7 +117,6 @@ The application gives an accurate and simple way to represent unsettled debts be ## User Stories - | Version | As a ... | I want to ... | So that I can ... | |---------|----------|----------------------------------------------------------------|-------------------------------------------------------------| | v1.0 | new user | see usage instructions | refer to them when I forget how to use the application | @@ -103,7 +126,6 @@ The application gives an accurate and simple way to represent unsettled debts be | v1.0 | user | check how much I owe each member in the group | keep track of my debts | | v2.0 | user | find a to-do item by name | locate a to-do without having to go through the entire list | - ## Non-Functional Requirements {Give non-functional requirements} diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java index b0a2706c35..111d1830a8 100644 --- a/src/main/java/seedu/duke/Help.java +++ b/src/main/java/seedu/duke/Help.java @@ -13,6 +13,7 @@ public class Help { "/user /user ...: Add an expense SPLIT UNEQUALLY.\n" + "list: List all expenses in the group.\n" + "balance : Show user's balance.\n" + + "settle /user : Settle the amount between two users.\n" + "luck : luck is in the air tonight"; static void printHelp() { diff --git a/src/main/java/seedu/duke/Settle.java b/src/main/java/seedu/duke/Settle.java index 0fe2570416..3210ff9b11 100644 --- a/src/main/java/seedu/duke/Settle.java +++ b/src/main/java/seedu/duke/Settle.java @@ -24,6 +24,9 @@ public class Settle extends Expense { */ public Settle(User payer, User payee, double amount) { super(payer, amount); + assert payer != null : "Payer cannot be null"; + assert payee != null : "Payee cannot be null"; + assert amount >= 0 : "Amount cannot be negative"; this.payer = payer; this.payee = payee; this.amount = amount; @@ -39,24 +42,6 @@ public String getPayer() { return payer.getName(); } - /** - * Returns the user who is receiving the payment. - * - * @return The user who is receiving the payment - */ - public String getPayee() { - return payee.getName(); - } - - /** - * Returns the amount of the payment. - * - * @return The amount of the payment - */ - public double getAmount() { - return amount; - } - /** * Returns a string representation of the Settle object. * The returned string is in the format "payerName paid payeeName amount". From 4cbcb3039daf374598a46082f9e347ec0fa3e324 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Tue, 2 Apr 2024 02:55:23 +0800 Subject: [PATCH 147/270] Add UniversalExceptions to exceptions package --- src/main/java/seedu/duke/exceptions/GroupLoadException.java | 2 +- src/main/java/seedu/duke/exceptions/GroupSaveException.java | 2 +- .../java/seedu/duke/{ => exceptions}/UniversalExceptions.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/seedu/duke/{ => exceptions}/UniversalExceptions.java (91%) diff --git a/src/main/java/seedu/duke/exceptions/GroupLoadException.java b/src/main/java/seedu/duke/exceptions/GroupLoadException.java index 6a2625da39..389a2ad48f 100644 --- a/src/main/java/seedu/duke/exceptions/GroupLoadException.java +++ b/src/main/java/seedu/duke/exceptions/GroupLoadException.java @@ -4,7 +4,7 @@ * Represents an exception that occurs when loading group information fails. * This exception is thrown when an error occurs while loading a group from a file. */ -public class GroupLoadException extends Exception { +public class GroupLoadException extends UniversalExceptions { /** * Constructs a new GroupLoadException with the specified detail message. * diff --git a/src/main/java/seedu/duke/exceptions/GroupSaveException.java b/src/main/java/seedu/duke/exceptions/GroupSaveException.java index 47deb1246d..ea739307b5 100644 --- a/src/main/java/seedu/duke/exceptions/GroupSaveException.java +++ b/src/main/java/seedu/duke/exceptions/GroupSaveException.java @@ -4,7 +4,7 @@ * Represents an exception that occurs when saving group information fails. * This exception is thrown when an error occurs while saving a group to a file. */ -public class GroupSaveException extends Exception { +public class GroupSaveException extends UniversalExceptions { /** * Constructs a new GroupSaveException with the specified detail message. * diff --git a/src/main/java/seedu/duke/UniversalExceptions.java b/src/main/java/seedu/duke/exceptions/UniversalExceptions.java similarity index 91% rename from src/main/java/seedu/duke/UniversalExceptions.java rename to src/main/java/seedu/duke/exceptions/UniversalExceptions.java index b25a68f05b..445a81526c 100644 --- a/src/main/java/seedu/duke/UniversalExceptions.java +++ b/src/main/java/seedu/duke/exceptions/UniversalExceptions.java @@ -1,4 +1,4 @@ -package seedu.duke; +package seedu.duke.exceptions; public class UniversalExceptions extends Exception { private final String errorMessage; From 39ce4d6f3644200e490ab591eafb357ac423f38f Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Tue, 2 Apr 2024 03:17:35 +0800 Subject: [PATCH 148/270] Optional for loadedgroup --- src/main/java/seedu/duke/Group.java | 8 ++++---- src/main/java/seedu/duke/storage/GroupStorage.java | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 09d44ae117..3d10756f28 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -88,10 +88,10 @@ public static Optional enterGroup(String groupName) { //@@author hafizuddin-a try { // If the group doesn't exist in memory, try loading it from file - Group loadedGroup = groupStorage.loadGroupFromFile(groupName); - if (loadedGroup != null) { - groups.put(groupName, loadedGroup); - group = Optional.of(loadedGroup); + Optional loadedGroup = Optional.ofNullable(groupStorage.loadGroupFromFile(groupName)); + if (loadedGroup.isPresent()) { + groups.put(groupName, loadedGroup.get()); + group = loadedGroup; } else { //@@ author avrilgk System.out.println("Group does not exist."); diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 3e4fc9eeaa..9248a8bfcd 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -184,13 +184,11 @@ private void loadExpenses(BufferedReader reader, Group group) throws IOException String description = expenseData[2]; String[] payeeData = expenseData[3].split(PAYEE_DATA_DELIMITER); - //List payeeList = new ArrayList<>(); ArrayList> payeeList = new ArrayList<>(); for (String payee : payeeData) { String[] payeeInfo = payee.split(PAYEE_DELIMITER); String payeeName = payeeInfo[0]; float amountDue = Float.parseFloat(payeeInfo[1]); - //payeeList.add(payeeName + " " + amountDue); payeeList.add(new Pair<>(payeeName,amountDue)); } From 266bf30142f39d3a3b457c0e2e6a194000b80a54 Mon Sep 17 00:00:00 2001 From: "[avrilgk]" <[avrilguok@gmail.com]> Date: Wed, 3 Apr 2024 17:51:48 +0800 Subject: [PATCH 149/270] Update UG and DG --- docs/DeveloperGuide.md | 7 +++-- docs/UserGuide.md | 71 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index d21ceec446..d2f304abf4 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -93,13 +93,14 @@ To print a user's Balance List, perform the following steps: The Settle feature is facilitated through the Settle class. It allows a user to settle the debts between two users in a Group. -The `Settle` class contains a `settleDebt(String userName1, String userName2)` method. + The `Settle` class contains a `settleDebt(String userName1, String userName2)` method. This method takes in two Strings `userName1` and `userName2` as parameters, representing the two users to settle the debt between. + The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed -by `userName2` to `userName1`. -It then prints out the total amount that is owed between the two users, and prompts the user to enter the amount to +by `userName2` to `userName1`. It then prints out the total amount that is owed between the two users, and prompts the user to enter the amount to settle the debt. + The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed by `userName2` to `userName1` after the settlement. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..71646ed1fd 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,7 +2,8 @@ ## Introduction -{Give a product intro} +Split-liang is an application that helps you split expenses with friends in a fun way! + ## Quick Start @@ -15,28 +16,72 @@ {Give detailed description of each feature} -### Adding a todo: `todo` -Adds a new item to the list of todo items. +### Viewing help: `help` +Shows a message explaining how to use the application. -Format: `todo n/TODO_NAME d/DEADLINE` +Format: `help` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +### Creating a group: `create group` -Example of usage: +Creates a new group with the specified group name. -`todo n/Write the rest of the User Guide d/next week` +Format: `create group GROUP_NAME` -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +- `GROUP_NAME` is the name of the group. -## FAQ +Example: `create Friends` + +This command will create a new group named 'Friends'. + +### Entering a group: `enter` + +Enters an existing group with the specified group name. + +Format: `enter GROUP_NAME` + +- `GROUP_NAME` is the name of the group. + +Example: `enter Friends` + +This command will enter the group named 'Friends'. + +### Exiting a group: `exit` + +Exits the current group. -**Q**: How do I transfer my data to another computer? +Format: `exit GROUP_NAME` + +Example: `exit Friends` + +This command will exit the current group. + +### Settle expenses: `settle` + +Settles the expenses between two users in the group. + +Format: `settle USER_NAME1 /user USER_NAME2` + +- `USER_NAME1` is the name of the first user. +- `USER_NAME2` is the name of the second user. +- `/user` is a keyword to indicate the start of the second user's name. + +Example: `settle Alice /user Bob` + +This command will settle the expenses between Alice and Bob, showing what Alice owes Bob. + + +## FAQ -**A**: {your answer here} +1. **How do I create a new group?** + To create a new group, use the `create group` command followed by the group name. ## Command Summary {Give a 'cheat sheet' of commands here} -* Add todo `todo n/TODO_NAME d/DEADLINE` +- `help`: Shows a message explaining how to use the application. +- `create group GROUP_NAME`: Creates a new group with the specified group name. +- `enter GROUP_NAME`: Enters an existing group with the specified group name. +- `exit`: Exits the current group. +- `settle USER_NAME1 /user USER_NAME2`: Settles the expenses between two users in the group. +- `bye`: Exits the application. \ No newline at end of file From 8c7bc2464509d8c5f9e0a7679466704d604e82e1 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 20:07:42 +0800 Subject: [PATCH 150/270] Update UG --- docs/UserGuide.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 71646ed1fd..b9f058f98c 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -45,6 +45,20 @@ Example: `enter Friends` This command will enter the group named 'Friends'. +### Add members to group: `member` + +Adds a new member to the group. + +Format: `member USER_NAME` +- `USER_NAME` is the name of the user to be added to the group. + +Example: `member Alice` + +This command will add a new member named 'Alice' to the group. + +Output: `Alice has been added to group.` + + ### Exiting a group: `exit` Exits the current group. @@ -69,11 +83,22 @@ Example: `settle Alice /user Bob` This command will settle the expenses between Alice and Bob, showing what Alice owes Bob. +### Saving the data + +Split-liang automatically saves the data in each group to `GROUP_NAME.txt` in the `data` folder after the application exits. There is no need to save manually. + +The data is loaded automatically when the application starts. + +### Saying goodbye: `bye` + +This command exits the application. ## FAQ -1. **How do I create a new group?** - To create a new group, use the `create group` command followed by the group name. +1. **Q: How do I create a new group?** + - A: To create a new group, use the `create group` command followed by the group name. +2. **Q: How do I transfer my data to another device?** + - A: You can copy the `data` folder to the new device to transfer your data. ## Command Summary @@ -82,6 +107,7 @@ This command will settle the expenses between Alice and Bob, showing what Alice - `help`: Shows a message explaining how to use the application. - `create group GROUP_NAME`: Creates a new group with the specified group name. - `enter GROUP_NAME`: Enters an existing group with the specified group name. +- `member USER_NAME`: Adds a new member to the group. - `exit`: Exits the current group. - `settle USER_NAME1 /user USER_NAME2`: Settles the expenses between two users in the group. - `bye`: Exits the application. \ No newline at end of file From 833d423b638872d793eb3d344ae1db0e3b1019f7 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 20:08:45 +0800 Subject: [PATCH 151/270] Fix checkstyle --- src/main/java/seedu/duke/Expense.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 4a08cfe844..a38f09076e 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -24,7 +24,6 @@ public class Expense { * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided -<<<<<<< HEAD * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) */ From 814c0bf1a75e524d07446da5995b6215966c91f7 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 20:37:03 +0800 Subject: [PATCH 152/270] Add diagram --- docs/AddMember.png | Bin 0 -> 24719 bytes docs/DeveloperGuide.md | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 docs/AddMember.png diff --git a/docs/AddMember.png b/docs/AddMember.png new file mode 100644 index 0000000000000000000000000000000000000000..e9fa667714a613f9063b847cd71736620c712149 GIT binary patch literal 24719 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 literal 0 HcmV?d00001 diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index d2f304abf4..df402113df 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -44,8 +44,7 @@ and allows users to add new members to an existing group. The implementation of The Group class maintains a list of members as a `private List` field called `members`. -The `addMember(String memberName)` method is responsible for adding a new member to the group. It performs the following -steps: +Users can add a new member to the group by using the command `member USER_NAME`. The `addMember(String memberName)` method is responsible for adding a new member to the group. It performs the following steps: 1. Checks if a user with the given `memberName` is already a member of the group using the `isMember(String memberName)` method. @@ -53,6 +52,8 @@ steps: 3. Adds the new `User` object to the `members` list. 4. Prints a success message indicating that the member has been added to the group. +![Sequence Diagram](AddMember.png) + ### Expenses feature #### Implementation From fdcc46cc8a5c09f71e48f14fcb88d54e59f6b9e2 Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 3 Apr 2024 21:28:41 +0800 Subject: [PATCH 153/270] Update User Guide Update Balance of user guide. --- docs/UserGuide.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 71646ed1fd..2d4f130c99 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -55,6 +55,16 @@ Example: `exit Friends` This command will exit the current group. +### Show balance of user: `balance` + +Shows list of members the user owes money to. + +Format: `balance USER_NAME` + +Example: `balance Shaoliang` + +This command will display the balance of the user named Shaoliang. + ### Settle expenses: `settle` Settles the expenses between two users in the group. From 2cef30d8e0b9be57e896012c794dbae79d71ffb8 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Wed, 3 Apr 2024 21:36:47 +0800 Subject: [PATCH 154/270] add expense feature to user guide --- docs/UserGuide.md | 29 +++++++++++++++++++ src/main/java/seedu/duke/Parser.java | 4 ++- .../seedu/duke/commands/ExpenseCommand.java | 5 ++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 71646ed1fd..a12bfbc6a4 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -69,6 +69,35 @@ Example: `settle Alice /user Bob` This command will settle the expenses between Alice and Bob, showing what Alice owes Bob. +### Create expenses: `expense` + +Create a new expense for a given group. + +#### Create expense split equally +Format:`expense DESCRIPITON /amount AMOUNT /paid PAYER_USER_NAME /user USER_NAME /user USER_NAME` + +`PAYER_USER_NAME` is the username of the person who paid for the transaction. +`USER_NAME` is the username of the payee. + +- The amount will be split equally between all members including the payer. +- The expense will be added to a list of expenses. + +#### Create expense split unequally +Format:`expense DESCRIPITON /unequal /amount TOTAL_AMOUNT +/paid PAYER_USER_NAME /user USER_NAME AMOUNT_OWED /user USER_NAME AMOUNT_OWED` + +`PAYER_USER_NAME` is the username of the person who paid for the transaction. +`USER_NAME` is the username of the payee. +`AMOUNT_OWED` is the amount owed by the + +- The amount will be split unequally between all members including the payer based on the `AMOUNT_OWED`. +- The expense will be added to a list of expenses. + + + + + + ## FAQ diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 0fc0d5a500..863670ce8c 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -15,7 +15,7 @@ public class Parser { * For example, "/amount (amount)". * Add new Keys to extract additional user parameters for future functionality. */ - private static final String[] paramKeys = {"amount", "paid", "user"}; + private static final String[] paramKeys = {"amount", "paid", "user", "group" , "expense"}; private final String userInput; @@ -50,6 +50,8 @@ public Parser(String userInput, String command, String argument, this.params.put("amount", new ArrayList<>(List.of(amount))); this.params.put("paid", new ArrayList<>(List.of(paid))); this.params.put("user", new ArrayList<>(List.of(user))); + this.params.put("group", new ArrayList<>(List.of(user))); + this.params.put("expense", new ArrayList<>(List.of(user))); } public Parser(String userInput) { diff --git a/src/main/java/seedu/duke/commands/ExpenseCommand.java b/src/main/java/seedu/duke/commands/ExpenseCommand.java index e3b5552173..44cfa866ff 100644 --- a/src/main/java/seedu/duke/commands/ExpenseCommand.java +++ b/src/main/java/seedu/duke/commands/ExpenseCommand.java @@ -47,6 +47,11 @@ public static void addExpense(HashMap > params,String currentGroup.get().addExpense(newTransaction); } + public static void deleteExpense(String argument){ + + } + + //@@author mukund1403 private static Float checkTotal(HashMap > params) throws ExpensesException { float totalAmount; From 3bba04803971853dd6ff7960fb9a7783fbaa0b0a Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 3 Apr 2024 21:57:16 +0800 Subject: [PATCH 155/270] Fix issue with end programme When user enters 'bye' current group is saved to file. --- src/main/java/seedu/duke/Parser.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 0fc0d5a500..89c66e8c6b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -129,6 +129,7 @@ public String toString() { public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": + GroupCommand.exitGroup(); throw new EndProgramException(); case "help": // Help code here From 0aba7ccb9784d22dbcef00754990d224acca45ba Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 3 Apr 2024 22:00:48 +0800 Subject: [PATCH 156/270] Fix checkstyle error Forgot to delete text from merge conflict. --- src/main/java/seedu/duke/Expense.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 4a08cfe844..80b7b5daa8 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -24,7 +24,7 @@ public class Expense { * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided -<<<<<<< HEAD + * * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) */ From f8c861e8c040c1d2b593e112f7ee0100b05ac414 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 22:07:45 +0800 Subject: [PATCH 157/270] Add GroupNameChecker --- src/main/java/seedu/duke/storage/GroupNameChecker.java | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/java/seedu/duke/storage/GroupNameChecker.java diff --git a/src/main/java/seedu/duke/storage/GroupNameChecker.java b/src/main/java/seedu/duke/storage/GroupNameChecker.java new file mode 100644 index 0000000000..1bad14682f --- /dev/null +++ b/src/main/java/seedu/duke/storage/GroupNameChecker.java @@ -0,0 +1,2 @@ +package seedu.duke.storage;public class GroupNameChecker { +} From c3ef76f5dbf7d71b9e26d37b90f51590a56b7dd8 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 22:08:34 +0800 Subject: [PATCH 158/270] Fix GroupNameChecker --- src/main/java/seedu/duke/Group.java | 10 ++- .../seedu/duke/storage/GroupNameChecker.java | 71 ++++++++++++++++++- .../java/seedu/duke/storage/GroupStorage.java | 23 ------ 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 09d44ae117..2342d453a8 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -4,6 +4,7 @@ import seedu.duke.exceptions.GroupLoadException; import seedu.duke.exceptions.GroupSaveException; +import seedu.duke.storage.GroupNameChecker; import seedu.duke.storage.GroupStorage; import seedu.duke.storage.FileIOImpl; @@ -17,6 +18,8 @@ public class Group { static Map groups = new HashMap<>(); private static Optional currentGroupName = Optional.empty(); private static final GroupStorage groupStorage = new GroupStorage(new FileIOImpl()); + private static GroupNameChecker groupNameChecker = new GroupNameChecker(); + private final String groupName; private final List members; @@ -47,7 +50,7 @@ public static Optional getOrCreateGroup(String groupName) { // Check if user is accessing a group they are already in getCurrentGroup().ifPresent(currentGroup -> { if (currentGroup.getGroupName().equals(groupName)) { - System.out.println("You are in " + groupName); + System.out.println("You are already in " + groupName); } }); @@ -64,12 +67,15 @@ public static Optional getOrCreateGroup(String groupName) { Optional group = Optional.ofNullable(groups.get(groupName)); // Create a new group if it doesn't exist - if (group.isEmpty()) { + if (group.isEmpty() && !groupNameChecker.doesGroupNameExist(groupName)) { Group newGroup = new Group(groupName); groups.put(groupName, newGroup); System.out.println(groupName + " created."); currentGroupName = Optional.of(groupName); group = Optional.of(newGroup); + } else if (groupNameChecker.doesGroupNameExist(groupName)) { + System.out.println("Group already exists. Use 'enter " + groupName + "' to enter the group."); + return Optional.empty(); } System.out.println("You are now in " + groupName); diff --git a/src/main/java/seedu/duke/storage/GroupNameChecker.java b/src/main/java/seedu/duke/storage/GroupNameChecker.java index 1bad14682f..cb6e6216bb 100644 --- a/src/main/java/seedu/duke/storage/GroupNameChecker.java +++ b/src/main/java/seedu/duke/storage/GroupNameChecker.java @@ -1,2 +1,71 @@ -package seedu.duke.storage;public class GroupNameChecker { +package seedu.duke.storage; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class GroupNameChecker { + + /** + * Checks if a specific group name exists in the saved files. + * + * @param groupNameToCheck the group name to check for + * @return true if the group name exists, false otherwise + */ + public boolean doesGroupNameExist(String groupNameToCheck) { + Path groupsDirectory = getGroupsDirectoryPath(); + if (groupsDirectory == null) { + return false; + } + return checkGroupNameInDirectory(groupsDirectory, groupNameToCheck); + } + + /** + * Gets the Path object for the groups directory. + * + * @return the Path object for the groups directory, or null if an error occurs + */ + private Path getGroupsDirectoryPath() { + try { + return Paths.get(GroupFilePath.getGroupsDirectory()); + } catch (Exception e) { + System.out.println("An error occurred while getting the groups directory: " + e.getMessage()); + return null; + } + } + + /** + * Checks if the given group name exists within the specified directory. + * + * @param directoryPath the path to the directory containing group files + * @param groupNameToCheck the group name to check for + * @return true if the group name exists, false otherwise + */ + private boolean checkGroupNameInDirectory(Path directoryPath, String groupNameToCheck) { + try (DirectoryStream stream = Files.newDirectoryStream(directoryPath, "*.txt")) { + for (Path file : stream) { + if (extractGroupNameFromFile(file).equals(groupNameToCheck)) { + return true; + } + } + } catch (IOException e) { + System.out.println("An error occurred while checking for the group name: " + e.getMessage()); + } + return false; + } + + /** + * Extracts the group name from a file path. + * + * @param file the Path object of the file + * @return the extracted group name + */ + private String extractGroupNameFromFile(Path file) { + String fileName = file.getFileName().toString(); + return fileName.substring(0, fileName.lastIndexOf('.')); + } } + + diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 3e4fc9eeaa..e9f831c8ae 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -203,27 +203,4 @@ private void loadExpenses(BufferedReader reader, Group group) throws IOException } } } - - /** - * Loads all the group names from the saved files. - * - * @return a list of group names - */ - public List loadGroupNames() { - List groupNames = new ArrayList<>(); - try { - GroupFilePath.createGroupDirectory(); - Path groupsDirectory = Paths.get(GroupFilePath.getGroupsDirectory()); - try (DirectoryStream stream = Files.newDirectoryStream(groupsDirectory, "*.txt")) { - for (Path file : stream) { - String fileName = file.getFileName().toString(); - String groupName = fileName.substring(0, fileName.lastIndexOf('.')); - groupNames.add(groupName); - } - } - } catch (IOException e) { - System.out.println("An error occurred while loading group names."); - } - return groupNames; - } } From 87d52c3dcc1c2d9e010ee9dd9a99b796c0ac566e Mon Sep 17 00:00:00 2001 From: "[avrilgk]" <[avrilguok@gmail.com]> Date: Wed, 3 Apr 2024 22:14:31 +0800 Subject: [PATCH 159/270] Refactoring of settle to remove arrowhead code and settle JUNIT testing --- src/main/java/seedu/duke/Group.java | 89 ++++++++++++++++++------ src/test/java/seedu/duke/SettleTest.java | 67 ++++++++++++++++++ 2 files changed, 135 insertions(+), 21 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 09d44ae117..2f12032406 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -51,15 +51,7 @@ public static Optional getOrCreateGroup(String groupName) { } }); - // If the user is in a different group, prevent them from creating or joining a new group. - if (isInGroup()) { - getCurrentGroup().ifPresent(currentGroup -> { - if (!currentGroup.getGroupName().equals(groupName)) { - throw new IllegalStateException("Please exit the current group '" + currentGroup.getGroupName() - + "' to create or join another group."); - } - }); - } + Optional group = Optional.ofNullable(groups.get(groupName)); @@ -73,6 +65,12 @@ public static Optional getOrCreateGroup(String groupName) { } System.out.println("You are now in " + groupName); + + assert group.isPresent() : "Group should be created and present"; + assert currentGroupName.isPresent() : "Current group name should be set"; + assert currentGroupName.get().equals(groupName) : "Current group name should match the created or retrieved group"; + assert groups.containsKey(groupName) : "Groups map should contain the new or retrieved group"; + return group; } @@ -242,6 +240,13 @@ public void settle(String payerName, String payeeName) { } } + /** + * Finds a user by their name. + * + * @param userName The name of the user to find. + * @return The user with the given name, or null if the user is not found. + */ + private User findUser(String userName) { for (User user : members) { if (user.getName().equals(userName)) { @@ -251,24 +256,66 @@ private User findUser(String userName) { return null; } + /** + * Calculates the outstanding amount between two users. + * + * @param payer The user who paid the expense. + * @param payee The user who owes money for the expense. + * @return The outstanding amount between the two users. + */ + private double calculateOutstandingAmount(User payer, User payee) { double totalAmount = 0; for (Expense expense : expenseList) { - if (expense.getPayer().equals(payer.getName())) { - for (Pair user : expense.getPayees()) { - if (user.getKey().equals(payee.getName())) { - totalAmount += user.getValue(); - } - } - } else if (expense.getPayer().equals(payee.getName())) { - for (Pair user : expense.getPayees()) { - if (user.getKey().equals(payer.getName())) { - totalAmount -= user.getValue(); - } - } + if (!isRelevantExpense(expense, payer, payee)) { + continue; + } + + // Process the relevant expense + for (Pair userExpense : expense.getPayees()) { + totalAmount += calculateAdjustedAmount(expense, payer, payee, userExpense); } } return totalAmount; } + + /** + * Checks if an expense is relevant to the payer and payee. + * + * @param expense The expense to check. + * @param payer The user who paid the expense. + * @param payee The user who owes money for the expense. + * @return true if the expense is relevant to the payer and payee, false otherwise. + */ + private boolean isRelevantExpense(Expense expense, User payer, User payee) { + String payerName = payer.getName(); + String payeeName = payee.getName(); + String expensePayer = expense.getPayer(); + + return expensePayer.equals(payerName) || expensePayer.equals(payeeName); + } + + /** + * Calculates the adjusted amount for a user in an expense. + * + * @param expense The expense to calculate the adjusted amount for. + * @param payer The user who paid the expense. + * @param payee The user who owes money for the expense. + * @param userExpense The user and the amount they owe for the expense. + * @return The adjusted amount for the user in the expense. + */ + + private double calculateAdjustedAmount(Expense expense, User payer, User payee, Pair userExpense) { + String payerName = payer.getName(); + String payeeName = payee.getName(); + String expensePayer = expense.getPayer(); + + if (userExpense.getKey().equals(payeeName) && expensePayer.equals(payerName)) { + return userExpense.getValue(); + } else if (userExpense.getKey().equals(payerName) && expensePayer.equals(payeeName)) { + return -userExpense.getValue(); + } + return 0; + } } diff --git a/src/test/java/seedu/duke/SettleTest.java b/src/test/java/seedu/duke/SettleTest.java index d964eff2d3..fe52a721c6 100644 --- a/src/test/java/seedu/duke/SettleTest.java +++ b/src/test/java/seedu/duke/SettleTest.java @@ -1,4 +1,71 @@ +//@@author avrilgk + package seedu.duke; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class SettleTest { + + private User payer; + private User payee; + private Settle settle; + + @BeforeEach + void setUp() { + // Initialize your objects before each test + payer = new User("Alice"); + payee = new User("Bob"); + settle = new Settle(payer, payee, 100.0); // Assuming the amount to settle is 100.0 + } + + @Test + void testGetPayer() { + assertEquals(payer.getName(), settle.getPayer(), "Payer's name should match the one provided at creation"); + } + + @Test + void testToString() { + String expected = "Alice paid Bob 100.0"; + assertEquals(expected, settle.toString(), "toString should return a string in the format 'payerName paid payeeName amount'"); + } + + @Test + void testNegativeAmount() { + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(payer, payee, -50.0), + "Constructor should throw IllegalArgumentException for negative amounts"); + assertTrue(exception.getMessage().contains("Amount cannot be negative"), "Exception message should indicate the negative amount problem"); + } + + @Test + void testNullPayer() { + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(null, payee, 50.0), + "Constructor should throw IllegalArgumentException for null payer"); + assertTrue(exception.getMessage().contains("Payer cannot be null"), "Exception message should indicate the null payer problem"); + } + + @Test + void testNullPayee() { + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(payer, null, 50.0), + "Constructor should throw IllegalArgumentException for null payee"); + assertTrue(exception.getMessage().contains("Payee cannot be null"), "Exception message should indicate the null payee problem"); + } + + @Test + void testNullPayerAndPayee() { + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(null, null, 50.0), + "Constructor should throw IllegalArgumentException for null payer and payee"); + assertTrue(exception.getMessage().contains("Payer cannot be null"), "Exception message should indicate the null payer problem"); + } + + @Test + void testNullPayerPayeeAndAmount() { + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(null, null, -50.0), + "Constructor should throw IllegalArgumentException for null payer, payee and negative amount"); + assertTrue(exception.getMessage().contains("Payer cannot be null"), "Exception message should indicate the null payer problem"); + } } From 109a41e3462c1627c58121bffb8e0b4d0623c891 Mon Sep 17 00:00:00 2001 From: MonkeScripts Date: Wed, 3 Apr 2024 22:19:48 +0800 Subject: [PATCH 160/270] Update UG --- docs/UserGuide.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 71646ed1fd..452b5dc207 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -69,6 +69,18 @@ Example: `settle Alice /user Bob` This command will settle the expenses between Alice and Bob, showing what Alice owes Bob. +### Trying your luck: `luck` + +Play slots to remove debts + +Format: `luck` (Coming soon feature) + +- Enters the slot machine + - `/reroll` to reroll the slots + - `/exit` to exit the slot machine + - Example: `/reroll` + +This command enable users play slots to remove their debts ## FAQ From 38793a6abc2cf5344fd216f4f07ae37f119ee27a Mon Sep 17 00:00:00 2001 From: Cohii Date: Wed, 3 Apr 2024 22:33:29 +0800 Subject: [PATCH 161/270] Fix Balance Test --- src/test/java/seedu/duke/BalanceTest.java | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index 6f1a94a0b1..a0eedd0a47 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; //@@author Cohii2 @@ -17,13 +18,22 @@ public void testConstructor() throws ExpensesException { users.add(new User("member2")); users.add(new User("member3")); + // public Expense(String payerName, String description, float totalAmount, ArrayList> payees){ List expenses = new ArrayList<>(); expenses.add(new Expense(true, "member1", "expense1", 20f, - new String[]{"member2 5", "member3 10"})); + new ArrayList<>(Arrays.asList( + new Pair<>("member2", 5.0f), + new Pair<>("member3", 10.0f) + )))); expenses.add(new Expense("member2", "expense2", 30f, - new String[]{"member1", "member3"})); + new ArrayList<>(Arrays.asList( + new Pair<>("member1", 10.0f), + new Pair<>("member3", 10.0f) + )))); expenses.add(new Expense("member3", "expense3", 100f, - new String[]{"member1"})); + new ArrayList<>(Arrays.asList( + new Pair<>("member1", 100.0f) + )))); Balance member1Balance = new Balance("member1", expenses, users); member1Balance.printBalance(); @@ -32,13 +42,13 @@ public void testConstructor() throws ExpensesException { Balance member3Balance = new Balance("member3", expenses, users); member3Balance.printBalance(); - assertEquals(-10.0f, member1Balance.getBalanceList().get("member2")); + assertEquals(-5.0f, member1Balance.getBalanceList().get("member2")); assertEquals(-90.0f, member1Balance.getBalanceList().get("member3")); - assertEquals(10.0f, member2Balance.getBalanceList().get("member1")); - assertEquals(15.0f, member2Balance.getBalanceList().get("member3")); + assertEquals(5.0f, member2Balance.getBalanceList().get("member1")); + assertEquals(10.0f, member2Balance.getBalanceList().get("member3")); assertEquals(90.0f, member3Balance.getBalanceList().get("member1")); - assertEquals(-15.0f, member3Balance.getBalanceList().get("member2")); + assertEquals(-10.0f, member3Balance.getBalanceList().get("member2")); } } From 5dab2c4160bd11f0447c890143137a79a3586332 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 22:41:59 +0800 Subject: [PATCH 162/270] Update loadGroupName --- src/main/java/seedu/duke/Group.java | 2 +- .../java/seedu/duke/storage/GroupStorage.java | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 2342d453a8..a3e8ee7319 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -25,7 +25,7 @@ public class Group { private final List members; private final List expenseList; - private Group(String groupName) { + public Group(String groupName) { this.groupName = groupName; this.members = new ArrayList<>(); this.expenseList = new ArrayList<>(); diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index e9f831c8ae..b30d94892c 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -11,10 +11,6 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -129,16 +125,23 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { BufferedReader reader = fileIO.getFileReader(filePath); Group group = loadGroupName(reader); + if (group == null) { + throw new GroupLoadException("Failed to load group name from file."); + } + loadMembers(reader, group); loadExpenses(reader, group); reader.close(); return group; } catch (IOException e) { - throw new GroupLoadException("An error occurred while loading the group information."); + throw new GroupLoadException("An error occurred while loading the group: " + e.getMessage()); + } catch (Exception e) { + throw new GroupLoadException("An unexpected error occurred while loading the group: " + e.getMessage()); } } + /** * Loads the group name from the file. * @@ -147,8 +150,11 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { * @throws IOException if an I/O error occurs while reading from the file */ private Group loadGroupName(BufferedReader reader) throws IOException { - String name = reader.readLine(); - return Group.getOrCreateGroup(name).orElse(null); + String line = reader.readLine(); + if (line != null && !line.isEmpty()) { + return new Group(line.trim()); + } + return null; } /** From 4ddb382920e7295ab21030bf9732092e71f7708c Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Wed, 3 Apr 2024 22:42:51 +0800 Subject: [PATCH 163/270] Fix Checkstyle --- src/main/java/seedu/duke/storage/GroupStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index b30d94892c..69ff942af9 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -112,6 +112,7 @@ private void saveExpenses(BufferedWriter writer, List expenses) throws writer.newLine(); } } + /** * Loads the group information from a file. * @@ -141,7 +142,6 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { } } - /** * Loads the group name from the file. * From a31a57db75bf2c444629e626a37def6945ffebd2 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Wed, 3 Apr 2024 23:13:23 +0800 Subject: [PATCH 164/270] add member check to expense class --- src/main/java/seedu/duke/Expense.java | 1 - src/main/java/seedu/duke/Group.java | 6 +++--- src/main/java/seedu/duke/Parser.java | 4 +--- .../java/seedu/duke/commands/ExpenseCommand.java | 16 ++++++++++++---- src/test/java/seedu/duke/BalanceTest.java | 1 - src/test/java/seedu/duke/ExpenseTest.java | 12 +++++++++++- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 80b7b5daa8..a38f09076e 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -24,7 +24,6 @@ public class Expense { * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided - * * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) */ diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 09d44ae117..ba11dae40c 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -17,9 +17,9 @@ public class Group { static Map groups = new HashMap<>(); private static Optional currentGroupName = Optional.empty(); private static final GroupStorage groupStorage = new GroupStorage(new FileIOImpl()); - + private static List members = null; private final String groupName; - private final List members; + private final List expenseList; private Group(String groupName) { @@ -154,7 +154,7 @@ public static boolean isInGroup() { * @param memberName The name of the member to check. * @return true if the user is a member of the group, false otherwise. */ - public boolean isMember(String memberName) { + public static boolean isMember(String memberName) { for (User member : members) { if (member.getName().equals(memberName)) { return true; diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 319e6ac6de..89c66e8c6b 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -15,7 +15,7 @@ public class Parser { * For example, "/amount (amount)". * Add new Keys to extract additional user parameters for future functionality. */ - private static final String[] paramKeys = {"amount", "paid", "user", "group" , "expense"}; + private static final String[] paramKeys = {"amount", "paid", "user"}; private final String userInput; @@ -50,8 +50,6 @@ public Parser(String userInput, String command, String argument, this.params.put("amount", new ArrayList<>(List.of(amount))); this.params.put("paid", new ArrayList<>(List.of(paid))); this.params.put("user", new ArrayList<>(List.of(user))); - this.params.put("group", new ArrayList<>(List.of(user))); - this.params.put("expense", new ArrayList<>(List.of(user))); } public Parser(String userInput) { diff --git a/src/main/java/seedu/duke/commands/ExpenseCommand.java b/src/main/java/seedu/duke/commands/ExpenseCommand.java index 44cfa866ff..4eb7120044 100644 --- a/src/main/java/seedu/duke/commands/ExpenseCommand.java +++ b/src/main/java/seedu/duke/commands/ExpenseCommand.java @@ -16,15 +16,13 @@ public static void addExpense(HashMap > params,String throws ExpensesException { Optional currentGroup = Group.getCurrentGroup(); if (currentGroup.isEmpty()) { - String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; - throw new ExpensesException(exceptionMessage); + throw new ExpensesException("Not signed in to a Group! Use 'create ' to create Group"); } String[] expenseParams = {"amount", "paid", "user"}; for (String expenseParam : expenseParams) { if (params.get(expenseParam).isEmpty()) { - String exceptionMessage = "No " + expenseParam + " for expenses! Add /" + expenseParam; - throw new ExpensesException(exceptionMessage); + throw new ExpensesException("No " + expenseParam + " for expenses! Add /" + expenseParam); } } @@ -32,7 +30,17 @@ public static void addExpense(HashMap > params,String // Obtain necessary information from 'params' and create new Expense ArrayList payeeList = params.get("user"); + + for(String payee : payeeList){ + if(!Group.isMember(payee)){ + throw new ExpensesException(payee + " is not a member of the group!"); + } + } + String payerName = params.get("paid").get(0); + if(!Group.isMember(payerName)){ + throw new ExpensesException(payerName + " is not a member of the group!"); + } if(argument.isEmpty()){ System.out.println("Warning! Empty description"); } diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java index a0eedd0a47..df4ba0906d 100644 --- a/src/test/java/seedu/duke/BalanceTest.java +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -18,7 +18,6 @@ public void testConstructor() throws ExpensesException { users.add(new User("member2")); users.add(new User("member3")); - // public Expense(String payerName, String description, float totalAmount, ArrayList> payees){ List expenses = new ArrayList<>(); expenses.add(new Expense(true, "member1", "expense1", 20f, new ArrayList<>(Arrays.asList( diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java index a6149651d3..5ff134282e 100644 --- a/src/test/java/seedu/duke/ExpenseTest.java +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -3,6 +3,9 @@ import org.junit.jupiter.api.Test; import seedu.duke.exceptions.ExpensesException; +import java.util.ArrayList; +import java.util.Arrays; + import static org.junit.jupiter.api.Assertions.assertEquals; @@ -10,7 +13,14 @@ class ExpenseTest{ @Test public void newExpenseTest() throws ExpensesException { Expense testExpense = new Expense(true,"mukund","disneyland", - 10, new String[]{"cohii 2", "shao 3.2", "avril 1", "hafiz 2"}); + 10, + new ArrayList<>(Arrays.asList( + new Pair<>("cohii", 2.0f), + new Pair<>("shao", 3.20f), + new Pair<>("avril", 1.0f), + new Pair<>("hafiz", 2.0f), + new Pair<>("mukund", 1.8f) + ))); assertEquals("description disneyland and amount 10.0 paid by mukund " + "and split between:\ncohii who owes 2.00\nshao who owes 3.20\navril who owes 1.00" + "\nhafiz who owes 2.00\nmukund who owes 1.80\n",testExpense.toString()); From 7f51707857c75d515ea871ed8aea7babdfbf0a76 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 11 Apr 2024 18:01:33 +0800 Subject: [PATCH 165/270] Add PPP --- docs/team/hafizuddin-a.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/team/hafizuddin-a.md diff --git a/docs/team/hafizuddin-a.md b/docs/team/hafizuddin-a.md new file mode 100644 index 0000000000..9b6db20969 --- /dev/null +++ b/docs/team/hafizuddin-a.md @@ -0,0 +1,29 @@ +# Hafizuddin - Project Portfolio Page + +### Project: Split-liang + +Split-liang is a CLI application that helps you split expenses with friends in a fun way! If you can type fast, Split-liang can help you manage your expenses faster than traditional GUI apps. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to add members to groups. + * What it does: allows the user to add names of members to groups so that expenses can be split among them. + +* **New Feature**: Added database storage for groups and members. + * What it does: allows the user to save the groups and members to a file so that they can be accessed later. + +* **Code contributed**: [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=hafizuddin-a&tabRepo=AY2324S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Documentation**: + * User Guide: + * Added documentation for the addition of members to groups [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + * Added documentation for the storage of groups and members [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + * Developer Guide: + * Added implementation for the addition of members to groups [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + * Added sequence diagram for the addition of members to groups [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#55](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/55) + From 8b983aa982f9b894ee091cd6597b3d35ac780778 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 11 Apr 2024 20:16:10 +0800 Subject: [PATCH 166/270] Fix checkstyle --- src/main/java/seedu/duke/Group.java | 3 ++- src/test/java/seedu/duke/SettleTest.java | 33 ++++++++++++++++-------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index d7e1297bed..9765be6fdc 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -76,7 +76,8 @@ public static Optional getOrCreateGroup(String groupName) { assert group.isPresent() : "Group should be created and present"; assert currentGroupName.isPresent() : "Current group name should be set"; - assert currentGroupName.get().equals(groupName) : "Current group name should match the created or retrieved group"; + assert currentGroupName.get().equals(groupName) : "Current group name should match " + + "the created or retrieved group"; assert groups.containsKey(groupName) : "Groups map should contain the new or retrieved group"; return group; diff --git a/src/test/java/seedu/duke/SettleTest.java b/src/test/java/seedu/duke/SettleTest.java index fe52a721c6..c6b047359a 100644 --- a/src/test/java/seedu/duke/SettleTest.java +++ b/src/test/java/seedu/duke/SettleTest.java @@ -31,41 +31,52 @@ void testGetPayer() { @Test void testToString() { String expected = "Alice paid Bob 100.0"; - assertEquals(expected, settle.toString(), "toString should return a string in the format 'payerName paid payeeName amount'"); + assertEquals(expected, settle.toString(), "toString should return a string in the format 'payerName " + + "paid payeeName amount'"); } @Test void testNegativeAmount() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(payer, payee, -50.0), + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, + () -> new Settle(payer, payee, -50.0), "Constructor should throw IllegalArgumentException for negative amounts"); - assertTrue(exception.getMessage().contains("Amount cannot be negative"), "Exception message should indicate the negative amount problem"); + assertTrue(exception.getMessage().contains("Amount cannot be negative"), + "Exception message should indicate the negative amount problem"); } @Test void testNullPayer() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(null, payee, 50.0), + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, + () -> new Settle(null, payee, 50.0), "Constructor should throw IllegalArgumentException for null payer"); - assertTrue(exception.getMessage().contains("Payer cannot be null"), "Exception message should indicate the null payer problem"); + assertTrue(exception.getMessage().contains("Payer cannot be null"), + "Exception message should indicate the null payer problem"); } @Test void testNullPayee() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(payer, null, 50.0), + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, + () -> new Settle(payer, null, 50.0), "Constructor should throw IllegalArgumentException for null payee"); - assertTrue(exception.getMessage().contains("Payee cannot be null"), "Exception message should indicate the null payee problem"); + assertTrue(exception.getMessage().contains("Payee cannot be null"), + "Exception message should indicate the null payee problem"); } @Test void testNullPayerAndPayee() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(null, null, 50.0), + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, + () -> new Settle(null, null, 50.0), "Constructor should throw IllegalArgumentException for null payer and payee"); - assertTrue(exception.getMessage().contains("Payer cannot be null"), "Exception message should indicate the null payer problem"); + assertTrue(exception.getMessage().contains("Payer cannot be null"), + "Exception message should indicate the null payer problem"); } @Test void testNullPayerPayeeAndAmount() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Settle(null, null, -50.0), + Exception exception = Assertions.assertThrows(IllegalArgumentException.class, + () -> new Settle(null, null, -50.0), "Constructor should throw IllegalArgumentException for null payer, payee and negative amount"); - assertTrue(exception.getMessage().contains("Payer cannot be null"), "Exception message should indicate the null payer problem"); + assertTrue(exception.getMessage().contains("Payer cannot be null"), + "Exception message should indicate the null payer problem"); } } From 33def81848f0ce494126f6299f255ba2b1e80519 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 11 Apr 2024 22:12:30 +0800 Subject: [PATCH 167/270] Bye command exit at start - no longer asks for name at start --- src/main/java/seedu/duke/Duke.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 8b899cc60f..a2fd4c8343 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -18,10 +18,10 @@ public static void main(String[] args) { "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); + System.out.println("Start splitting your expenses now!"); Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); + Help.printHelp(); while(in.hasNextLine()) { From da9a8f060e9a1437a338755128dbd4bb216f3fc7 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 11 Apr 2024 22:20:02 +0800 Subject: [PATCH 168/270] Update non-existent group error message - issue #109 --- src/main/java/seedu/duke/Group.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index d7e1297bed..463cf5533c 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -105,7 +105,7 @@ public static Optional enterGroup(String groupName) { } // @@author hafizuddin-a } catch (GroupLoadException e) { - System.out.println("Error loading group: " + e.getMessage()); + System.out.println("Group does not exist."); return Optional.empty(); } } From 408d411aad3dd890cce90f7bdddaa0d4cc57d620 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Thu, 11 Apr 2024 22:37:35 +0800 Subject: [PATCH 169/270] Updated PPP --- docs/UserGuide.md | 65 ++++++++++++++++++++++++++---------------- docs/team/avrilgk.md | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 docs/team/avrilgk.md diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 20cff285f0..8271448e33 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -4,30 +4,45 @@ Split-liang is an application that helps you split expenses with friends in a fun way! - ## Quick Start -{Give steps to get started quickly} - 1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `Duke` [here](http://link.to/duke). -## Features - -{Give detailed description of each feature} +## Features ### Viewing help: `help` -Shows a message explaining how to use the application. + +This command will display a message explaining how to use the application. Format: `help` -### Creating a group: `create group` +Example: `help` + +Output: + +Welcome, here is a list of commands: + +- `help`: Access help menu. +- `create `: Create a group. +- `exit `: Exit current group. +- `member `: Add a member to the group. +- `expense /amount /paid /user /user ...`: Add an expense SPLIT + EQUALLY. +- `expense /unequal /amount /paid /user /user ...`: + Add an expense SPLIT UNEQUALLY. +- `list`: List all expenses in the group. +- `balance `: Show user's balance. +- `settle /user `: Settle the amount between two users. +- `luck`: Luck is in the air tonight. + +### Creating a group: `create` Creates a new group with the specified group name. -Format: `create group GROUP_NAME` +Format: `create GROUP_NAME` -- `GROUP_NAME` is the name of the group. +`GROUP_NAME` is the name of the group. Example: `create Friends` @@ -39,7 +54,7 @@ Enters an existing group with the specified group name. Format: `enter GROUP_NAME` -- `GROUP_NAME` is the name of the group. +`GROUP_NAME` is the name of the group. Example: `enter Friends` @@ -50,7 +65,8 @@ This command will enter the group named 'Friends'. Adds a new member to the group. Format: `member USER_NAME` -- `USER_NAME` is the name of the user to be added to the group. + +`USER_NAME` is the name of the user to be added to the group. Example: `member Alice` @@ -58,7 +74,6 @@ This command will add a new member named 'Alice' to the group. Output: `Alice has been added to group.` - ### Exiting a group: `exit` Exits the current group. @@ -99,10 +114,10 @@ Play slots to remove debts Format: `luck` (Coming soon feature) -- Enters the slot machine - - `/reroll` to reroll the slots - - `/exit` to exit the slot machine - - Example: `/reroll` +- Enters the slot machine + - `/reroll` to reroll the slots + - `/exit` to exit the slot machine + - Example: `/reroll` This command enable users play slots to remove their debts @@ -111,6 +126,7 @@ This command enable users play slots to remove their debts Create a new expense for a given group. #### Create expense split equally + Format:`expense DESCRIPITON /amount AMOUNT /paid PAYER_USER_NAME /user USER_NAME /user USER_NAME` `PAYER_USER_NAME` is the username of the person who paid for the transaction. @@ -120,19 +136,21 @@ Format:`expense DESCRIPITON /amount AMOUNT /paid PAYER_USER_NAME /user USER_NAME - The expense will be added to a list of expenses. #### Create expense split unequally -Format:`expense DESCRIPITON /unequal /amount TOTAL_AMOUNT + +Format:`expense DESCRIPITON /unequal /amount TOTAL_AMOUNT /paid PAYER_USER_NAME /user USER_NAME AMOUNT_OWED /user USER_NAME AMOUNT_OWED` `PAYER_USER_NAME` is the username of the person who paid for the transaction. `USER_NAME` is the username of the payee. -`AMOUNT_OWED` is the amount owed by the +`AMOUNT_OWED` is the amount owed by the - The amount will be split unequally between all members including the payer based on the `AMOUNT_OWED`. - The expense will be added to a list of expenses. ### Saving the data -Split-liang automatically saves the data in each group to `GROUP_NAME.txt` in the `data` folder after the application exits. There is no need to save manually. +Split-liang automatically saves the data in each group to `GROUP_NAME.txt` in the `data` folder after the application +exits. There is no need to save manually. The data is loaded automatically when the application starts. @@ -140,13 +158,12 @@ The data is loaded automatically when the application starts. This command exits the application. - ## FAQ 1. **Q: How do I create a new group?** - - A: To create a new group, use the `create group` command followed by the group name. + - A: To create a new group, use the `create group` command followed by the group name. 2. **Q: How do I transfer my data to another device?** - - A: You can copy the `data` folder to the new device to transfer your data. + - A: You can copy the `data` folder to the new device to transfer your data. ## Command Summary diff --git a/docs/team/avrilgk.md b/docs/team/avrilgk.md new file mode 100644 index 0000000000..c2d8dad167 --- /dev/null +++ b/docs/team/avrilgk.md @@ -0,0 +1,67 @@ +# Avril Guok - Project Portfolio Page + +## Overview + +Split-liang is a CLI application that helps you split expenses with friends in a fun way! If you can type fast, +Split-liang can help you manage your expenses faster than traditional GUI apps + +### Summary of Contributions + +* **Code Contributions**: + [RepoSense Link] + +https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=avrilgk&tabRepo=AY2324S2-CS2113-T15-3/tp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false + +* **Enhancements implemented**: + + 1. Implemented features relating to group creation + * Group creation feature + * What it does: allows the user to create a new group with a specified group name + * Group entering feature + * What it does: allows the user to enter an existing group with a specified group name + * Group exit feature + * What it does: allows the user to exit the current group + * Group deletion feature + * What it does: allows the user to delete an existing group with a specified group name + + 2. Implemented settle function + * Settle feature + * What it does: allows the user to settle the debts within the group + * This feature can be used for both scenarios where the bill is split equally or unequally. Entering the + settle + function between Person A /user Person B will settle expenses between the two, showing what A owes B. + + +* **Contributions to UG** + * Added documentation relating to the group function (create, enter, exit, delete group) + * Added documentation relating to the settle function + + +* **Contributions to DG** + * Added the implementation for the group creation feature. + * Added the implementation for the settle feature. + + +* **Contributions to team-based tasks** + * Added a short introduction to Split-Liang in UG + * Added FAQs in UG + + +* **Review/mentoring contributions** + * PRs reviewed (with non-trivial review comments): + + https://github.com/AY2324S2-CS2113-T15-3/tp/pull/64 + + https://github.com/AY2324S2-CS2113-T15-3/tp/pull/152 + + https://github.com/AY2324S2-CS2113-T15-3/tp/pull/56 + + https://github.com/AY2324S2-CS2113-T15-3/tp/pull/6 + + +* **Contributions beyond the project team** + + https://github.com/avrilgk/ped/issues + + + From f82dcf2ce47255b59b3b0f307d02bf399f99aa29 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Thu, 11 Apr 2024 23:02:04 +0800 Subject: [PATCH 170/270] Ignore case for member - issue #126 --- src/main/java/seedu/duke/Group.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 463cf5533c..27e20640cf 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -162,7 +162,7 @@ public static boolean isInGroup() { */ public static boolean isMember(String memberName) { for (User member : members) { - if (member.getName().equals(memberName)) { + if (member.getName().equalsIgnoreCase(memberName)) { return true; } } From aa747791e9c89ed352fda9a23e1efe04b316486a Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Fri, 12 Apr 2024 01:40:55 +0800 Subject: [PATCH 171/270] Add isLoading flag - isLoading flag prevents the console for printing the statments for adding member and expenses when loading file - resolve issue #119 of printing error message from bye command when not in any group --- src/main/java/seedu/duke/Expense.java | 13 ++++++++----- src/main/java/seedu/duke/Group.java | 4 +++- src/main/java/seedu/duke/Parser.java | 4 +++- src/main/java/seedu/duke/storage/GroupStorage.java | 4 ++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index a38f09076e..192ada042f 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -3,6 +3,7 @@ import seedu.duke.exceptions.ExpensesException; +import seedu.duke.storage.GroupStorage; import java.util.ArrayList; @@ -81,12 +82,14 @@ public String toString() { } void printSuccessMessage() { - System.out.printf("Added new expense with description %s and amount %.2f paid by %s and split between:\n", - this.description, this.totalAmount, this.payerName); - for (Pair payee : payees) { - System.out.printf("%s who owes %.2f\n", payee.getKey(), payee.getValue()); + if (!GroupStorage.isLoading) { + System.out.println("Added new expense with description " + description + " and amount " + totalAmount + + " paid by " + payerName + " and split between:"); + for (Pair payee : payees) { + System.out.println(payee.getKey() + " who owes " + String.format("%.2f", payee.getValue())); + } + System.out.println(); } - System.out.println(); } public String getPayer() { diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 27e20640cf..e8c995b46d 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -188,7 +188,9 @@ public User addMember(String memberName) { User newMember = new User(memberName); members.add(newMember); - System.out.println(memberName + " has been added to " + groupName + "."); + if (!GroupStorage.isLoading) { + System.out.println(memberName + " has been added to " + groupName + "."); + } return newMember; } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 89c66e8c6b..28c5628ea6 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -129,7 +129,9 @@ public String toString() { public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": - GroupCommand.exitGroup(); + if (Group.isInGroup()) { + GroupCommand.exitGroup(); + } throw new EndProgramException(); case "help": // Help code here diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 2e46a1cf51..509e628853 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -19,6 +19,8 @@ * Handles the saving and loading of group information to and from files. */ public class GroupStorage { + public static boolean isLoading = false; + private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; private static final String EXPENSE_DELIMITER = ","; @@ -121,6 +123,7 @@ private void saveExpenses(BufferedWriter writer, List expenses) throws * @throws GroupLoadException if an error occurs while loading the group information */ public Group loadGroupFromFile(String groupName) throws GroupLoadException { + isLoading = true; try { String filePath = GroupFilePath.getFilePath(groupName); BufferedReader reader = fileIO.getFileReader(filePath); @@ -134,6 +137,7 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { loadExpenses(reader, group); reader.close(); + isLoading = false; return group; } catch (IOException e) { throw new GroupLoadException("An error occurred while loading the group: " + e.getMessage()); From e38f85eb6ec2a7e2d7a80b00e1bf6dc252846213 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Fri, 12 Apr 2024 04:33:27 +0800 Subject: [PATCH 172/270] Update isLoading in finally --- src/main/java/seedu/duke/storage/GroupStorage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 509e628853..f06de2b561 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -137,12 +137,13 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { loadExpenses(reader, group); reader.close(); - isLoading = false; return group; } catch (IOException e) { throw new GroupLoadException("An error occurred while loading the group: " + e.getMessage()); } catch (Exception e) { throw new GroupLoadException("An unexpected error occurred while loading the group: " + e.getMessage()); + } finally { + isLoading = false; } } From 34529e3ca080042363050be38ff0db168c95c78a Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Fri, 12 Apr 2024 04:55:07 +0800 Subject: [PATCH 173/270] Fix double message group bug - moved check for whether user is already in group before creating new group to prevent duplicate messages - modified check to compare current group with group being accessed to avoid false positives - if user is already in group, return empty Optional to prevent further execution - issue #123 --- src/main/java/seedu/duke/Group.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index e8c995b46d..0019d83451 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -41,6 +41,7 @@ public Group(String groupName) { * @return The existing or newly created group. * @throws IllegalStateException If trying to create or join a new group while already in another group. */ + //@@ author avrilgk public static Optional getOrCreateGroup(String groupName) { // Check if group name is empty @@ -49,17 +50,14 @@ public static Optional getOrCreateGroup(String groupName) { return Optional.empty(); } - // Check if user is accessing a group they are already in - getCurrentGroup().ifPresent(currentGroup -> { - if (currentGroup.getGroupName().equals(groupName)) { - System.out.println("You are already in " + groupName); - } - }); - - - Optional group = Optional.ofNullable(groups.get(groupName)); + // Check if user is accessing a group they are already in + if (group.isPresent() && getCurrentGroup().isPresent() && getCurrentGroup().get().equals(group.get())) { + System.out.println("You are already in " + groupName); + return Optional.empty(); + } + // Create a new group if it doesn't exist if (group.isEmpty() && !groupNameChecker.doesGroupNameExist(groupName)) { Group newGroup = new Group(groupName); @@ -67,13 +65,12 @@ public static Optional getOrCreateGroup(String groupName) { System.out.println(groupName + " created."); currentGroupName = Optional.of(groupName); group = Optional.of(newGroup); + System.out.println("You are now in " + groupName); } else if (groupNameChecker.doesGroupNameExist(groupName)) { System.out.println("Group already exists. Use 'enter " + groupName + "' to enter the group."); return Optional.empty(); } - System.out.println("You are now in " + groupName); - assert group.isPresent() : "Group should be created and present"; assert currentGroupName.isPresent() : "Current group name should be set"; assert currentGroupName.get().equals(groupName) : "Current group name should match the created or retrieved group"; From 9d6a8f9d3ccc17fc9c6faa9c0170ed6685e12264 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sat, 13 Apr 2024 12:42:16 +0800 Subject: [PATCH 174/270] fixed exit and enter group bugs --- src/main/java/seedu/duke/Group.java | 13 +++++++++++-- src/main/java/seedu/duke/GroupCommand.java | 4 ++-- src/main/java/seedu/duke/Parser.java | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index a7dcd5a3c8..6491bd2f02 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -87,6 +87,11 @@ public static Optional getOrCreateGroup(String groupName) { * @return The existing group. */ public static Optional enterGroup(String groupName) { + if (currentGroupName.isPresent()) { + System.out.println("You are currently in " + currentGroupName.get() + ". Exit current group before entering another one."); + return Optional.empty(); + } + Optional group = Optional.ofNullable(groups.get(groupName)); if (group.isEmpty()) { //@@author hafizuddin-a @@ -117,8 +122,12 @@ public static Optional enterGroup(String groupName) { * Exits the current group. * If the user is not in any group, it displays a message asking the user to try again. */ - public static void exitGroup() { + public static void exitGroup(String groupName) { if (currentGroupName.isPresent()) { + if (!currentGroupName.get().equals(groupName)) { + System.out.println("You are not currently in group " + groupName + ". Please enter the correct group name."); + return; + } //@@author hafizuddin-a try { groupStorage.saveGroupToFile(groups.get(currentGroupName.get())); @@ -130,7 +139,7 @@ public static void exitGroup() { System.out.println("You have exited " + currentGroupName.get() + "."); currentGroupName = Optional.empty(); } else { - System.out.println("You are not currently in any group. Please try again."); + System.out.println("You are not currently in a group."); } } diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index f1d548e141..3552e9e8af 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -71,8 +71,8 @@ public static void enterGroup(String groupName) { * Exits the current group. * If the user is not currently in a group, prints a message indicating so. */ - public static void exitGroup() { - Group.exitGroup(); + public static void exitGroup(String groupName) { + Group.exitGroup(groupName); } } diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 28c5628ea6..81aa77fc7e 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -130,7 +130,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": if (Group.isInGroup()) { - GroupCommand.exitGroup(); + GroupCommand.exitGroup(argument); } throw new EndProgramException(); case "help": @@ -151,7 +151,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { GroupCommand.enterGroup(argument); break; case "exit": - GroupCommand.exitGroup(); + GroupCommand.exitGroup(argument); break; case "expense": ExpenseCommand.addExpense(params, argument, userInput); From 37a56eb0bc69febcc042912136daad7cad45ee8d Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 13 Apr 2024 19:07:28 +0800 Subject: [PATCH 175/270] Exceptions for tampered file --- src/main/java/seedu/duke/Group.java | 21 +++++++++++++++---- .../java/seedu/duke/storage/GroupStorage.java | 12 +++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 6491bd2f02..8e9a194f4c 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -8,6 +8,7 @@ import seedu.duke.storage.GroupStorage; import seedu.duke.storage.FileIOImpl; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -88,13 +89,20 @@ public static Optional getOrCreateGroup(String groupName) { */ public static Optional enterGroup(String groupName) { if (currentGroupName.isPresent()) { - System.out.println("You are currently in " + currentGroupName.get() + ". Exit current group before entering another one."); + System.out.println("You are currently in " + currentGroupName.get() + + ". Exit current group before entering another one."); return Optional.empty(); } Optional group = Optional.ofNullable(groups.get(groupName)); if (group.isEmpty()) { //@@author hafizuddin-a + GroupNameChecker groupNameChecker = new GroupNameChecker(); + if (!groupNameChecker.doesGroupNameExist(groupName)) { + System.out.println("Group does not exist."); + return Optional.empty(); + } + try { // If the group doesn't exist in memory, try loading it from file Optional loadedGroup = Optional.ofNullable(groupStorage.loadGroupFromFile(groupName)); @@ -103,12 +111,16 @@ public static Optional enterGroup(String groupName) { group = loadedGroup; } else { //@@ author avrilgk - System.out.println("Group does not exist."); + System.out.println("Unable to load group from file."); return Optional.empty(); } // @@author hafizuddin-a } catch (GroupLoadException e) { - System.out.println("Group does not exist."); + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "Failed to load group from file."; + } + System.out.println(errorMessage); return Optional.empty(); } } @@ -125,7 +137,8 @@ public static Optional enterGroup(String groupName) { public static void exitGroup(String groupName) { if (currentGroupName.isPresent()) { if (!currentGroupName.get().equals(groupName)) { - System.out.println("You are not currently in group " + groupName + ". Please enter the correct group name."); + System.out.println("You are not currently in group " + groupName + + ". Please enter the correct group name."); return; } //@@author hafizuddin-a diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index f06de2b561..22285c65e7 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -23,7 +23,7 @@ public class GroupStorage { private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; - private static final String EXPENSE_DELIMITER = ","; + private static final String EXPENSE_DELIMITER = "◇"; private static final String PAYEE_DELIMITER = ":"; private static final String PAYEE_DATA_DELIMITER = ","; @@ -130,11 +130,15 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { Group group = loadGroupName(reader); if (group == null) { - throw new GroupLoadException("Failed to load group name from file."); + throw new GroupLoadException("Invalid group data file. Unable to load group name."); } - loadMembers(reader, group); - loadExpenses(reader, group); + try { + loadMembers(reader, group); + loadExpenses(reader, group); + } catch (IOException e) { + throw new GroupLoadException("Error loading group members or expenses: " + e.getMessage()); + } reader.close(); return group; From f00576ed1749dfc0b33289da1cb2807626838f78 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 13 Apr 2024 19:24:06 +0800 Subject: [PATCH 176/270] Remove unused import --- src/main/java/seedu/duke/Group.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index 8e9a194f4c..b0444f035f 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -8,7 +8,6 @@ import seedu.duke.storage.GroupStorage; import seedu.duke.storage.FileIOImpl; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; From 7b70702670f0949e972b9f639e2d34acf9295971 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 13 Apr 2024 19:47:34 +0800 Subject: [PATCH 177/270] More exceptions --- .../java/seedu/duke/storage/GroupStorage.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 22285c65e7..f5473cbcc8 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -136,7 +136,7 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { try { loadMembers(reader, group); loadExpenses(reader, group); - } catch (IOException e) { + } catch (IOException | GroupLoadException e) { throw new GroupLoadException("Error loading group members or expenses: " + e.getMessage()); } @@ -173,11 +173,15 @@ private Group loadGroupName(BufferedReader reader) throws IOException { * @param group the group to add the loaded members to * @throws IOException if an I/O error occurs while reading from the file */ - private void loadMembers(BufferedReader reader, Group group) throws IOException { + private void loadMembers(BufferedReader reader, Group group) throws IOException, GroupLoadException { + String line = reader.readLine(); + if (line == null || !line.equals(MEMBERS_HEADER)) { + throw new GroupLoadException("Invalid group data file. Missing or invalid 'Members:' header."); + } + // Skip the "Members:" header reader.readLine(); - String line; while ((line = reader.readLine()) != null && !line.equals(EXPENSES_HEADER)) { group.addMember(line); } @@ -190,8 +194,11 @@ private void loadMembers(BufferedReader reader, Group group) throws IOException * @param group the group to add the loaded expenses to * @throws IOException if an I/O error occurs while reading from the file */ - private void loadExpenses(BufferedReader reader, Group group) throws IOException { - String line; + private void loadExpenses(BufferedReader reader, Group group) throws IOException, GroupLoadException { + String line = reader.readLine(); + if (line == null || !line.equals(EXPENSES_HEADER)) { + throw new GroupLoadException("Invalid group data file. Missing or invalid 'Expenses:' header."); + } while ((line = reader.readLine()) != null) { String[] expenseData = line.split(EXPENSE_DELIMITER, 4); float totalAmount = Float.parseFloat(expenseData[0]); From 345f366340db4374d50d1fc5a7e9c4a6c5b55d41 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 13 Apr 2024 22:46:19 +0800 Subject: [PATCH 178/270] Fix Loading --- src/main/java/seedu/duke/Parser.java | 1 + .../java/seedu/duke/storage/GroupStorage.java | 28 ++++++++----------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 81aa77fc7e..54dfb9b07e 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -130,6 +130,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { switch (command) { case "bye": if (Group.isInGroup()) { + argument = Group.getCurrentGroup().get().getGroupName(); GroupCommand.exitGroup(argument); } throw new EndProgramException(); diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index f5473cbcc8..0af7903297 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -136,7 +136,7 @@ public Group loadGroupFromFile(String groupName) throws GroupLoadException { try { loadMembers(reader, group); loadExpenses(reader, group); - } catch (IOException | GroupLoadException e) { + } catch (IOException e) { throw new GroupLoadException("Error loading group members or expenses: " + e.getMessage()); } @@ -173,15 +173,13 @@ private Group loadGroupName(BufferedReader reader) throws IOException { * @param group the group to add the loaded members to * @throws IOException if an I/O error occurs while reading from the file */ - private void loadMembers(BufferedReader reader, Group group) throws IOException, GroupLoadException { - String line = reader.readLine(); - if (line == null || !line.equals(MEMBERS_HEADER)) { - throw new GroupLoadException("Invalid group data file. Missing or invalid 'Members:' header."); + private void loadMembers(BufferedReader reader, Group group) throws IOException { + String header = reader.readLine(); + if (header == null || !header.equals(MEMBERS_HEADER)) { + throw new IOException("Invalid group data file. Missing or invalid 'Members:' header."); } - // Skip the "Members:" header - reader.readLine(); - + String line; while ((line = reader.readLine()) != null && !line.equals(EXPENSES_HEADER)) { group.addMember(line); } @@ -194,11 +192,8 @@ private void loadMembers(BufferedReader reader, Group group) throws IOException, * @param group the group to add the loaded expenses to * @throws IOException if an I/O error occurs while reading from the file */ - private void loadExpenses(BufferedReader reader, Group group) throws IOException, GroupLoadException { - String line = reader.readLine(); - if (line == null || !line.equals(EXPENSES_HEADER)) { - throw new GroupLoadException("Invalid group data file. Missing or invalid 'Expenses:' header."); - } + private void loadExpenses(BufferedReader reader, Group group) throws IOException { + String line; while ((line = reader.readLine()) != null) { String[] expenseData = line.split(EXPENSE_DELIMITER, 4); float totalAmount = Float.parseFloat(expenseData[0]); @@ -206,17 +201,16 @@ private void loadExpenses(BufferedReader reader, Group group) throws IOException String description = expenseData[2]; String[] payeeData = expenseData[3].split(PAYEE_DATA_DELIMITER); - ArrayList> payeeList = new ArrayList<>(); + ArrayList> payeeList = new ArrayList<>(); for (String payee : payeeData) { String[] payeeInfo = payee.split(PAYEE_DELIMITER); String payeeName = payeeInfo[0]; float amountDue = Float.parseFloat(payeeInfo[1]); - payeeList.add(new Pair<>(payeeName,amountDue)); + payeeList.add(new Pair<>(payeeName, amountDue)); } try { - Expense expense = new Expense(false, payerName, description, totalAmount, - payeeList); + Expense expense = new Expense(false, payerName, description, totalAmount, payeeList); group.addExpense(expense); } catch (ExpensesException e) { System.out.println("Error loading expense: " + e.getMessage()); From a4ee46e84fbf287beea0945eaf90da76d6f143b8 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 13 Apr 2024 23:45:32 +0800 Subject: [PATCH 179/270] Add content page and summary table --- docs/UserGuide.md | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 8271448e33..6336e04141 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,12 +2,32 @@ ## Introduction -Split-liang is an application that helps you split expenses with friends in a fun way! +Split-liang is an application that helps you split expenses with friends in a fun way! If you are tired of keeping track and calculating who owes who, Split-liang is here to help you. With Split-liang, you can create groups, add members, add expenses, and settle debts between members. Split-liang will automatically calculate the amount each member owes or is owed. You can also play slots to remove your debts! + +## Content Page +1. [Quick Start](#quick-start) +2. [Features](#features) + - [Viewing help: `help`](#viewing-help-help) + - [Creating a group: `create`](#creating-a-group-create) + - [Entering a group: `enter`](#entering-a-group-enter) + - [Add members to group: `member`](#add-members-to-group-member) + - [Exiting a group: `exit`](#exiting-a-group-exit) + - [Show balance of user: `balance`](#show-balance-of-user-balance) + - [Settle expenses: `settle`](#settle-expenses-settle) + - [Trying your luck: `luck`](#trying-your-luck-luck) + - [Create expenses: `expense`](#create-expenses-expense) + - [Saving the data](#saving-the-data) + - [Saying goodbye: `bye`](#saying-goodbye-bye) + +-------------------------------------------------------------------------------------------------------------------- + ## Quick Start -1. Ensure that you have Java 11 or above installed. -2. Download the latest version of `Duke` [here](http://link.to/duke). +1. Ensure that you have Java 11 or above installed in your computer. +2. Download the latest version of `Split-liang` from [here](https://github.com/AY2324S2-CS2113-T15-3/tp/releases). +3. Copy the file to the folder you want to use as the home folder for your Split-liang. +4. Open a command terminal, `cd` to the folder where the jar file is located. Run the command `java -jar Split-liang.jar`. The application should start and display the welcome message. ## Features @@ -36,6 +56,7 @@ Welcome, here is a list of commands: - `settle /user `: Settle the amount between two users. - `luck`: Luck is in the air tonight. + ### Creating a group: `create` Creates a new group with the specified group name. @@ -167,12 +188,14 @@ This command exits the application. ## Command Summary -{Give a 'cheat sheet' of commands here} +Action | Format, Examples +--------|------------------ +Help | `help` +Create group | `create GROUP_NAME`
e.g. `create Friends` +Enter group | `enter GROUP_NAME`
e.g. `enter Friends` +Add member | `member USER_NAME`
e.g. `member Alice` +Exit group | `exit GROUP_NAME`
e.g. `exit Friends` +Settle expenses | `settle USER_NAME1 /user USER_NAME2`
e.g. `settle Alice /user Bob` +Exit application | `bye` + -- `help`: Shows a message explaining how to use the application. -- `create group GROUP_NAME`: Creates a new group with the specified group name. -- `enter GROUP_NAME`: Enters an existing group with the specified group name. -- `member USER_NAME`: Adds a new member to the group. -- `exit`: Exits the current group. -- `settle USER_NAME1 /user USER_NAME2`: Settles the expenses between two users in the group. -- `bye`: Exits the application. \ No newline at end of file From 683afd615fa6d681d2ab86c747742301f8314b83 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sat, 13 Apr 2024 23:52:27 +0800 Subject: [PATCH 180/270] Add more format for member --- docs/UserGuide.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 6336e04141..3234547993 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -87,7 +87,11 @@ Adds a new member to the group. Format: `member USER_NAME` -`USER_NAME` is the name of the user to be added to the group. +- `USER_NAME` is the name of the user to be added to the group. +- `USER_NAME` must be unique. It cannot be the same as an existing member's name. +- `USER_NAME` can contain whitespaces but cannot be empty. +- `USER_NAME` is not case-sensitive. +- `USER_NAME` can contain special characters. Example: `member Alice` From 3e3b4bc7fc567c4f60feaf83c6f56e025791ba77 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 00:11:52 +0800 Subject: [PATCH 181/270] Add delete file --- src/main/java/seedu/duke/GroupCommand.java | 20 +++++++++++++++++++ .../duke/exceptions/GroupDeleteException.java | 7 +++++++ src/main/java/seedu/duke/storage/FileIO.java | 9 +++++++++ .../java/seedu/duke/storage/FileIOImpl.java | 19 +++++++++++++----- .../java/seedu/duke/storage/GroupStorage.java | 19 ++++++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/main/java/seedu/duke/exceptions/GroupDeleteException.java diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index 3552e9e8af..eb7ab4b100 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -2,6 +2,11 @@ package seedu.duke; +import seedu.duke.exceptions.GroupDeleteException; +import seedu.duke.storage.FileIO; +import seedu.duke.storage.FileIOImpl; +import seedu.duke.storage.GroupStorage; + import java.util.Optional; /** @@ -33,6 +38,7 @@ public static void createGroup(String groupName) { public static void deleteGroup(String groupName) { if (Group.groups.containsKey(groupName)) { Group.groups.remove(groupName); + deleteGroupFile(groupName); System.out.println("The group " + groupName + " has been deleted."); } else { System.out.println("The group " + groupName + " does not exist."); @@ -40,6 +46,20 @@ public static void deleteGroup(String groupName) { } //@@author hafizuddin-a + /** + * Deletes the group file for the specified group name. + * + * @param groupName the name of the group whose file needs to be deleted + */ + private static void deleteGroupFile(String groupName) { + try { + FileIO fileIO = new FileIOImpl(); + GroupStorage groupStorage = new GroupStorage(fileIO); + groupStorage.deleteGroupFile(groupName); + } catch (GroupDeleteException e) { + System.out.println("Failed to delete the group file: " + e.getMessage()); + } + } /** * Adds a member with the specified name to the current group. diff --git a/src/main/java/seedu/duke/exceptions/GroupDeleteException.java b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java new file mode 100644 index 0000000000..d23b9c87ca --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java @@ -0,0 +1,7 @@ +package seedu.duke.exceptions; + +public class GroupDeleteException extends Exception { + public GroupDeleteException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/storage/FileIO.java b/src/main/java/seedu/duke/storage/FileIO.java index e86f5d7a88..53770ddfe9 100644 --- a/src/main/java/seedu/duke/storage/FileIO.java +++ b/src/main/java/seedu/duke/storage/FileIO.java @@ -26,4 +26,13 @@ public interface FileIO { * @throws IOException If an I/O error occurs while creating the writer. */ BufferedWriter getFileWriter(String filePath) throws IOException; + + /** + * Deletes the file at the specified file path. + * + * @param filePath The path of the file to be deleted. + * @return true if the file was successfully deleted, false otherwise. + * @throws IOException If an I/O error occurs while deleting the file. + */ + boolean deleteFile(String filePath) throws IOException; } diff --git a/src/main/java/seedu/duke/storage/FileIOImpl.java b/src/main/java/seedu/duke/storage/FileIOImpl.java index cacdab685f..0c007bd40e 100644 --- a/src/main/java/seedu/duke/storage/FileIOImpl.java +++ b/src/main/java/seedu/duke/storage/FileIOImpl.java @@ -1,10 +1,6 @@ package seedu.duke.storage; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; +import java.io.*; /** * Implements the FileIO interface. @@ -34,4 +30,17 @@ public BufferedReader getFileReader(String filePath) throws IOException { public BufferedWriter getFileWriter(String filePath) throws IOException { return new BufferedWriter(new FileWriter(filePath)); } + + /** + * Deletes the file at the specified file path. + * + * @param filePath The path of the file to be deleted. + * @return true if the file was successfully deleted, false otherwise. + * @throws IOException If an I/O error occurs while deleting the file. + */ + @Override + public boolean deleteFile(String filePath) throws IOException { + File file = new File(filePath); + return file.delete(); + } } diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index 0af7903297..de574298ef 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -5,6 +5,7 @@ import seedu.duke.exceptions.ExpensesException; import seedu.duke.Group; import seedu.duke.User; +import seedu.duke.exceptions.GroupDeleteException; import seedu.duke.exceptions.GroupLoadException; import seedu.duke.exceptions.GroupSaveException; @@ -217,4 +218,22 @@ private void loadExpenses(BufferedReader reader, Group group) throws IOException } } } + + /** + * Deletes the group file for the specified group name. + * + * @param groupName the name of the group whose file needs to be deleted + * @throws GroupDeleteException if an error occurs while deleting the group file + */ + public void deleteGroupFile(String groupName) throws GroupDeleteException { + try { + String filePath = GroupFilePath.getFilePath(groupName); + boolean deleted = fileIO.deleteFile(filePath); + if (!deleted) { + throw new GroupDeleteException("Failed to delete the group file."); + } + } catch (IOException e) { + throw new GroupDeleteException("An error occurred while deleting the group file: " + e.getMessage()); + } + } } From 13c4d82982848e19472eabeccc9f3d96069872b0 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 00:32:02 +0800 Subject: [PATCH 182/270] Update delete command - allow user to delete saved groups before loading them --- src/main/java/seedu/duke/GroupCommand.java | 30 ++++++++-------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/main/java/seedu/duke/GroupCommand.java b/src/main/java/seedu/duke/GroupCommand.java index eb7ab4b100..eb508b5ed4 100644 --- a/src/main/java/seedu/duke/GroupCommand.java +++ b/src/main/java/seedu/duke/GroupCommand.java @@ -5,6 +5,7 @@ import seedu.duke.exceptions.GroupDeleteException; import seedu.duke.storage.FileIO; import seedu.duke.storage.FileIOImpl; +import seedu.duke.storage.GroupNameChecker; import seedu.duke.storage.GroupStorage; import java.util.Optional; @@ -36,31 +37,22 @@ public static void createGroup(String groupName) { * @param groupName the name of the group to delete */ public static void deleteGroup(String groupName) { - if (Group.groups.containsKey(groupName)) { - Group.groups.remove(groupName); - deleteGroupFile(groupName); - System.out.println("The group " + groupName + " has been deleted."); - } else { - System.out.println("The group " + groupName + " does not exist."); - } - } - - //@@author hafizuddin-a - /** - * Deletes the group file for the specified group name. - * - * @param groupName the name of the group whose file needs to be deleted - */ - private static void deleteGroupFile(String groupName) { try { FileIO fileIO = new FileIOImpl(); GroupStorage groupStorage = new GroupStorage(fileIO); - groupStorage.deleteGroupFile(groupName); + GroupNameChecker groupNameChecker = new GroupNameChecker(); + if (groupNameChecker.doesGroupNameExist(groupName)) { + groupStorage.deleteGroupFile(groupName); + Group.groups.remove(groupName); + System.out.println("The group " + groupName + " has been deleted."); + } else { + System.out.println("The group " + groupName + " does not exist."); + } } catch (GroupDeleteException e) { - System.out.println("Failed to delete the group file: " + e.getMessage()); + System.out.println("Failed to delete the group: " + e.getMessage()); } } - + //@@author hafizuddin-a /** * Adds a member with the specified name to the current group. * If the user is not currently in a group, prints a message asking them to create or join a group first. From 8b58e9565988f6e5b0370fd04db2be9dea303f33 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 00:34:21 +0800 Subject: [PATCH 183/270] Add Javadoc for delete exception --- .../java/seedu/duke/exceptions/GroupDeleteException.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/seedu/duke/exceptions/GroupDeleteException.java b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java index d23b9c87ca..8f9e44e133 100644 --- a/src/main/java/seedu/duke/exceptions/GroupDeleteException.java +++ b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java @@ -1,6 +1,15 @@ package seedu.duke.exceptions; +/** + * Represents an exception that occurs during the deletion of a group. + * This exception is thrown when there is an error or failure in the group deletion process. + */ public class GroupDeleteException extends Exception { + /** + * Constructs a new GroupDeleteException with the specified detail message. + * + * @param message the detail message describing the exception + */ public GroupDeleteException(String message) { super(message); } From fcac4c89e8989e45bdde617e5e63ce4ca9c6b1ff Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 00:46:39 +0800 Subject: [PATCH 184/270] Use Universalexceptions --- src/main/java/seedu/duke/exceptions/GroupDeleteException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/exceptions/GroupDeleteException.java b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java index 8f9e44e133..89f7c224b3 100644 --- a/src/main/java/seedu/duke/exceptions/GroupDeleteException.java +++ b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java @@ -4,7 +4,7 @@ * Represents an exception that occurs during the deletion of a group. * This exception is thrown when there is an error or failure in the group deletion process. */ -public class GroupDeleteException extends Exception { +public class GroupDeleteException extends UniversalExceptions { /** * Constructs a new GroupDeleteException with the specified detail message. * From 12fd5ac168325a3813908e56c3cb1deef80442e0 Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 00:47:42 +0800 Subject: [PATCH 185/270] Fix checkstyle --- src/main/java/seedu/duke/storage/FileIOImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/storage/FileIOImpl.java b/src/main/java/seedu/duke/storage/FileIOImpl.java index 0c007bd40e..388a74da91 100644 --- a/src/main/java/seedu/duke/storage/FileIOImpl.java +++ b/src/main/java/seedu/duke/storage/FileIOImpl.java @@ -1,6 +1,11 @@ package seedu.duke.storage; -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; /** * Implements the FileIO interface. From 971ab09a160008a05e411488a7130d2af490929a Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sun, 14 Apr 2024 00:58:54 +0800 Subject: [PATCH 186/270] add delete expense feature and fix all issues from pe-d --- src/main/java/seedu/duke/Expense.java | 10 +-- src/main/java/seedu/duke/Group.java | 4 + src/main/java/seedu/duke/Parser.java | 19 +++- .../seedu/duke/commands/ExpenseCommand.java | 86 ++++++++++++++----- .../java/seedu/duke/storage/GroupStorage.java | 2 +- 5 files changed, 92 insertions(+), 29 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 192ada042f..66e2795ddb 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -73,18 +73,18 @@ public String getDescription() { @Override public String toString() { String expensesDetails = ""; - expensesDetails += "description " + description + " and amount " + totalAmount + - " paid by " + payerName + " and split between:\n"; + expensesDetails += "description: " + description + "\n\tamount: " + String.format("%.2f",totalAmount) + + "\n\tpaid by: " + payerName + "\n\tThe split is:\n"; for (Pair payee : payees) { - expensesDetails += payee.getKey() + " who owes " + String.format("%.2f", payee.getValue()) + "\n"; + expensesDetails += "\t\t" + payee.getKey() + " : " + String.format("%.2f", payee.getValue()) + "\n"; } return expensesDetails; } void printSuccessMessage() { if (!GroupStorage.isLoading) { - System.out.println("Added new expense with description " + description + " and amount " + totalAmount - + " paid by " + payerName + " and split between:"); + System.out.println("Added new expense with description " + description + " and amount " + + String.format("%.2f",totalAmount) + " paid by " + payerName + " and split between:"); for (Pair payee : payees) { System.out.println(payee.getKey() + " who owes " + String.format("%.2f", payee.getValue())); } diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index a7dcd5a3c8..7128e12d76 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -201,6 +201,10 @@ public void addExpense(Expense expense) { expenseList.add(expense); } + public void deleteExpense(int expenseIndex){ + expenseList.remove(expenseIndex); + } + /** * Retrieves the name of the group. * diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 28c5628ea6..d1c4375e0d 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -142,7 +142,24 @@ public void handleUserInput() throws EndProgramException, ExpensesException { GroupCommand.createGroup(argument); break; case "delete": - GroupCommand.deleteGroup(argument); + String[] deleteTypeAndValue = argument.split(" "); + String deleteType = deleteTypeAndValue[0]; + String deleteValue; + try{ + deleteValue = deleteTypeAndValue[1]; + } catch (ArrayIndexOutOfBoundsException e){ + throw new ExpensesException("Mention if you want to delete an expense or group" + + " and which group or expense you want to delete."); + } + + if(deleteType.equals("group")){ + GroupCommand.deleteGroup(deleteValue); + } else if(deleteType.equals("expense")){ + ExpenseCommand.deleteExpense(deleteValue); + } else { + throw new ExpensesException("Mention if you want to delete an expense or group."); + } + break; case "member": GroupCommand.addMember(argument); diff --git a/src/main/java/seedu/duke/commands/ExpenseCommand.java b/src/main/java/seedu/duke/commands/ExpenseCommand.java index 4eb7120044..b29820054d 100644 --- a/src/main/java/seedu/duke/commands/ExpenseCommand.java +++ b/src/main/java/seedu/duke/commands/ExpenseCommand.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Optional; public class ExpenseCommand { @@ -26,25 +27,13 @@ public static void addExpense(HashMap > params,String } } - float totalAmount = checkTotal(params); - - // Obtain necessary information from 'params' and create new Expense + float totalAmount = getTotal(params); ArrayList payeeList = params.get("user"); - - for(String payee : payeeList){ - if(!Group.isMember(payee)){ - throw new ExpensesException(payee + " is not a member of the group!"); - } - } - String payerName = params.get("paid").get(0); - if(!Group.isMember(payerName)){ - throw new ExpensesException(payerName + " is not a member of the group!"); - } - if(argument.isEmpty()){ - System.out.println("Warning! Empty description"); - } + checkDescription(argument); + + //@@author mukund1403 Expense newTransaction; ArrayList> payees = new ArrayList<>(); if(userInput.contains("/unequal")){ @@ -55,22 +44,65 @@ public static void addExpense(HashMap > params,String currentGroup.get().addExpense(newTransaction); } - public static void deleteExpense(String argument){ - + //@@author mukund1403 + public static void deleteExpense(String argument) throws ExpensesException { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + List expenseList = currentGroup.get().getExpenseList(); + int listSize = expenseList.size(); + int index = getListIndex(argument, listSize) - 1; + String deletedExpenseDescription = expenseList.get(index).toString(); + currentGroup.get().deleteExpense(index); + System.out.println("Deleted expense:\n" + deletedExpenseDescription); } + private static void checkDescription(String argument) throws ExpensesException { + if(argument.isEmpty()){ + System.out.println("Warning! Empty description"); + } else if(argument.contains("◇")){ + throw new ExpensesException("Special characters not allowed in description! " + + "(Good try trynna catch a bug!)"); + } + } - //@@author mukund1403 - private static Float checkTotal(HashMap > params) throws ExpensesException { + private static Float getTotal(HashMap > params) throws ExpensesException { float totalAmount; try { totalAmount = Float.parseFloat(params.get("amount").get(0)); } catch (NumberFormatException e) { - String exceptionMessage = "Re-enter expense with amount as a proper number."; + String exceptionMessage = "Re-enter expense with amount as a proper number. (Good bug to start with tbh!)"; + throw new ExpensesException(exceptionMessage); + } + if(totalAmount <= 0){ + String exceptionMessage = "Expense amount cannot be 0 or a negative number " + + "(Can try using special characters. I have not handled that!)"; throw new ExpensesException(exceptionMessage); } return totalAmount; } + + private static int getListIndex(String listIndex, int listSize) throws ExpensesException { + int index; + try{ + index = Integer.parseInt(listIndex); + } catch(NumberFormatException e){ + String exceptionMessage = "Enter a list index that is an Integer"; + throw new ExpensesException(exceptionMessage); + } + + if(index > listSize){ + String exceptionMessage = "List index is greater than list size"; + throw new ExpensesException(exceptionMessage); + } else if (index <= 0){ + String exceptionMessage = "List index cannot be negative"; + throw new ExpensesException(exceptionMessage); + } + return index; + } + private static Expense addUnequalExpense(ArrayList payeeList,ArrayList> payees, float totalAmount,String payerName,String argument) throws ExpensesException{ float amountDueByPayees = 0; @@ -84,6 +116,7 @@ private static Expense addUnequalExpense(ArrayList payeeList,ArrayList

payeeList,ArrayList

payeeList, ArrayList> payees, - float totalAmount, String payerName, String argument){ + float totalAmount, String payerName, String argument) throws ExpensesException { Float amountDue = totalAmount / (payeeList.size() + 1); for (String payee : payeeList) { + checkPayeeInGroup(payee); payees.add(new Pair<>(payee, amountDue)); } + checkPayeeInGroup(payerName); payees.add(new Pair<>(payerName, amountDue)); return new Expense(payerName, argument, totalAmount, payees); } + private static void checkPayeeInGroup(String payee) + throws ExpensesException { + if(!Group.isMember(payee)){ + throw new ExpensesException(payee + " is not a member of the group!"); + } + } + private static String mergeBack(String[] splitArray){ String mergedString = ""; for(int i = 0; i < splitArray.length-2; i++){ diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java index f06de2b561..5a7bde30a6 100644 --- a/src/main/java/seedu/duke/storage/GroupStorage.java +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -23,7 +23,7 @@ public class GroupStorage { private static final String MEMBERS_HEADER = "Members:"; private static final String EXPENSES_HEADER = "Expenses:"; - private static final String EXPENSE_DELIMITER = ","; + private static final String EXPENSE_DELIMITER = "◇"; private static final String PAYEE_DELIMITER = ":"; private static final String PAYEE_DATA_DELIMITER = ","; From 8759b53521daed26f1159e471a54c416276063ca Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 02:03:22 +0800 Subject: [PATCH 187/270] Update DG - followed addressbook and add more diagrams --- docs/DeveloperGuide.md | 179 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 165 insertions(+), 14 deletions(-) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index df402113df..447c808c70 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -39,20 +39,81 @@ The `createGroup(String groupName)` method is responsible for creating a new gro #### Implementation -The "Add Member to Group" feature is facilitated by the `Group` class. It provides methods to manage group membership -and allows users to add new members to an existing group. The implementation of this feature is as follows: - -The Group class maintains a list of members as a `private List` field called `members`. - -Users can add a new member to the group by using the command `member USER_NAME`. The `addMember(String memberName)` method is responsible for adding a new member to the group. It performs the following steps: - -1. Checks if a user with the given `memberName` is already a member of the group using the `isMember(String memberName)` - method. -2. If the user is not a member, creates a new `User` object with the provided `memberName`. -3. Adds the new `User` object to the `members` list. -4. Prints a success message indicating that the member has been added to the group. - -![Sequence Diagram](AddMember.png) +The "Add Member to Group" feature is facilitated by the `Group` class. It extends the `Group` class with methods to manage group membership and allows users to add new members to an existing group. Additionally, it implements the following operations: + +- `Group#addMember(String memberName)` — Adds a new member to the group with the given `memberName`. +- `Group#isMember(String memberName)` — Checks if a user with the given `memberName` is already a member of the group. + +These operations are exposed in the `GroupCommand` class as `GroupCommand#addMember(String memberName)`. + +Given below is an example usage scenario and how the "Add Member to Group" feature behaves at each step. + +Step 1. The user launches the application and enters a group named "Project Team" using the `group Project Team` command. The `Group` object for "Project Team" will be initialized with an empty `members` list. + +Step 2. The user executes the `member John` command to add a new member named "John" to the "Project Team" group. The `member` command calls `GroupCommand#addMember("John")`, which in turn calls `Group#addMember("John")`. This operation checks if "John" is already a member of the group using `Group#isMember("John")`. Since "John" is not a member, a new `User` object with the name "John" is created and added to the `members` list of the "Project Team" group. + +```plantuml +@startuml +actor User +participant GroupCommand +participant Group +participant User + +User -> GroupCommand: member John +GroupCommand -> Group: addMember("John") +Group -> Group: isMember("John") +Group -> User: new User("John") +Group -> Group: members.add(johnUser) +@enduml +``` + +Step 3. The user executes the `member Emily` command to add another member named "Emily" to the "Project Team" group. Similar to step 2, the `member` command calls `GroupCommand#addMember("Emily")`, which then calls `Group#addMember("Emily")`. After checking that "Emily" is not already a member, a new `User` object with the name "Emily" is created and added to the `members` list of the "Project Team" group. + +Step 4. The user tries to add "John" again to the "Project Team" group by executing the `member John` command. However, since "John" is already a member of the group, the `Group#isMember("John")` check in `Group#addMember("John")` returns `true`. As a result, an error message is displayed to the user, indicating that "John" is already a member of the group, and no duplicate member is added. + +```plantuml +@startuml +actor User +participant GroupCommand +participant Group + +User -> GroupCommand: member John +GroupCommand -> Group: addMember("John") +Group -> Group: isMember("John") +Group --> GroupCommand: "John is already a member" +GroupCommand --> User: "John is already a member" +@enduml +``` + +The following sequence diagram illustrates the flow of the "Add Member to Group" feature: + +```plantuml +@startuml +actor User +participant GroupCommand +participant Group +participant User + +User -> GroupCommand: member USER_NAME +GroupCommand -> Group: addMember(memberName) +Group -> Group: isValidMemberName(memberName) +alt is valid member name + Group -> Group: isMember(memberName) + alt is not a member + Group -> User: new User(memberName) + Group -> Group: members.add(newMember) + Group --> GroupCommand: success message + else is already a member + Group --> GroupCommand: failure message + end +else is invalid member name + Group --> GroupCommand: failure message +end +GroupCommand --> User: command result +@enduml +``` + +[//]: # (![Sequence Diagram](AddMember.png)) ### Expenses feature @@ -105,6 +166,96 @@ settle the debt. The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed by `userName2` to `userName1` after the settlement. +### Group Storage feature + +#### Implementation + +The "Group Storage" feature is facilitated by the `GroupStorage` class. It extends the functionality of the `Group` class by providing methods to save and load group information to and from files. The `GroupStorage` class interacts with the `FileIO` interface for file input/output operations. Additionally, it implements the following key operations: + +- `GroupStorage#saveGroupToFile(Group group)` — Saves the group information to a file when a user exits a group or ends the program. +- `GroupStorage#loadGroupFromFile(String groupName)` — Loads the group information from a file when a user enters a group. + +These operations are invoked from the `Group` class when the user performs specific actions related to groups. + +Given below is an example usage scenario and how the "Group Storage" feature behaves at each step. + +Step 1. The user launches the application and tries to create a group named "Project Team" using the `create Project Team` command. The `Group#getOrCreateGroup(String groupName)` method is called to retrieve or create the group. + +Step 2. Inside the `Group#getOrCreateGroup(String groupName)` method, it checks if the group already exists in memory. If not, it uses the `GroupNameChecker` class to check if the group file exists. If the group file doesn't exist, a new `Group` object is created, and the user is placed in the newly created group. + +Step 3. The user executes various commands to add members and expenses to the "Project Team" group. These changes are made to the `Group` object in memory. + +Step 4. The user executes the `exit Project Team` command to exit the "Project Team" group. This command invokes the `Group#exitGroup(String groupName)` method, which in turn calls the `GroupStorage#saveGroupToFile(Group group)` method to save the current state of the "Project Team" group to a file. The saving process includes writing the group name, members, and expenses to the file in a structured format. + +```plantuml +@startuml +actor User +participant GroupCommand +participant Group +participant GroupStorage +participant FileIO + +User -> GroupCommand: exit Project Team +GroupCommand -> Group: exitGroup("Project Team") +Group -> GroupStorage: saveGroupToFile(projectTeamGroup) +GroupStorage -> FileIO: getFileWriter(filePath) +GroupStorage -> GroupStorage: saveGroupName(writer, groupName) +GroupStorage -> GroupStorage: saveMembers(writer, members) +GroupStorage -> GroupStorage: saveExpenses(writer, expenses) +GroupStorage -> FileIO: writer.close() +@enduml +``` +Step 5. Later, the user decides to enter the "Project Team" group again using the `enter Project Team` command. The `Group#enterGroup(String groupName)` method is called to enter the group. + +Step 6. Inside the `Group#enterGroup(String groupName)` method, it first checks if the group exists in memory. If not, it uses the `GroupNameChecker` class to check if the group file exists. If the group file exists, it invokes the `GroupStorage#loadGroupFromFile(String groupName)` method to load the group information from the file. + +```plantuml +@startuml +actor User +participant GroupCommand +participant Group +participant GroupNameChecker +participant GroupStorage +participant FileIO + +User -> GroupCommand: enter Project Team +GroupCommand -> Group: enterGroup("Project Team") +Group -> Group: check if group exists in memory +alt group does not exist in memory + Group -> GroupNameChecker: doesGroupNameExist("Project Team") + alt group file exists + Group -> GroupStorage: loadGroupFromFile("Project Team") + GroupStorage -> FileIO: getFileReader(filePath) + GroupStorage -> GroupStorage: loadGroupName(reader) + GroupStorage -> GroupStorage: loadMembers(reader, group) + GroupStorage -> GroupStorage: loadExpenses(reader, group) + GroupStorage -> FileIO: reader.close() + GroupStorage --> Group: loadedGroup + else group file does not exist + Group --> GroupCommand: group does not exist + end +else group exists in memory + Group --> GroupCommand: group found in memory +end +@enduml +``` + +The `GroupStorage#loadGroupFromFile(String groupName)` method reads the group information from the file, creates a new `Group` object, and populates it with the loaded data. This includes the group name, members, and expenses. The loaded `Group` object is then returned to the `Group` class. + +Step 7. The user continues to interact with the "Project Team" group, making changes to its members and expenses. These changes are made to the loaded `Group` object in memory. + +Step 8. When the user ends the program using the `bye` command, the `GroupStorage#saveGroupToFile(Group group)` method is invoked again to save the current state of all loaded groups to their respective files. This ensures that any changes made during the session are persisted. + +**Design Considerations:** + +- **Alternative 1 (current choice):** Saving group information to files when exiting groups or ending the program, and loading group information when entering groups. + - Pros: Minimizes file I/O operations and reduces the overhead of constantly saving and loading group information. + - Cons: Changes made to a group are not persisted until the user explicitly exits the group or ends the program. +- **Alternative 2:** Saving group information after every command that modifies the group, and loading group information whenever a group is accessed. + - Pros: Ensures that changes are immediately persisted and reduces the risk of data loss in case of unexpected program termination. + - Cons: Increases file I/O operations and may impact performance, especially for frequent group modifications. + + ## Product scope ### Target user profile From 009aa7d2ab43e79026591cefb1cbb9442ba6043f Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 02:12:19 +0800 Subject: [PATCH 188/270] Add images --- docs/DeveloperGuide.md | 12 +++++++++++- docs/{ => diagrams}/AddMember.png | Bin docs/diagrams/addMember1.png | Bin 0 -> 12196 bytes docs/diagrams/addMember2.png | Bin 0 -> 12831 bytes docs/diagrams/addMember3.png | Bin 0 -> 24425 bytes docs/diagrams/groupStorage1.png | Bin 0 -> 22619 bytes docs/diagrams/groupStorage2.png | Bin 0 -> 38716 bytes 7 files changed, 11 insertions(+), 1 deletion(-) rename docs/{ => diagrams}/AddMember.png (100%) create mode 100644 docs/diagrams/addMember1.png create mode 100644 docs/diagrams/addMember2.png create mode 100644 docs/diagrams/addMember3.png create mode 100644 docs/diagrams/groupStorage1.png create mode 100644 docs/diagrams/groupStorage2.png diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 447c808c70..ffa4eb5953 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -67,6 +67,8 @@ Group -> Group: members.add(johnUser) @enduml ``` +![Sequence Diagram](diagrams/addMember1.png) + Step 3. The user executes the `member Emily` command to add another member named "Emily" to the "Project Team" group. Similar to step 2, the `member` command calls `GroupCommand#addMember("Emily")`, which then calls `Group#addMember("Emily")`. After checking that "Emily" is not already a member, a new `User` object with the name "Emily" is created and added to the `members` list of the "Project Team" group. Step 4. The user tries to add "John" again to the "Project Team" group by executing the `member John` command. However, since "John" is already a member of the group, the `Group#isMember("John")` check in `Group#addMember("John")` returns `true`. As a result, an error message is displayed to the user, indicating that "John" is already a member of the group, and no duplicate member is added. @@ -85,6 +87,8 @@ GroupCommand --> User: "John is already a member" @enduml ``` +![Sequence Diagram](diagrams/addMember2.png) + The following sequence diagram illustrates the flow of the "Add Member to Group" feature: ```plantuml @@ -113,7 +117,8 @@ GroupCommand --> User: command result @enduml ``` -[//]: # (![Sequence Diagram](AddMember.png)) +![Sequence Diagram](diagrams/addMember3.png) + ### Expenses feature @@ -205,6 +210,9 @@ GroupStorage -> GroupStorage: saveExpenses(writer, expenses) GroupStorage -> FileIO: writer.close() @enduml ``` + +![Sequence Diagram](diagrams/groupStorage1.png) + Step 5. Later, the user decides to enter the "Project Team" group again using the `enter Project Team` command. The `Group#enterGroup(String groupName)` method is called to enter the group. Step 6. Inside the `Group#enterGroup(String groupName)` method, it first checks if the group exists in memory. If not, it uses the `GroupNameChecker` class to check if the group file exists. If the group file exists, it invokes the `GroupStorage#loadGroupFromFile(String groupName)` method to load the group information from the file. @@ -240,6 +248,8 @@ end @enduml ``` +![Sequence Diagram](diagrams/groupStorage2.png) + The `GroupStorage#loadGroupFromFile(String groupName)` method reads the group information from the file, creates a new `Group` object, and populates it with the loaded data. This includes the group name, members, and expenses. The loaded `Group` object is then returned to the `Group` class. Step 7. The user continues to interact with the "Project Team" group, making changes to its members and expenses. These changes are made to the loaded `Group` object in memory. diff --git a/docs/AddMember.png b/docs/diagrams/AddMember.png similarity index 100% rename from docs/AddMember.png rename to docs/diagrams/AddMember.png diff --git a/docs/diagrams/addMember1.png b/docs/diagrams/addMember1.png new file mode 100644 index 0000000000000000000000000000000000000000..028941b9d48fc9610e527bda56d3ccce14efef84 GIT binary patch literal 12196 zcmd6NbySpH`|co$3eqs3fNu{RI4Cc9eoU8r!{awKje32$3=t?&bGT?2nGvvxZ~f?Ay{CXSBXyVJCvF@h~8|RkmY}jkVrq5B;Up&*Ulj$ zdHS@sCLtD~UQFAA7(Y5kW{fy7}I&x0ixx)QeyqVFMwWeNjjqCLs_a8 z+?ej?#XG#rE^(LX&3ziiZpJcW;j)d(3!UstDg#bEYCS1UI9t~r(K@>0FUntuI9|2W zyqZ{PPFfT!Lb)_WIUl^de$~)($}CDMR3@ODo{Ax|hfcBAoK|>(mq3P_DjOk49!VTs zjJLW8Jjlqq6rYURNKt>U1s z%geS%G@|0lR|Lf8bymi#rFy?v9{E7`i;GC*O3Uqz7vKw1uGF3FzrhXHtM_tQ8TdXu zjZh5sMMEGXzq~K1R@1}ys2;^Khz=X5^?mX7b~aypkgJ|&^bkx)EFn?oz}Unj1Wr|J z(kjZhwUnLwAu4J&=Q8Fs@cJhxFjEBU1n~jGLBI+$VBQ1s0kdBmhplaGBSlz^jg9fv z(wU85pT2<~2&-1K zJN3g)hZv)(c(}N5;-|P7loAEpc5}oZ_#30q=-Mhu|LKw-XpFwM0{8p3=pJ!b(Is^w z+zIhKRX(o_&MZlq*I=dKH=r}@dwY8b6ddhw(fsbVj*gBI=BXe0@Y+%kUFLBaMVvyV z?YN+#Nb;W!HyG(2^peWSuSKios?(7)g1v#^99{_u+RgaYoSB&^F+{AJ87jUPv_X@%gjzpP3IaB9%Zouqt{{0%3yc6 zxVYSEg`Du$&(9}x6lkhvW_IF>H^x?1H<+1kv9M%jE9s3@t~t24guS*1<>l>A%dR_~ zPq^?r-bIg$=uognd+z-_*VK>0L2k7nwCxy5C`IpM?bX@@`up?p^B+tg68L30l1>cr z+#j4>tt_D|&dzR`la%s&Zfwl&@k!+DUFbw=8etigJRM!e=TDz{FE+1}TF*>J4DqcD z2A-T;%@M#N4I*XZlZB0RFXIy~*V1O2k;%X#Hi zaV=kcMn?47$7pB6<$g`Z$$m%c7n?eoMY6MFQL-@-3({BC$ByRiRsLB430Yy9@SKfF ziw~_PbfraiZ7nT1H3wTKz1*dpRuVy2=bovl?mvAZ{9-C$d)r`zqKP9Xb!gpHgE$Wyye6z0f!;2)sy@dE+;p4SqwDY+wJF1#XOyb*2pW?AEdo=bK|z7w{~}XGn`7L74SYx z&GGs@38OT6v1gf7nw~F0)%x~{+I8G~ll(h2S(|4z%TCq@qzs*?7_al{k)feCPa^Tj z*(`~46js(QL;D`{R8Y#jxkTju0Nwb^%E}I<*%IRVSYI$Ii%L=3%wh@usMdTnD+sNA zcfI+Y`*w>ee68m>1)FXrab#}pO_#N%0DR-y{XZ)l-Jd#M$EDzX%bK<4j5)jVtkSf( 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 Qn6(lI_uE^Po1cvSH)yW|82|tP literal 0 HcmV?d00001 diff --git a/docs/diagrams/groupStorage1.png b/docs/diagrams/groupStorage1.png new file mode 100644 index 0000000000000000000000000000000000000000..a69b66be717661c65991aa0291c2ce0af38b3ce0 GIT binary patch literal 22619 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�fN~^g+UTSPWW#ye^JxzN)Ke(G`F4en+2k7*C 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 i1!?nt|DGo? Date: Sun, 14 Apr 2024 11:11:58 +0800 Subject: [PATCH 189/270] Settle update --- docs/UserGuide.md | 27 +++++++------- src/main/java/seedu/duke/Balance.java | 16 +++++---- src/main/java/seedu/duke/Expense.java | 23 ++++++------ src/main/java/seedu/duke/Group.java | 47 +++++++++++++++++++++---- src/main/java/seedu/duke/Settle.java | 20 +++-------- src/test/java/seedu/duke/GroupTest.java | 43 ++++++++++++---------- 6 files changed, 103 insertions(+), 73 deletions(-) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 8271448e33..1a37938747 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -40,11 +40,12 @@ Welcome, here is a list of commands: Creates a new group with the specified group name. -Format: `create GROUP_NAME` +Format: `create /group GROUP_NAME` -`GROUP_NAME` is the name of the group. +- `/group` is a keyword to indicate the start of the group name. +- `GROUP_NAME` is the name of the group. -Example: `create Friends` +Example: `create /group Friends` This command will create a new group named 'Friends'. @@ -52,11 +53,12 @@ This command will create a new group named 'Friends'. Enters an existing group with the specified group name. -Format: `enter GROUP_NAME` +Format: `enter /group GROUP_NAME` -`GROUP_NAME` is the name of the group. +- `/group` is a keyword to indicate the start of the group name. +- `GROUP_NAME` is the name of the group. -Example: `enter Friends` +Example: `enter /group Friends` This command will enter the group named 'Friends'. @@ -78,11 +80,14 @@ Output: `Alice has been added to group.` Exits the current group. -Format: `exit GROUP_NAME` +Format: `exit /group GROUP_NAME` -Example: `exit Friends` +- `/group` is a keyword to indicate the start of the group name. +- `GROUP_NAME` is the name of the group. -This command will exit the current group. +Example: `exit /group Friends` + +This command will exit the group named 'Friends'. ### Show balance of user: `balance` @@ -167,10 +172,8 @@ This command exits the application. ## Command Summary -{Give a 'cheat sheet' of commands here} - - `help`: Shows a message explaining how to use the application. -- `create group GROUP_NAME`: Creates a new group with the specified group name. +- `create GROUP_NAME`: Creates a new group with the specified group name. - `enter GROUP_NAME`: Enters an existing group with the specified group name. - `member USER_NAME`: Adds a new member to the group. - `exit`: Exits the current group. diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index d09242bc4d..774fc7a13d 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -13,7 +13,7 @@ public Balance(String userName, Map userList) { this.balanceList = userList; } - public Balance(String userName, Group group){ + public Balance(String userName, Group group) { this(userName, group.getExpenseList(), group.getMembers()); } @@ -23,7 +23,7 @@ public Balance(String userName, List expenses, List users) { // Populate balanceList with other Users from Group for (User user : users) { - if(!user.getName().equals(userName)) { + if (!user.getName().equals(userName)) { balanceList.put(user.getName(), 0f); } } @@ -46,12 +46,12 @@ private void addExpense(Expense expense) { String payerName = expense.getPayerName(); List> payees = expense.getPayees(); - if(payerName.equals(userName)) { - for(Pair payee : payees) { + if (payerName.equals(userName)) { + for (Pair payee : payees) { String payeeName = payee.getKey(); Float payeeAmount = payee.getValue(); - if(payeeName.equals(userName)){ + if (payeeName.equals(userName)) { continue; } @@ -61,7 +61,7 @@ private void addExpense(Expense expense) { balanceList.put(payeeName, newOwed); } } else { - for(Pair payee : payees) { + for (Pair payee : payees) { String payeeName = payee.getKey(); Float payeeAmount = payee.getValue(); @@ -79,6 +79,8 @@ private void addExpense(Expense expense) { } public void printBalance() { + System.out.println("Debug - Current balances for " + userName + ": " + balanceList); + String firstLine = String.format("User %s's Balance List:", userName); System.out.println(firstLine); @@ -89,4 +91,4 @@ public void printBalance() { System.out.println("End of Balance List"); } -} +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 192ada042f..76076dfd45 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -25,11 +25,11 @@ public class Expense { * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided - * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and - * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) + * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and + * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) */ public Expense(boolean isUnequal, String payerName, String description, - float totalAmount, ArrayList> payees) + float totalAmount, ArrayList> payees) throws ExpensesException { this.payees = payees; this.payerName = payerName; @@ -38,7 +38,7 @@ public Expense(boolean isUnequal, String payerName, String description, printSuccessMessage(); } - public Expense(String payerName, String description, float totalAmount, ArrayList> payees){ + public Expense(String payerName, String description, float totalAmount, ArrayList> payees) { this.payees = payees; this.payerName = payerName; this.totalAmount = totalAmount; @@ -46,9 +46,6 @@ public Expense(String payerName, String description, float totalAmount, ArrayLis printSuccessMessage(); } - public Expense(User payer, double amount) { - } - //@@author Cohii2 public String getPayerName() { return payerName; @@ -83,12 +80,14 @@ public String toString() { void printSuccessMessage() { if (!GroupStorage.isLoading) { - System.out.println("Added new expense with description " + description + " and amount " + totalAmount - + " paid by " + payerName + " and split between:"); - for (Pair payee : payees) { - System.out.println(payee.getKey() + " who owes " + String.format("%.2f", payee.getValue())); + if (!(this instanceof Settle)) { + System.out.println("Added new expense with description " + description + " and amount " + totalAmount + + " paid by " + payerName + " and split between:"); + for (Pair payee : payees) { + System.out.println(payee.getKey() + " who owes " + String.format("%.2f", payee.getValue())); + } + System.out.println(); } - System.out.println(); } } diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index b0444f035f..a66ba7442a 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -16,7 +16,7 @@ public class Group { static Map groups = new HashMap<>(); - private static Optional currentGroupName = Optional.empty(); + static Optional currentGroupName = Optional.empty(); private static final GroupStorage groupStorage = new GroupStorage(new FileIOImpl()); private static GroupNameChecker groupNameChecker = new GroupNameChecker(); @@ -41,6 +41,7 @@ public Group(String groupName) { * @return The existing or newly created group. * @throws IllegalStateException If trying to create or join a new group while already in another group. */ + //@@ author avrilgk public static Optional getOrCreateGroup(String groupName) { @@ -50,6 +51,12 @@ public static Optional getOrCreateGroup(String groupName) { return Optional.empty(); } + if (currentGroupName.isPresent()) { + System.out.println("You are currently in " + currentGroupName.get() + + ". Exit current group before creating another one."); + return Optional.empty(); + } + Optional group = Optional.ofNullable(groups.get(groupName)); // Check if user is accessing a group they are already in @@ -87,6 +94,12 @@ public static Optional getOrCreateGroup(String groupName) { * @return The existing group. */ public static Optional enterGroup(String groupName) { + + if (groupName == null || groupName.trim().isEmpty()) { + System.out.println("Group name cannot be empty. Please try again."); + return Optional.empty(); + } + if (currentGroupName.isPresent()) { System.out.println("You are currently in " + currentGroupName.get() + ". Exit current group before entering another one."); @@ -134,10 +147,10 @@ public static Optional enterGroup(String groupName) { * If the user is not in any group, it displays a message asking the user to try again. */ public static void exitGroup(String groupName) { + if (currentGroupName.isPresent()) { if (!currentGroupName.get().equals(groupName)) { - System.out.println("You are not currently in group " + groupName - + ". Please enter the correct group name."); + System.out.println("Invalid group name. Please enter the correct group name."); return; } //@@author hafizuddin-a @@ -261,14 +274,34 @@ public void settle(String payerName, String payeeName) { double amount = calculateOutstandingAmount(payee, payer); if (amount > 0) { - Settle settle = new Settle(payer, payee, amount); - addExpense(settle); System.out.println(payerName + " should pay " + payeeName + " " + String.format("%.2f", amount)); - } else { - System.out.println(payerName + " does not owe " + payeeName + " any money."); } + + updateBalancesAfterSettlement(payerName, payeeName); + System.out.println(payerName + " has settled the full amount with " + payeeName); + + } + + private void updateBalancesAfterSettlement(String payerName, String payeeName) { + Balance payerBalance = findOrCreateBalance(payerName); + Balance payeeBalance = findOrCreateBalance(payeeName); + + if (payerBalance != null && payeeBalance != null) { + payerBalance.getBalanceList().put(payeeName, 0f); // Reset payer's debt to payee + payeeBalance.getBalanceList().put(payerName, 0f); // Reset payee's debt to payer + } + } + + + private Map userBalances = new HashMap<>(); + + private Balance findOrCreateBalance(String userName) { + Balance balance = userBalances.computeIfAbsent(userName, k -> new Balance(userName, new HashMap<>())); + System.out.println("Retrieving/Creating balance for " + userName + ": " + balance.getBalanceList()); + return balance; } + /** * Finds a user by their name. * diff --git a/src/main/java/seedu/duke/Settle.java b/src/main/java/seedu/duke/Settle.java index 3210ff9b11..a63aab79ed 100644 --- a/src/main/java/seedu/duke/Settle.java +++ b/src/main/java/seedu/duke/Settle.java @@ -2,6 +2,8 @@ package seedu.duke; +import java.util.ArrayList; + /** * The Settle class represents a transaction between two users. * It extends the Expense class and has a payer, payee and amount. @@ -23,31 +25,17 @@ public class Settle extends Expense { * @param amount The amount of the payment */ public Settle(User payer, User payee, double amount) { - super(payer, amount); - assert payer != null : "Payer cannot be null"; - assert payee != null : "Payee cannot be null"; - assert amount >= 0 : "Amount cannot be negative"; + super(payer.getName(), "", (float) amount, new ArrayList<>()); this.payer = payer; this.payee = payee; this.amount = amount; } - /** - * Returns the user who is making the payment. - * - * @return The user who is making the payment - */ - + @Override public String getPayer() { return payer.getName(); } - /** - * Returns a string representation of the Settle object. - * The returned string is in the format "payerName paid payeeName amount". - * - * @return A string representation of the Settle object - */ @Override public String toString() { return payer.getName() + " paid " + payee.getName() + " " + amount; diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index f8318b2987..4e14931a4d 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -14,53 +14,58 @@ public class GroupTest { @BeforeEach public void setup() { - // Reset the state before each test - Group.exitGroup(); + // Clear all groups and reset the current group name before each test + Group.groups.clear(); + Group.currentGroupName = Optional.empty(); } @AfterEach public void teardown() { - // Clean up after each test - Group.exitGroup(); + // Optionally clear all after each test if needed + Group.groups.clear(); + Group.currentGroupName = Optional.empty(); } @Test public void testGroupCreation() { String expectedName = "GroupName"; + Group.groups.clear(); Optional group = Group.getOrCreateGroup(expectedName); - assertEquals(expectedName, group.get().getGroupName(), "Group name is not the same as expected"); + assertEquals(group.isPresent(), true, "Group should be created"); + assertEquals(expectedName, group.get().getGroupName(), "Group name should match the expected name"); } @Test public void testGetOrCreateGroup() { String groupName = "NewGroup"; + Group.groups.clear(); // Ensure that the group does not exist before the test Optional newGroup = Group.getOrCreateGroup(groupName); + assertEquals(newGroup.isPresent(), true, "New group should be created successfully"); - assertEquals(groupName, newGroup.get().getGroupName(), "Group name is not the expected value"); - - Group.exitGroup(); + Group.exitGroup(groupName); + Group.groups.clear(); // Ensure that the group does not exist before the test Optional existingGroup = Group.getOrCreateGroup(groupName); - assertEquals(newGroup.get(), existingGroup.get(), "getOrCreateGroup should return the existing group"); - assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); + assertTrue(existingGroup.isPresent(), "Should retrieve the existing group"); + assertEquals(newGroup.get(), existingGroup.get(), "Should return the same group object for existing group"); } @Test public void testExitGroup() { String groupName = "ExitingGroup"; + Group.groups.clear(); // Ensure that the group does not exist before the test Group.getOrCreateGroup(groupName); - Group.exitGroup(); - assertTrue(Group.getCurrentGroup().isEmpty(), "Did not successfully exit the group"); + Group.exitGroup(groupName); + assertTrue(Group.getCurrentGroup().isEmpty(), "Should successfully exit the group"); } @Test public void testGetCurrentGroup() { String groupName = "CurrentGroup"; - Optional group = Group.getOrCreateGroup(groupName); - - assertEquals(group.get(), Group.getCurrentGroup().get(), "Current group is not the expected group"); - - Group.exitGroup(); - assertTrue(Group.getCurrentGroup().isEmpty(), "Current group should be empty after exiting"); + Group.groups.clear(); // Ensure that the group does not exist before the test + Group.getOrCreateGroup(groupName); + Optional currentGroup = Group.getCurrentGroup(); + assertEquals(currentGroup.isPresent(), true, "Current group should not be empty"); + assertEquals(groupName, currentGroup.get().getGroupName(), "Current group should match the expected group"); } -} +} \ No newline at end of file From 7a851c093f83ae2f74f17224642569df8883ba75 Mon Sep 17 00:00:00 2001 From: "KRISHNAAYAGARI\\kak36" Date: Sun, 14 Apr 2024 12:47:07 +0800 Subject: [PATCH 190/270] add junit testing for ExpenseCommand --- src/main/java/seedu/duke/Expense.java | 21 +++- .../seedu/duke/commands/ExpenseCommand.java | 44 +++++--- src/test/java/seedu/duke/ExpenseTest.java | 104 +++++++++++++++--- 3 files changed, 134 insertions(+), 35 deletions(-) diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 66e2795ddb..65e1b3231e 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -19,9 +19,8 @@ public class Expense { private String description; /** - * Constructor to create new Expense - * - * @param isUnequal : Boolean showing whether expense is split unequally or not + * Constructor to create new Unequal Expense + * @param isUnequal : Boolean showing whether expense is split unequally or not * @param payerName : The name of the user who paid for the Expense * @param description : Description of the expense * @param totalAmount : The total amount before being divided @@ -38,6 +37,14 @@ public Expense(boolean isUnequal, String payerName, String description, printSuccessMessage(); } + /** + * Constructor to create new Equal Expense + * @param payerName : The name of the user who paid for the Expense + * @param description : Description of the expense + * @param totalAmount : The total amount before being divided + * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and + * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) + */ public Expense(String payerName, String description, float totalAmount, ArrayList> payees){ this.payees = payees; this.payerName = payerName; @@ -70,6 +77,10 @@ public String getDescription() { return description; } + /** + * Converts the Expense to string form containing all the expense details + * @return : A string containing expense details in a tabular format + */ @Override public String toString() { String expensesDetails = ""; @@ -84,9 +95,9 @@ public String toString() { void printSuccessMessage() { if (!GroupStorage.isLoading) { System.out.println("Added new expense with description " + description + " and amount " + - String.format("%.2f",totalAmount) + " paid by " + payerName + " and split between:"); + String.format("%.2f",totalAmount) + " paid by " + payerName + " . The split is:"); for (Pair payee : payees) { - System.out.println(payee.getKey() + " who owes " + String.format("%.2f", payee.getValue())); + System.out.println(payee.getKey() + " : " + String.format("%.2f", payee.getValue())); } System.out.println(); } diff --git a/src/main/java/seedu/duke/commands/ExpenseCommand.java b/src/main/java/seedu/duke/commands/ExpenseCommand.java index b29820054d..4be5f6804c 100644 --- a/src/main/java/seedu/duke/commands/ExpenseCommand.java +++ b/src/main/java/seedu/duke/commands/ExpenseCommand.java @@ -45,7 +45,12 @@ public static void addExpense(HashMap > params,String } //@@author mukund1403 - public static void deleteExpense(String argument) throws ExpensesException { + + /** + * The method deletes expense from the expenses list + * @param listIndex : The index from the list, the user wishes to delete (will be 1 indexed) + */ + public static void deleteExpense(String listIndex) throws ExpensesException { Optional currentGroup = Group.getCurrentGroup(); if (currentGroup.isEmpty()) { String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; @@ -53,22 +58,13 @@ public static void deleteExpense(String argument) throws ExpensesException { } List expenseList = currentGroup.get().getExpenseList(); int listSize = expenseList.size(); - int index = getListIndex(argument, listSize) - 1; + int index = getListIndex(listIndex, listSize) - 1; String deletedExpenseDescription = expenseList.get(index).toString(); currentGroup.get().deleteExpense(index); System.out.println("Deleted expense:\n" + deletedExpenseDescription); } - private static void checkDescription(String argument) throws ExpensesException { - if(argument.isEmpty()){ - System.out.println("Warning! Empty description"); - } else if(argument.contains("◇")){ - throw new ExpensesException("Special characters not allowed in description! " + - "(Good try trynna catch a bug!)"); - } - } - - private static Float getTotal(HashMap > params) throws ExpensesException { + public static Float getTotal(HashMap> params) throws ExpensesException { float totalAmount; try { totalAmount = Float.parseFloat(params.get("amount").get(0)); @@ -76,15 +72,20 @@ private static Float getTotal(HashMap > params) throws String exceptionMessage = "Re-enter expense with amount as a proper number. (Good bug to start with tbh!)"; throw new ExpensesException(exceptionMessage); } + int maxNumberHandled = 2000000000; if(totalAmount <= 0){ String exceptionMessage = "Expense amount cannot be 0 or a negative number " + "(Can try using special characters. I have not handled that!)"; throw new ExpensesException(exceptionMessage); + } else if(totalAmount > maxNumberHandled) { + String exceptionMessage = "This amount is too big for a small computer like me to handle :(. " + + "Please use a smaller amount"; + throw new ExpensesException(exceptionMessage); } return totalAmount; } - private static int getListIndex(String listIndex, int listSize) throws ExpensesException { + public static int getListIndex(String listIndex, int listSize) throws ExpensesException { int index; try{ index = Integer.parseInt(listIndex); @@ -97,13 +98,13 @@ private static int getListIndex(String listIndex, int listSize) throws ExpensesE String exceptionMessage = "List index is greater than list size"; throw new ExpensesException(exceptionMessage); } else if (index <= 0){ - String exceptionMessage = "List index cannot be negative"; + String exceptionMessage = "List index cannot be 0 or negative"; throw new ExpensesException(exceptionMessage); } return index; } - private static Expense addUnequalExpense(ArrayList payeeList,ArrayList> payees, + public static Expense addUnequalExpense(ArrayList payeeList,ArrayList> payees, float totalAmount,String payerName,String argument) throws ExpensesException{ float amountDueByPayees = 0; int payeeInfoMinLength = 2; @@ -135,8 +136,8 @@ private static Expense addUnequalExpense(ArrayList payeeList,ArrayList

payeeList, ArrayList> payees, - float totalAmount, String payerName, String argument) throws ExpensesException { + public static Expense addEqualExpense(ArrayList payeeList, ArrayList> payees, + float totalAmount,String payerName,String argument)throws ExpensesException { Float amountDue = totalAmount / (payeeList.size() + 1); for (String payee : payeeList) { checkPayeeInGroup(payee); @@ -147,6 +148,15 @@ private static Expense addEqualExpense(ArrayList payeeList, ArrayList(Arrays.asList( - new Pair<>("cohii", 2.0f), - new Pair<>("shao", 3.20f), - new Pair<>("avril", 1.0f), - new Pair<>("hafiz", 2.0f), - new Pair<>("mukund", 1.8f) - ))); - assertEquals("description disneyland and amount 10.0 paid by mukund " + - "and split between:\ncohii who owes 2.00\nshao who owes 3.20\navril who owes 1.00" + - "\nhafiz who owes 2.00\nmukund who owes 1.80\n",testExpense.toString()); + ArrayList> payees = new ArrayList<>(Arrays.asList( + new Pair<>("cohii", 2.0f), + new Pair<>("shao", 3.20f), + new Pair<>("avril", 1.0f), + new Pair<>("hafiz", 2.0f), + new Pair<>("mukund", 1.8f) + )); + Expense testExpense1 = new Expense(true,"mukund","disneyland", + 10, payees); + + assertEquals(testExpense1.getPayees(),payees); + } + + @Test + public void amountNotFloatTest() { + ArrayList amountArrayList = new ArrayList<>(); + amountArrayList.add("b"); + HashMap> params = new HashMap<>(); + params.put("amount",amountArrayList); + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getTotal(params), + "Function should throw Expenses exception since exception occured in expenses class."); + assertEquals("Re-enter expense with amount as a proper number. " + + "(Good bug to start with tbh!)", e.getMessage(), + "Exception message should indicate that amount entered was not a number"); + + } + + @Test + public void listIndexGreaterTest(){ + String listIndex = "20"; + int listSize = 2; + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex, listSize), + "Function should throw ExpensesException"); + assertEquals("List index is greater than list size",e.getMessage(), + "Exception message should indicate that listIndex entered was greater than list size."); + } + + @Test + public void listIndexNegativeTest(){ + String listIndex = "-1"; + int listSize = 2; + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex, listSize), + "Function should throw ExpensesException"); + assertEquals("List index cannot be 0 or negative",e.getMessage(), + "Exception message should indicate that listIndex entered was less than or equal to 0."); + } + + @Test + public void listIndexNotIntegerTest(){ + String listIndex1 = "a"; + int listSize = 2; + Exception e1 = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex1, listSize), + "Function should throw ExpensesException"); + assertEquals("Enter a list index that is an Integer",e1.getMessage(), + "Exception message should indicate that listIndex entered was not an integer."); + + String listIndex2 = "5.0"; + Exception e2 = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex1, listSize), + "Function should throw ExpensesException"); + assertEquals("Enter a list index that is an Integer",e2.getMessage(), + "Exception message should indicate that listIndex entered was not an integer."); + } + + @Test + public void unequalExpenseTest() throws ExpensesException { + ArrayList payeeList = new ArrayList<>(Arrays.asList( + ("cohii"), + ("shao 1"), + ("avril 5.5"), + ("hafiz"), + ("mukund 2") + )); + ArrayList> payees = new ArrayList<>(); + float totalAmount = 10; + String payerName = "mukund"; + String argument = "disneyland"; + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.addUnequalExpense(payeeList, payees, totalAmount, payerName, argument), + "Function should throw ExpensesException"); + assertEquals("Amount due for payee with name cohii" + + " is empty. Enter it and try again", e.getMessage(), + "Exception message should indicate that amount for user has not been entered"); } } From a5f99419fae4858d53f966d2c54f15143e84b4f1 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 14 Apr 2024 13:23:10 +0800 Subject: [PATCH 191/270] update settle function and fixed all PE-D bugs --- src/main/java/seedu/duke/Balance.java | 11 --- src/main/java/seedu/duke/Expense.java | 18 ++++- src/main/java/seedu/duke/Group.java | 88 ++++++++++++++++++++--- src/test/java/seedu/duke/SettleTest.java | 90 +++++++++--------------- 4 files changed, 128 insertions(+), 79 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index 774fc7a13d..8e865fcbd9 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -8,11 +8,6 @@ public class Balance { protected String userName; protected Map balanceList; - public Balance(String userName, Map userList) { - this.userName = userName; - this.balanceList = userList; - } - public Balance(String userName, Group group) { this(userName, group.getExpenseList(), group.getMembers()); } @@ -34,10 +29,6 @@ public Balance(String userName, List expenses, List users) { } } - public String getUserName() { - return userName; - } - public Map getBalanceList() { return balanceList; } @@ -79,8 +70,6 @@ private void addExpense(Expense expense) { } public void printBalance() { - System.out.println("Debug - Current balances for " + userName + ": " + balanceList); - String firstLine = String.format("User %s's Balance List:", userName); System.out.println(firstLine); diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java index 76076dfd45..25bb7d9165 100644 --- a/src/main/java/seedu/duke/Expense.java +++ b/src/main/java/seedu/duke/Expense.java @@ -14,7 +14,7 @@ public class Expense { private String payerName; private float totalAmount; - private ArrayList> payees = new ArrayList<>(); + private ArrayList> payees; private String description; @@ -67,6 +67,16 @@ public String getDescription() { return description; } + + public void clearPayeeValue(String payeeName) { + // replace the value of the payee with 0 + for (int i = 0; i < payees.size(); i++) { + if (payees.get(i).getKey().equals(payeeName)) { + payees.set(i, new Pair<>(payeeName, 0f)); + } + } + } + @Override public String toString() { String expensesDetails = ""; @@ -91,10 +101,14 @@ void printSuccessMessage() { } } + public void clear() { + totalAmount = 0; + payees = new ArrayList<>(); + } + public String getPayer() { return payerName; } } - diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index a66ba7442a..add5fa0ee7 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -262,6 +262,12 @@ public List getExpenseList() { return new ArrayList<>(expenseList); } + /** + * Settles the outstanding amount between two users. + * + * @param payerName The name of the user who will pay the outstanding amount. + * @param payeeName The name of the user who will receive the outstanding amount. + */ public void settle(String payerName, String payeeName) { User payer = findUser(payerName); User payee = findUser(payeeName); @@ -282,25 +288,88 @@ public void settle(String payerName, String payeeName) { } + /** + * Updates the balances of the users after a settlement. + * + * @param payerName The name of the user who will pay the outstanding amount. + * @param payeeName The name of the user who will receive the outstanding amount. + */ private void updateBalancesAfterSettlement(String payerName, String payeeName) { - Balance payerBalance = findOrCreateBalance(payerName); - Balance payeeBalance = findOrCreateBalance(payeeName); - if (payerBalance != null && payeeBalance != null) { - payerBalance.getBalanceList().put(payeeName, 0f); // Reset payer's debt to payee - payeeBalance.getBalanceList().put(payerName, 0f); // Reset payee's debt to payer + ArrayList payeeExpense = getPayerExpense(payeeName); + + if (payeeExpense.size() == 0) { + return; + } + + ArrayList payerExpense = getPayerExpense(payerName); + + int balance = caluclateBalance(payerName, payeeName); + + if (balance < 0) { + return; + } + + for (Expense expense : payeeExpense) { + expense.clear(); + } + + if (payerExpense.size() != 0) { + for (Expense expense : payerExpense) { + expense.clearPayeeValue(payeeName); + } } } + /** + * Calculates the balance between two users. + * + * @param payerName The name of the user who will pay the outstanding amount. + * @param payeeName The name of the user who will receive the outstanding amount. + * @return The balance between the two users. + */ + private int caluclateBalance(String payerName, String payeeName) { + ArrayList payeeExpense = getPayerExpense(payeeName); + ArrayList payerExpense = getPayerExpense(payerName); + + int balance = 0; + + for (Expense expense : payeeExpense) { + for (Pair payee : expense.getPayees()) { + if (payee.getKey().equals(payerName)) { + balance += payee.getValue(); + } + } + } - private Map userBalances = new HashMap<>(); + for (Expense expense : payerExpense) { + for (Pair payee : expense.getPayees()) { + if (payee.getKey().equals(payeeName)) { + balance -= payee.getValue(); + } + } + } - private Balance findOrCreateBalance(String userName) { - Balance balance = userBalances.computeIfAbsent(userName, k -> new Balance(userName, new HashMap<>())); - System.out.println("Retrieving/Creating balance for " + userName + ": " + balance.getBalanceList()); return balance; } + /** + * Retrieves the expenses paid by a user. + * + * @param payerName The name of the user to retrieve expenses for. + * @return The list of expenses paid by the user. + */ + public ArrayList getPayerExpense(String payerName) { + ArrayList payerExpenses = new ArrayList<>(); + + for (Expense expense : expenseList) { + if (expense.getPayerName().equals(payerName)) { + payerExpenses.add(expense); + } + } + + return payerExpenses; + } /** * Finds a user by their name. @@ -380,4 +449,3 @@ private double calculateAdjustedAmount(Expense expense, User payer, User payee, return 0; } } - diff --git a/src/test/java/seedu/duke/SettleTest.java b/src/test/java/seedu/duke/SettleTest.java index c6b047359a..e659d2ceba 100644 --- a/src/test/java/seedu/duke/SettleTest.java +++ b/src/test/java/seedu/duke/SettleTest.java @@ -5,78 +5,56 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ExpensesException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class SettleTest { - private User payer; - private User payee; - private Settle settle; + private Group group; @BeforeEach - void setUp() { - // Initialize your objects before each test - payer = new User("Alice"); - payee = new User("Bob"); - settle = new Settle(payer, payee, 100.0); // Assuming the amount to settle is 100.0 - } - - @Test - void testGetPayer() { - assertEquals(payer.getName(), settle.getPayer(), "Payer's name should match the one provided at creation"); - } - - @Test - void testToString() { - String expected = "Alice paid Bob 100.0"; - assertEquals(expected, settle.toString(), "toString should return a string in the format 'payerName " + - "paid payeeName amount'"); - } - - @Test - void testNegativeAmount() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, - () -> new Settle(payer, payee, -50.0), - "Constructor should throw IllegalArgumentException for negative amounts"); - assertTrue(exception.getMessage().contains("Amount cannot be negative"), - "Exception message should indicate the negative amount problem"); - } - - @Test - void testNullPayer() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, - () -> new Settle(null, payee, 50.0), - "Constructor should throw IllegalArgumentException for null payer"); - assertTrue(exception.getMessage().contains("Payer cannot be null"), - "Exception message should indicate the null payer problem"); + public void setup() throws ExpensesException { + group = new Group("Test Group"); + User payer = new User("Alice"); + User payee = new User("Bob"); + group.addMember(payer.getName()); + group.addMember(payee.getName()); + ArrayList> payees = new ArrayList<>(); + payees.add(new Pair<>(payee.getName(), 50.0f)); + Expense expense = new Expense(false, payer.getName(), "Test Expense", 100.0f, payees); + group.addExpense(expense); } @Test - void testNullPayee() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, - () -> new Settle(payer, null, 50.0), - "Constructor should throw IllegalArgumentException for null payee"); - assertTrue(exception.getMessage().contains("Payee cannot be null"), - "Exception message should indicate the null payee problem"); + public void testSettleCreation() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, 50.0); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob 50.0", settle.toString()); } @Test - void testNullPayerAndPayee() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, - () -> new Settle(null, null, 50.0), - "Constructor should throw IllegalArgumentException for null payer and payee"); - assertTrue(exception.getMessage().contains("Payer cannot be null"), - "Exception message should indicate the null payer problem"); + public void testSettleCreationWithNegativeAmount() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, -50.0); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob -50.0", settle.toString()); } @Test - void testNullPayerPayeeAndAmount() { - Exception exception = Assertions.assertThrows(IllegalArgumentException.class, - () -> new Settle(null, null, -50.0), - "Constructor should throw IllegalArgumentException for null payer, payee and negative amount"); - assertTrue(exception.getMessage().contains("Payer cannot be null"), - "Exception message should indicate the null payer problem"); + public void testSettleCreationWithZeroAmount() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, 0.0); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob 0.0", settle.toString()); } } From 447bd4415680c96311a93dc87af81c718262cc84 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 14 Apr 2024 13:38:56 +0800 Subject: [PATCH 192/270] fixed group tests --- src/test/java/seedu/duke/GroupTest.java | 70 +++++++++++++------------ 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java index 4e14931a4d..c6cfb4e23b 100644 --- a/src/test/java/seedu/duke/GroupTest.java +++ b/src/test/java/seedu/duke/GroupTest.java @@ -2,7 +2,6 @@ package seedu.duke; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,53 +18,58 @@ public void setup() { Group.currentGroupName = Optional.empty(); } - @AfterEach - public void teardown() { - // Optionally clear all after each test if needed - Group.groups.clear(); - Group.currentGroupName = Optional.empty(); + @Test + public void testGetOrCreateGroup() { + String groupName = "TestGroup"; + Optional group = Group.getOrCreateGroup(groupName); + assertTrue(group.isPresent(), "Group should be created"); + assertEquals(groupName, group.get().getGroupName(), "Group name should match the expected name"); } @Test - public void testGroupCreation() { - String expectedName = "GroupName"; - Group.groups.clear(); - Optional group = Group.getOrCreateGroup(expectedName); - assertEquals(group.isPresent(), true, "Group should be created"); - assertEquals(expectedName, group.get().getGroupName(), "Group name should match the expected name"); + public void testEnterGroup() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + assertEquals(groupName, Group.currentGroupName.get(), "Current group name should match the expected name"); } @Test - public void testGetOrCreateGroup() { - String groupName = "NewGroup"; - Group.groups.clear(); // Ensure that the group does not exist before the test - Optional newGroup = Group.getOrCreateGroup(groupName); - assertEquals(newGroup.isPresent(), true, "New group should be created successfully"); - + public void testExitGroup() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); Group.exitGroup(groupName); - Group.groups.clear(); // Ensure that the group does not exist before the test - Optional existingGroup = Group.getOrCreateGroup(groupName); - - assertTrue(existingGroup.isPresent(), "Should retrieve the existing group"); - assertEquals(newGroup.get(), existingGroup.get(), "Should return the same group object for existing group"); + assertTrue(Group.currentGroupName.isEmpty(), "Current group name should be empty"); } @Test - public void testExitGroup() { - String groupName = "ExitingGroup"; - Group.groups.clear(); // Ensure that the group does not exist before the test + public void testNullGroup() { + String groupName = "TestGroup"; Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + Group.exitGroup(groupName); Group.exitGroup(groupName); - assertTrue(Group.getCurrentGroup().isEmpty(), "Should successfully exit the group"); + assertTrue(Group.currentGroupName.isEmpty(), "Current group name should be empty"); + } + + @Test + public void testAddMember() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + Group.getCurrentGroup().get().addMember("Alice"); + assertTrue(Group.getCurrentGroup().get().isMember("Alice"), "Alice should be a member of the group"); } @Test - public void testGetCurrentGroup() { - String groupName = "CurrentGroup"; - Group.groups.clear(); // Ensure that the group does not exist before the test + public void testAddMultipleMembers() { + String groupName = "TestGroup"; Group.getOrCreateGroup(groupName); - Optional currentGroup = Group.getCurrentGroup(); - assertEquals(currentGroup.isPresent(), true, "Current group should not be empty"); - assertEquals(groupName, currentGroup.get().getGroupName(), "Current group should match the expected group"); + Group.enterGroup(groupName); + Group.getCurrentGroup().get().addMember("Alice"); + Group.getCurrentGroup().get().addMember("Bob"); + assertTrue(Group.getCurrentGroup().get().isMember("Alice"), "Alice should be a member of the group"); + assertTrue(Group.getCurrentGroup().get().isMember("Bob"), "Bob should be a member of the group"); } } \ No newline at end of file From fbd56d75abf7e46cf0ab2f782377b2fc45be60ff Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 14:12:41 +0800 Subject: [PATCH 193/270] Add diagrams folder --- docs/DeveloperGuide.md | 10 +++++----- docs/{diagrams => images}/AddMember.png | Bin docs/{diagrams => images}/addMember1.png | Bin docs/{diagrams => images}/addMember2.png | Bin docs/{diagrams => images}/addMember3.png | Bin docs/{diagrams => images}/groupStorage1.png | Bin docs/{diagrams => images}/groupStorage2.png | Bin 7 files changed, 5 insertions(+), 5 deletions(-) rename docs/{diagrams => images}/AddMember.png (100%) rename docs/{diagrams => images}/addMember1.png (100%) rename docs/{diagrams => images}/addMember2.png (100%) rename docs/{diagrams => images}/addMember3.png (100%) rename docs/{diagrams => images}/groupStorage1.png (100%) rename docs/{diagrams => images}/groupStorage2.png (100%) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index ffa4eb5953..b4cc0d62fd 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -67,7 +67,7 @@ Group -> Group: members.add(johnUser) @enduml ``` -![Sequence Diagram](diagrams/addMember1.png) +![Sequence Diagram](images/addMember1.png) Step 3. The user executes the `member Emily` command to add another member named "Emily" to the "Project Team" group. Similar to step 2, the `member` command calls `GroupCommand#addMember("Emily")`, which then calls `Group#addMember("Emily")`. After checking that "Emily" is not already a member, a new `User` object with the name "Emily" is created and added to the `members` list of the "Project Team" group. @@ -87,7 +87,7 @@ GroupCommand --> User: "John is already a member" @enduml ``` -![Sequence Diagram](diagrams/addMember2.png) +![Sequence Diagram](images/addMember2.png) The following sequence diagram illustrates the flow of the "Add Member to Group" feature: @@ -117,7 +117,7 @@ GroupCommand --> User: command result @enduml ``` -![Sequence Diagram](diagrams/addMember3.png) +![Sequence Diagram](images/addMember3.png) ### Expenses feature @@ -211,7 +211,7 @@ GroupStorage -> FileIO: writer.close() @enduml ``` -![Sequence Diagram](diagrams/groupStorage1.png) +![Sequence Diagram](images/groupStorage1.png) Step 5. Later, the user decides to enter the "Project Team" group again using the `enter Project Team` command. The `Group#enterGroup(String groupName)` method is called to enter the group. @@ -248,7 +248,7 @@ end @enduml ``` -![Sequence Diagram](diagrams/groupStorage2.png) +![Sequence Diagram](images/groupStorage2.png) The `GroupStorage#loadGroupFromFile(String groupName)` method reads the group information from the file, creates a new `Group` object, and populates it with the loaded data. This includes the group name, members, and expenses. The loaded `Group` object is then returned to the `Group` class. diff --git a/docs/diagrams/AddMember.png b/docs/images/AddMember.png similarity index 100% rename from docs/diagrams/AddMember.png rename to docs/images/AddMember.png diff --git a/docs/diagrams/addMember1.png b/docs/images/addMember1.png similarity index 100% rename from docs/diagrams/addMember1.png rename to docs/images/addMember1.png diff --git a/docs/diagrams/addMember2.png b/docs/images/addMember2.png similarity index 100% rename from docs/diagrams/addMember2.png rename to docs/images/addMember2.png diff --git a/docs/diagrams/addMember3.png b/docs/images/addMember3.png similarity index 100% rename from docs/diagrams/addMember3.png rename to docs/images/addMember3.png diff --git a/docs/diagrams/groupStorage1.png b/docs/images/groupStorage1.png similarity index 100% rename from docs/diagrams/groupStorage1.png rename to docs/images/groupStorage1.png diff --git a/docs/diagrams/groupStorage2.png b/docs/images/groupStorage2.png similarity index 100% rename from docs/diagrams/groupStorage2.png rename to docs/images/groupStorage2.png From ff62cf21bad450f9c6438dc00d6284e9e83a4663 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 14 Apr 2024 15:01:22 +0800 Subject: [PATCH 194/270] Separate argument handling into BalanceCommand class --- src/main/java/seedu/duke/Parser.java | 17 ++--------- .../seedu/duke/commands/BalanceCommand.java | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/main/java/seedu/duke/commands/BalanceCommand.java diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java index 87260f609a..a982fc58db 100644 --- a/src/main/java/seedu/duke/Parser.java +++ b/src/main/java/seedu/duke/Parser.java @@ -1,5 +1,6 @@ package seedu.duke; +import seedu.duke.commands.BalanceCommand; import seedu.duke.commands.ExpenseCommand; import seedu.duke.commands.ListCommand; import seedu.duke.exceptions.ExpensesException; @@ -195,21 +196,7 @@ public void handleUserInput() throws EndProgramException, ExpensesException { ListCommand.printList(); break; case "balance": - // Checks if user is currently in a Group - // named 'currentGroup1' to prevent conflict with previous declaration - Optional currentGroup1 = Group.getCurrentGroup(); - if (currentGroup1.isEmpty()) { - String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; - throw new ExpensesException(exceptionMessage); - } - - // Checks if user specified is in Current Group - if (!currentGroup1.get().isMember(argument)) { - String exceptionMessage = argument + " is not in current Group!"; - throw new ExpensesException(exceptionMessage); - } - Balance balance = new Balance(argument, currentGroup1.get()); - balance.printBalance(); + BalanceCommand.handleBalance(argument); break; default: System.out.println("That is not a command. " + diff --git a/src/main/java/seedu/duke/commands/BalanceCommand.java b/src/main/java/seedu/duke/commands/BalanceCommand.java new file mode 100644 index 0000000000..75321173c4 --- /dev/null +++ b/src/main/java/seedu/duke/commands/BalanceCommand.java @@ -0,0 +1,29 @@ +package seedu.duke.commands; + +import seedu.duke.Balance; +import seedu.duke.Group; +import seedu.duke.exceptions.ExpensesException; + +import java.util.Optional; + +public class BalanceCommand { + public static void handleBalance(String argument) throws ExpensesException{ + // Checks if user is currently in a Group + // named 'currentGroup1' to prevent conflict with previous declaration + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + assert currentGroup.isPresent() : "Group should be created and present"; + + // Checks if user specified is in Current Group + if (Group.isMember(argument)) { + String exceptionMessage = argument + " is not in current Group!"; + throw new ExpensesException(exceptionMessage); + } + + Balance balance = new Balance(argument, currentGroup.get()); + balance.printBalance(); + } +} From 8b64601956721aad637bec7a7a6ec12831d22ab4 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 14 Apr 2024 15:06:04 +0800 Subject: [PATCH 195/270] fixed spelling --- src/main/java/seedu/duke/Group.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index add5fa0ee7..ddfcc8b5eb 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -304,7 +304,7 @@ private void updateBalancesAfterSettlement(String payerName, String payeeName) { ArrayList payerExpense = getPayerExpense(payerName); - int balance = caluclateBalance(payerName, payeeName); + int balance = calulateBalance(payerName, payeeName); if (balance < 0) { return; @@ -328,7 +328,7 @@ private void updateBalancesAfterSettlement(String payerName, String payeeName) { * @param payeeName The name of the user who will receive the outstanding amount. * @return The balance between the two users. */ - private int caluclateBalance(String payerName, String payeeName) { + private int calulateBalance(String payerName, String payeeName) { ArrayList payeeExpense = getPayerExpense(payeeName); ArrayList payerExpense = getPayerExpense(payerName); From d0d582b66bab3b0b9e4065df64796f28c5f78eb2 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 14 Apr 2024 15:08:34 +0800 Subject: [PATCH 196/270] Update printing to new UI --- src/main/java/seedu/duke/Balance.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index d09242bc4d..a5190446ec 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -79,14 +79,14 @@ private void addExpense(Expense expense) { } public void printBalance() { - String firstLine = String.format("User %s's Balance List:", userName); - System.out.println(firstLine); + StringBuilder printOutput = new StringBuilder(); + printOutput.append(String.format("User %s's Balance List:", userName)); for (Map.Entry entry : balanceList.entrySet()) { - String balanceLine = String.format(" %s : %.2f", entry.getKey(), entry.getValue()); - System.out.println(balanceLine); + printOutput.append(String.format("\n %s : %.2f", entry.getKey(), entry.getValue())); } - System.out.println("End of Balance List"); + printOutput.append("\nEnd of Balance List"); + UserInterface.printMessage(printOutput.toString(), MessageType.SUCCESS); } } From 3809f995231261bd4052b683fce8b3abb5540448 Mon Sep 17 00:00:00 2001 From: avrilgk Date: Sun, 14 Apr 2024 15:09:21 +0800 Subject: [PATCH 197/270] fixed spelling --- src/main/java/seedu/duke/Group.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java index ddfcc8b5eb..bcf9ae63ff 100644 --- a/src/main/java/seedu/duke/Group.java +++ b/src/main/java/seedu/duke/Group.java @@ -304,7 +304,7 @@ private void updateBalancesAfterSettlement(String payerName, String payeeName) { ArrayList payerExpense = getPayerExpense(payerName); - int balance = calulateBalance(payerName, payeeName); + int balance = calculateBalance(payerName, payeeName); if (balance < 0) { return; @@ -328,7 +328,7 @@ private void updateBalancesAfterSettlement(String payerName, String payeeName) { * @param payeeName The name of the user who will receive the outstanding amount. * @return The balance between the two users. */ - private int calulateBalance(String payerName, String payeeName) { + private int calculateBalance(String payerName, String payeeName) { ArrayList payeeExpense = getPayerExpense(payeeName); ArrayList payerExpense = getPayerExpense(payerName); From 6df3306704b5697c3b996ce31db0626a34c4b632 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 14 Apr 2024 15:15:45 +0800 Subject: [PATCH 198/270] Add docs to important functions in Balance and BalanceCommand classes --- src/main/java/seedu/duke/Balance.java | 14 ++++++++++++++ .../java/seedu/duke/commands/BalanceCommand.java | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index a5190446ec..771d9662f2 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -1,5 +1,7 @@ package seedu.duke; +import seedu.duke.exceptions.ExpensesException; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,6 +19,13 @@ public Balance(String userName, Group group){ this(userName, group.getExpenseList(), group.getMembers()); } + /** + * Populates balanceList with list of Users and list of Expenses in Group. + * + * @param userName The name of User to print the balance of. + * @param expenses The list of Expenses in Group. + * @param users The list of Users in Group + */ public Balance(String userName, List expenses, List users) { this.userName = userName; this.balanceList = new HashMap<>(); @@ -42,6 +51,11 @@ public Map getBalanceList() { return balanceList; } + /** + * Adds an Expense to balanceList. + * + * @param expense The Expense to be added. + */ private void addExpense(Expense expense) { String payerName = expense.getPayerName(); List> payees = expense.getPayees(); diff --git a/src/main/java/seedu/duke/commands/BalanceCommand.java b/src/main/java/seedu/duke/commands/BalanceCommand.java index 75321173c4..9518c3b313 100644 --- a/src/main/java/seedu/duke/commands/BalanceCommand.java +++ b/src/main/java/seedu/duke/commands/BalanceCommand.java @@ -7,6 +7,13 @@ import java.util.Optional; public class BalanceCommand { + /** + * Checks if user is currently in a Group, and if User specified is in said Group. + * Creates a Balance object and prints User's balance if so. + * + * @param argument The name of User to print the balance of. + * @throws ExpensesException If user is not in a Group, or specified User is not in said Group. + */ public static void handleBalance(String argument) throws ExpensesException{ // Checks if user is currently in a Group // named 'currentGroup1' to prevent conflict with previous declaration From 7ef810ef88896b994b571dd47ca2c3149b966af2 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 14 Apr 2024 15:22:45 +0800 Subject: [PATCH 199/270] Fix Bug Fix bug where member is in group but message says otherwise --- src/main/java/seedu/duke/commands/BalanceCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/commands/BalanceCommand.java b/src/main/java/seedu/duke/commands/BalanceCommand.java index 9518c3b313..8b6dc20f78 100644 --- a/src/main/java/seedu/duke/commands/BalanceCommand.java +++ b/src/main/java/seedu/duke/commands/BalanceCommand.java @@ -25,7 +25,7 @@ public static void handleBalance(String argument) throws ExpensesException{ assert currentGroup.isPresent() : "Group should be created and present"; // Checks if user specified is in Current Group - if (Group.isMember(argument)) { + if (!currentGroup.get().isMember(argument)) { String exceptionMessage = argument + " is not in current Group!"; throw new ExpensesException(exceptionMessage); } From 4493585220ae560358a04df8c366abbe12936405 Mon Sep 17 00:00:00 2001 From: Cohii Date: Sun, 14 Apr 2024 15:39:43 +0800 Subject: [PATCH 200/270] Change Balance printing Makes it clearer which user owes which user. --- src/main/java/seedu/duke/Balance.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java index 771d9662f2..027323f926 100644 --- a/src/main/java/seedu/duke/Balance.java +++ b/src/main/java/seedu/duke/Balance.java @@ -97,7 +97,16 @@ public void printBalance() { printOutput.append(String.format("User %s's Balance List:", userName)); for (Map.Entry entry : balanceList.entrySet()) { - printOutput.append(String.format("\n %s : %.2f", entry.getKey(), entry.getValue())); + if (entry.getValue() > 0f) { + printOutput.append(String.format( + "\n %s owes %s : %.2f", entry.getKey(), userName, entry.getValue() + )); + } + else { + printOutput.append(String.format( + "\n %s owes %s : %.2f", userName, entry.getKey(), -entry.getValue() + )); + } } printOutput.append("\nEnd of Balance List"); From 1dad63fea5266ec1a655d67bd1679e343af5980e Mon Sep 17 00:00:00 2001 From: Hafizuddin Bin Aminuddin Date: Sun, 14 Apr 2024 16:42:12 +0800 Subject: [PATCH 201/270] Edit sequence diagrams --- docs/DeveloperGuide.md | 105 ------------------------------- docs/diagrams/addMember1.puml | 29 +++++++++ docs/diagrams/addMember2.puml | 22 +++++++ docs/diagrams/addMember3.puml | 50 +++++++++++++++ docs/diagrams/groupStorage1.puml | 50 +++++++++++++++ docs/diagrams/groupStorage2.puml | 70 +++++++++++++++++++++ docs/images/AddMember.png | Bin 24719 -> 0 bytes docs/images/addMember1.png | Bin 12196 -> 19222 bytes docs/images/addMember2.png | Bin 12831 -> 16056 bytes docs/images/addMember3.png | Bin 24425 -> 41734 bytes docs/images/groupStorage1.png | Bin 22619 -> 33733 bytes docs/images/groupStorage2.png | Bin 38716 -> 64888 bytes 12 files changed, 221 insertions(+), 105 deletions(-) create mode 100644 docs/diagrams/addMember1.puml create mode 100644 docs/diagrams/addMember2.puml create mode 100644 docs/diagrams/addMember3.puml create mode 100644 docs/diagrams/groupStorage1.puml create mode 100644 docs/diagrams/groupStorage2.puml delete mode 100644 docs/images/AddMember.png diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index b4cc0d62fd..c2611b6bb0 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -52,71 +52,16 @@ Step 1. The user launches the application and enters a group named "Project Team Step 2. The user executes the `member John` command to add a new member named "John" to the "Project Team" group. The `member` command calls `GroupCommand#addMember("John")`, which in turn calls `Group#addMember("John")`. This operation checks if "John" is already a member of the group using `Group#isMember("John")`. Since "John" is not a member, a new `User` object with the name "John" is created and added to the `members` list of the "Project Team" group. -```plantuml -@startuml -actor User -participant GroupCommand -participant Group -participant User - -User -> GroupCommand: member John -GroupCommand -> Group: addMember("John") -Group -> Group: isMember("John") -Group -> User: new User("John") -Group -> Group: members.add(johnUser) -@enduml -``` - ![Sequence Diagram](images/addMember1.png) Step 3. The user executes the `member Emily` command to add another member named "Emily" to the "Project Team" group. Similar to step 2, the `member` command calls `GroupCommand#addMember("Emily")`, which then calls `Group#addMember("Emily")`. After checking that "Emily" is not already a member, a new `User` object with the name "Emily" is created and added to the `members` list of the "Project Team" group. Step 4. The user tries to add "John" again to the "Project Team" group by executing the `member John` command. However, since "John" is already a member of the group, the `Group#isMember("John")` check in `Group#addMember("John")` returns `true`. As a result, an error message is displayed to the user, indicating that "John" is already a member of the group, and no duplicate member is added. -```plantuml -@startuml -actor User -participant GroupCommand -participant Group - -User -> GroupCommand: member John -GroupCommand -> Group: addMember("John") -Group -> Group: isMember("John") -Group --> GroupCommand: "John is already a member" -GroupCommand --> User: "John is already a member" -@enduml -``` - ![Sequence Diagram](images/addMember2.png) The following sequence diagram illustrates the flow of the "Add Member to Group" feature: -```plantuml -@startuml -actor User -participant GroupCommand -participant Group -participant User - -User -> GroupCommand: member USER_NAME -GroupCommand -> Group: addMember(memberName) -Group -> Group: isValidMemberName(memberName) -alt is valid member name - Group -> Group: isMember(memberName) - alt is not a member - Group -> User: new User(memberName) - Group -> Group: members.add(newMember) - Group --> GroupCommand: success message - else is already a member - Group --> GroupCommand: failure message - end -else is invalid member name - Group --> GroupCommand: failure message -end -GroupCommand --> User: command result -@enduml -``` - ![Sequence Diagram](images/addMember3.png) @@ -192,62 +137,12 @@ Step 3. The user executes various commands to add members and expenses to the "P Step 4. The user executes the `exit Project Team` command to exit the "Project Team" group. This command invokes the `Group#exitGroup(String groupName)` method, which in turn calls the `GroupStorage#saveGroupToFile(Group group)` method to save the current state of the "Project Team" group to a file. The saving process includes writing the group name, members, and expenses to the file in a structured format. -```plantuml -@startuml -actor User -participant GroupCommand -participant Group -participant GroupStorage -participant FileIO - -User -> GroupCommand: exit Project Team -GroupCommand -> Group: exitGroup("Project Team") -Group -> GroupStorage: saveGroupToFile(projectTeamGroup) -GroupStorage -> FileIO: getFileWriter(filePath) -GroupStorage -> GroupStorage: saveGroupName(writer, groupName) -GroupStorage -> GroupStorage: saveMembers(writer, members) -GroupStorage -> GroupStorage: saveExpenses(writer, expenses) -GroupStorage -> FileIO: writer.close() -@enduml -``` - ![Sequence Diagram](images/groupStorage1.png) Step 5. Later, the user decides to enter the "Project Team" group again using the `enter Project Team` command. The `Group#enterGroup(String groupName)` method is called to enter the group. Step 6. Inside the `Group#enterGroup(String groupName)` method, it first checks if the group exists in memory. If not, it uses the `GroupNameChecker` class to check if the group file exists. If the group file exists, it invokes the `GroupStorage#loadGroupFromFile(String groupName)` method to load the group information from the file. -```plantuml -@startuml -actor User -participant GroupCommand -participant Group -participant GroupNameChecker -participant GroupStorage -participant FileIO - -User -> GroupCommand: enter Project Team -GroupCommand -> Group: enterGroup("Project Team") -Group -> Group: check if group exists in memory -alt group does not exist in memory - Group -> GroupNameChecker: doesGroupNameExist("Project Team") - alt group file exists - Group -> GroupStorage: loadGroupFromFile("Project Team") - GroupStorage -> FileIO: getFileReader(filePath) - GroupStorage -> GroupStorage: loadGroupName(reader) - GroupStorage -> GroupStorage: loadMembers(reader, group) - GroupStorage -> GroupStorage: loadExpenses(reader, group) - GroupStorage -> FileIO: reader.close() - GroupStorage --> Group: loadedGroup - else group file does not exist - Group --> GroupCommand: group does not exist - end -else group exists in memory - Group --> GroupCommand: group found in memory -end -@enduml -``` - ![Sequence Diagram](images/groupStorage2.png) The `GroupStorage#loadGroupFromFile(String groupName)` method reads the group information from the file, creates a new `Group` object, and populates it with the loaded data. This includes the group name, members, and expenses. The loaded `Group` object is then returned to the `Group` class. diff --git a/docs/diagrams/addMember1.puml b/docs/diagrams/addMember1.puml new file mode 100644 index 0000000000..b938a3bdbe --- /dev/null +++ b/docs/diagrams/addMember1.puml @@ -0,0 +1,29 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":User" as JohnUser + +User -> GroupCommand: member John +activate GroupCommand +GroupCommand -> Group: addMember("John") +activate Group + +Group -> Group: isMember("John") +activate Group +deactivate Group + +Group -> JohnUser: new User("John") +activate JohnUser +JohnUser --> Group: johnUser +deactivate JohnUser + +Group -> Group: members.add(johnUser) +activate Group +deactivate Group + +Group --> GroupCommand +deactivate Group +GroupCommand --> User +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/addMember2.puml b/docs/diagrams/addMember2.puml new file mode 100644 index 0000000000..f919eed964 --- /dev/null +++ b/docs/diagrams/addMember2.puml @@ -0,0 +1,22 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group + +User -> GroupCommand: member John +activate GroupCommand + +GroupCommand -> Group: addMember("John") +activate Group + +Group -> Group: isMember("John") +activate Group +Group --> Group: true +deactivate Group + +Group --> GroupCommand: "John is already a member" +deactivate Group + +GroupCommand --> User: "John is already a member" +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/addMember3.puml b/docs/diagrams/addMember3.puml new file mode 100644 index 0000000000..ae838c1942 --- /dev/null +++ b/docs/diagrams/addMember3.puml @@ -0,0 +1,50 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":User" as NewUser + +User -> GroupCommand: member USER_NAME +activate GroupCommand + +GroupCommand -> Group: addMember(memberName) +activate Group + +Group -> Group: isValidMemberName(memberName) +activate Group +alt is valid member name + Group -> Group: isMember(memberName) + activate Group + alt is not a member + Group --> Group: false + deactivate Group + + Group -> NewUser: new User(memberName) + activate NewUser + NewUser --> Group: newUser + deactivate NewUser + + Group -> Group: members.add(newUser) + activate Group + deactivate Group + + Group --> GroupCommand: success message + deactivate Group + else is already a member + Group --> Group: true + deactivate Group + + Group --> GroupCommand: failure message + deactivate Group + end +else is invalid member name + Group --> Group: false + deactivate Group + + Group --> GroupCommand: failure message + deactivate Group +end + +GroupCommand --> User: command result +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/groupStorage1.puml b/docs/diagrams/groupStorage1.puml new file mode 100644 index 0000000000..ae838c1942 --- /dev/null +++ b/docs/diagrams/groupStorage1.puml @@ -0,0 +1,50 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":User" as NewUser + +User -> GroupCommand: member USER_NAME +activate GroupCommand + +GroupCommand -> Group: addMember(memberName) +activate Group + +Group -> Group: isValidMemberName(memberName) +activate Group +alt is valid member name + Group -> Group: isMember(memberName) + activate Group + alt is not a member + Group --> Group: false + deactivate Group + + Group -> NewUser: new User(memberName) + activate NewUser + NewUser --> Group: newUser + deactivate NewUser + + Group -> Group: members.add(newUser) + activate Group + deactivate Group + + Group --> GroupCommand: success message + deactivate Group + else is already a member + Group --> Group: true + deactivate Group + + Group --> GroupCommand: failure message + deactivate Group + end +else is invalid member name + Group --> Group: false + deactivate Group + + Group --> GroupCommand: failure message + deactivate Group +end + +GroupCommand --> User: command result +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/groupStorage2.puml b/docs/diagrams/groupStorage2.puml new file mode 100644 index 0000000000..42fc5c541a --- /dev/null +++ b/docs/diagrams/groupStorage2.puml @@ -0,0 +1,70 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":GroupNameChecker" as GroupNameChecker +participant ":GroupStorage" as GroupStorage +participant ":FileIO" as FileIO + +User -> GroupCommand: enter Project Team +activate GroupCommand + +GroupCommand -> Group: enterGroup("Project Team") +activate Group + +Group -> Group: check if group exists in memory +activate Group +alt group does not exist in memory + Group --> Group: false + deactivate Group + + Group -> GroupNameChecker: doesGroupNameExist("Project Team") + activate GroupNameChecker + alt group file exists + GroupNameChecker --> Group: true + deactivate GroupNameChecker + + Group -> GroupStorage: loadGroupFromFile("Project Team") + activate GroupStorage + + GroupStorage -> FileIO: getFileReader(filePath) + activate FileIO + FileIO --> GroupStorage: reader + deactivate FileIO + + GroupStorage -> GroupStorage: loadGroupName(reader) + activate GroupStorage + deactivate GroupStorage + + GroupStorage -> GroupStorage: loadMembers(reader, group) + activate GroupStorage + deactivate GroupStorage + + GroupStorage -> GroupStorage: loadExpenses(reader, group) + activate GroupStorage + deactivate GroupStorage + + GroupStorage -> FileIO: reader.close() + activate FileIO + deactivate FileIO + + GroupStorage --> Group: loadedGroup + deactivate GroupStorage + else group file does not exist + GroupNameChecker --> Group: false + deactivate GroupNameChecker + + Group --> GroupCommand: group does not exist + deactivate Group + end +else group exists in memory + Group --> Group: true + deactivate Group + + Group --> GroupCommand: group found in memory + deactivate Group +end + +GroupCommand --> User +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/images/AddMember.png b/docs/images/AddMember.png deleted file mode 100644 index e9fa667714a613f9063b847cd71736620c712149..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24719 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 diff --git a/docs/images/addMember1.png b/docs/images/addMember1.png index 028941b9d48fc9610e527bda56d3ccce14efef84..efb608b885d0a012c2e1557366494a90160a69d2 100644 GIT binary patch literal 19222 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 DlDU)f literal 12196 zcmd6NbySpH`|co$3eqs3fNu{RI4Cc9eoU8r!{awKje32$3=t?&bGT?2nGvvxZ~f?Ay{CXSBXyVJCvF@h~8|RkmY}jkVrq5B;Up&*Ulj$ zdHS@sCLtD~UQFAA7(Y5kW{fy7}I&x0ixx)QeyqVFMwWeNjjqCLs_a8 z+?ej?#XG#rE^(LX&3ziiZpJcW;j)d(3!UstDg#bEYCS1UI9t~r(K@>0FUntuI9|2W zyqZ{PPFfT!Lb)_WIUl^de$~)($}CDMR3@ODo{Ax|hfcBAoK|>(mq3P_DjOk49!VTs zjJLW8Jjlqq6rYURNKt>U1s z%geS%G@|0lR|Lf8bymi#rFy?v9{E7`i;GC*O3Uqz7vKw1uGF3FzrhXHtM_tQ8TdXu zjZh5sMMEGXzq~K1R@1}ys2;^Khz=X5^?mX7b~aypkgJ|&^bkx)EFn?oz}Unj1Wr|J z(kjZhwUnLwAu4J&=Q8Fs@cJhxFjEBU1n~jGLBI+$VBQ1s0kdBmhplaGBSlz^jg9fv z(wU85pT2<~2&-1K zJN3g)hZv)(c(}N5;-|P7loAEpc5}oZ_#30q=-Mhu|LKw-XpFwM0{8p3=pJ!b(Is^w z+zIhKRX(o_&MZlq*I=dKH=r}@dwY8b6ddhw(fsbVj*gBI=BXe0@Y+%kUFLBaMVvyV z?YN+#Nb;W!HyG(2^peWSuSKios?(7)g1v#^99{_u+RgaYoSB&^F+{AJ87jUPv_X@%gjzpP3IaB9%Zouqt{{0%3yc6 zxVYSEg`Du$&(9}x6lkhvW_IF>H^x?1H<+1kv9M%jE9s3@t~t24guS*1<>l>A%dR_~ zPq^?r-bIg$=uognd+z-_*VK>0L2k7nwCxy5C`IpM?bX@@`up?p^B+tg68L30l1>cr z+#j4>tt_D|&dzR`la%s&Zfwl&@k!+DUFbw=8etigJRM!e=TDz{FE+1}TF*>J4DqcD z2A-T;%@M#N4I*XZlZB0RFXIy~*V1O2k;%X#Hi zaV=kcMn?47$7pB6<$g`Z$$m%c7n?eoMY6MFQL-@-3({BC$ByRiRsLB430Yy9@SKfF ziw~_PbfraiZ7nT1H3wTKz1*dpRuVy2=bovl?mvAZ{9-C$d)r`zqKP9Xb!gpHgE$Wyye6z0f!;2)sy@dE+;p4SqwDY+wJF1#XOyb*2pW?AEdo=bK|z7w{~}XGn`7L74SYx z&GGs@38OT6v1gf7nw~F0)%x~{+I8G~ll(h2S(|4z%TCq@qzs*?7_al{k)feCPa^Tj z*(`~46js(QL;D`{R8Y#jxkTju0Nwb^%E}I<*%IRVSYI$Ii%L=3%wh@usMdTnD+sNA zcfI+Y`*w>ee68m>1)FXrab#}pO_#N%0DR-y{XZ)l-Jd#M$EDzX%bK<4j5)jVtkSf( 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 Qn6(lI_uE^Po1cvSH)yW|82|tP diff --git a/docs/images/groupStorage1.png b/docs/images/groupStorage1.png index a69b66be717661c65991aa0291c2ce0af38b3ce0..a4538ff7a7455b0fcd5654392ce4b1ca434654fc 100644 GIT binary patch literal 33733 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