diff --git a/TestCSV/Linux/empty/category-store.csv b/TestCSV/Linux/empty/category-store.csv new file mode 100644 index 0000000000..df75801c5a --- /dev/null +++ b/TestCSV/Linux/empty/category-store.csv @@ -0,0 +1,5 @@ +Name +food + +transport +toy diff --git a/TestCSV/Linux/empty/expense-store.csv b/TestCSV/Linux/empty/expense-store.csv new file mode 100644 index 0000000000..ac22da15cc --- /dev/null +++ b/TestCSV/Linux/empty/expense-store.csv @@ -0,0 +1,10 @@ +Description,Amount,Date,Category,Recurrence,Has Next Reccurence +buy dinner,15,29/10/2023,food,daily,FALSE +popmart,12,29/10/2023,toy,none,FALSE +,20,29/10/2023,transport,none,FALSE +grab,,29/10/2023,transport,none,FALSE +grab,20,,transport,none,FALSE +grab,20,29/10/2023,,none,FALSE +grab,20,29/10/2023,transport,,FALSE +grab,20,29/10/2023,transport,none, +grab,20,29/10/2023,transport,none,FALSE diff --git a/TestCSV/Linux/empty/goal-store.csv b/TestCSV/Linux/empty/goal-store.csv new file mode 100644 index 0000000000..82fc41a4c8 --- /dev/null +++ b/TestCSV/Linux/empty/goal-store.csv @@ -0,0 +1,5 @@ +Description,Amount +car,1000 +,1000 +ps5, +ps5,1000 diff --git a/TestCSV/Linux/empty/income-store.csv b/TestCSV/Linux/empty/income-store.csv new file mode 100644 index 0000000000..b9854f568a --- /dev/null +++ b/TestCSV/Linux/empty/income-store.csv @@ -0,0 +1,10 @@ +Description,Amount,Date,Goal,Recurrence,Has Next Recurrence +part-time job,1000,29/10/2023,car,none,FALSE +allowance,500,29/10/2023,car,monthly,FALSE +,50,29/10/2023,ps5,none,FALSE +sell stuff,,29/10/2023,ps5,none,FALSE +sell stuff,50,,ps5,none,FALSE +sell stuff,50,29/10/2023,,none,FALSE +sell stuff,50,29/10/2023,ps5,,FALSE +sell stuff,50,29/10/2023,ps5,none, +sell stuff,50,29/10/2023,ps5,none,FALSE diff --git a/TestCSV/Linux/error/category-store.csv b/TestCSV/Linux/error/category-store.csv new file mode 100644 index 0000000000..0f3cb2e86d --- /dev/null +++ b/TestCSV/Linux/error/category-store.csv @@ -0,0 +1,5 @@ +"Name" +"food" +"toy" +"toy" +"transport" diff --git a/TestCSV/Linux/error/expense-store.csv b/TestCSV/Linux/error/expense-store.csv new file mode 100644 index 0000000000..05937a6cf2 --- /dev/null +++ b/TestCSV/Linux/error/expense-store.csv @@ -0,0 +1,8 @@ +Description,Amount,Date,Category,Recurrence,Has Next Reccurence +buy dinner,15,29/10/2023,food,daily,FALSE +popmart,12,29/10/2023,toy,none,FALSE +grab,-100,29/10/2023,transport,none,FALSE +grab,20,asdas,transport,none,FALSE +grab,asdasd,29/10/2023,transport,none,FALSE +grab,20,29/10/2023,transport,none,asdasds +grab,20,29/10/2023,transport,none,FALSE diff --git a/TestCSV/Linux/error/goal-store.csv b/TestCSV/Linux/error/goal-store.csv new file mode 100644 index 0000000000..7dc4f43c58 --- /dev/null +++ b/TestCSV/Linux/error/goal-store.csv @@ -0,0 +1,5 @@ +Description,Amount +car,1000 +ps5,-1203123 +ps5,1000 +ps5,1500 diff --git a/TestCSV/Linux/error/income-store.csv b/TestCSV/Linux/error/income-store.csv new file mode 100644 index 0000000000..ee413c3665 --- /dev/null +++ b/TestCSV/Linux/error/income-store.csv @@ -0,0 +1,8 @@ +Description,Amount,Date,Goal,Recurrence,Has Next Recurrence +part-time job,1000,29/10/2023,car,none,FALSE +allowance,500,29/10/2023,car,monthly,FALSE +sell stuff,-1,29/10/2023,ps5,none,FALSE +sell stuff,50,asdasd,ps5,none,FALSE +sell stuff,asdasd,29/10/2023,ps5,none,FALSE +sell stuff,50,29/10/2023,ps5,none,asdasdasd +sell stuff,50,29/10/2023,ps5,none,FALSE diff --git a/TestCSV/Linux/valid/Transactions-all.csv b/TestCSV/Linux/valid/Transactions-all.csv new file mode 100644 index 0000000000..7dc6960e11 --- /dev/null +++ b/TestCSV/Linux/valid/Transactions-all.csv @@ -0,0 +1,7 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Income","part-time job","2023-10-29","1000.00","car",,"none" +"Income","allowance","2023-10-29","500.00","car",,"none" +"Income","sell stuff","2023-10-29","50.00","ps5",,"none" +"Expense","buy dinner","2023-10-29","15.00",,"food","monthly" +"Expense","popmart","2023-10-29","12.00",,"toy","none" +"Expense","grab","2023-10-29","20.00",,"transport","none" diff --git a/TestCSV/Linux/valid/Transactions-in.csv b/TestCSV/Linux/valid/Transactions-in.csv new file mode 100644 index 0000000000..687cdcd1a8 --- /dev/null +++ b/TestCSV/Linux/valid/Transactions-in.csv @@ -0,0 +1,4 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Income","part-time job","2023-10-29","1000.00","car",,"none" +"Income","allowance","2023-10-29","500.00","car",,"none" +"Income","sell stuff","2023-10-29","50.00","ps5",,"none" diff --git a/TestCSV/Linux/valid/Transactions-out.csv b/TestCSV/Linux/valid/Transactions-out.csv new file mode 100644 index 0000000000..f1a9b6e7a1 --- /dev/null +++ b/TestCSV/Linux/valid/Transactions-out.csv @@ -0,0 +1,4 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Expense","buy dinner","2023-10-29","15.00",,"food","monthly" +"Expense","popmart","2023-10-29","12.00",,"toy","none" +"Expense","grab","2023-10-29","20.00",,"transport","none" diff --git a/TestCSV/Linux/valid/category-store.csv b/TestCSV/Linux/valid/category-store.csv new file mode 100644 index 0000000000..3ebdbbb422 --- /dev/null +++ b/TestCSV/Linux/valid/category-store.csv @@ -0,0 +1,4 @@ +"Name" +"food" +"toy" +"transport" diff --git a/TestCSV/Linux/valid/expense-store.csv b/TestCSV/Linux/valid/expense-store.csv new file mode 100644 index 0000000000..5b32cb74a1 --- /dev/null +++ b/TestCSV/Linux/valid/expense-store.csv @@ -0,0 +1,4 @@ +"Description","Amount","Date","Category","Recurrence","Has Next Recurrence" +"buy dinner","15.0","29/10/2023","food","monthly","false" +"popmart","12.0","29/10/2023","toy","none","false" +"grab","20.0","29/10/2023","transport","none","false" diff --git a/TestCSV/Linux/valid/goal-store.csv b/TestCSV/Linux/valid/goal-store.csv new file mode 100644 index 0000000000..0eeb5d5fc8 --- /dev/null +++ b/TestCSV/Linux/valid/goal-store.csv @@ -0,0 +1,3 @@ +"Description","Amount" +"car","1000.0" +"ps5","1000.0" diff --git a/TestCSV/Linux/valid/income-store.csv b/TestCSV/Linux/valid/income-store.csv new file mode 100644 index 0000000000..c682d4b986 --- /dev/null +++ b/TestCSV/Linux/valid/income-store.csv @@ -0,0 +1,4 @@ +"Description","Amount","Date","Goal","Recurrence","Has Next Recurrence" +"part-time job","1000.0","29/10/2023","car","none","false" +"allowance","500.0","29/10/2023","car","monthly","false" +"sell stuff","50.0","29/10/2023","ps5","none","false" diff --git a/TestCSV/MacOS/empty/category-store.csv b/TestCSV/MacOS/empty/category-store.csv new file mode 100644 index 0000000000..df75801c5a --- /dev/null +++ b/TestCSV/MacOS/empty/category-store.csv @@ -0,0 +1,5 @@ +Name +food + +transport +toy diff --git a/TestCSV/MacOS/empty/expense-store.csv b/TestCSV/MacOS/empty/expense-store.csv new file mode 100644 index 0000000000..ac22da15cc --- /dev/null +++ b/TestCSV/MacOS/empty/expense-store.csv @@ -0,0 +1,10 @@ +Description,Amount,Date,Category,Recurrence,Has Next Reccurence +buy dinner,15,29/10/2023,food,daily,FALSE +popmart,12,29/10/2023,toy,none,FALSE +,20,29/10/2023,transport,none,FALSE +grab,,29/10/2023,transport,none,FALSE +grab,20,,transport,none,FALSE +grab,20,29/10/2023,,none,FALSE +grab,20,29/10/2023,transport,,FALSE +grab,20,29/10/2023,transport,none, +grab,20,29/10/2023,transport,none,FALSE diff --git a/TestCSV/MacOS/empty/goal-store.csv b/TestCSV/MacOS/empty/goal-store.csv new file mode 100644 index 0000000000..82fc41a4c8 --- /dev/null +++ b/TestCSV/MacOS/empty/goal-store.csv @@ -0,0 +1,5 @@ +Description,Amount +car,1000 +,1000 +ps5, +ps5,1000 diff --git a/TestCSV/MacOS/empty/income-store.csv b/TestCSV/MacOS/empty/income-store.csv new file mode 100644 index 0000000000..b9854f568a --- /dev/null +++ b/TestCSV/MacOS/empty/income-store.csv @@ -0,0 +1,10 @@ +Description,Amount,Date,Goal,Recurrence,Has Next Recurrence +part-time job,1000,29/10/2023,car,none,FALSE +allowance,500,29/10/2023,car,monthly,FALSE +,50,29/10/2023,ps5,none,FALSE +sell stuff,,29/10/2023,ps5,none,FALSE +sell stuff,50,,ps5,none,FALSE +sell stuff,50,29/10/2023,,none,FALSE +sell stuff,50,29/10/2023,ps5,,FALSE +sell stuff,50,29/10/2023,ps5,none, +sell stuff,50,29/10/2023,ps5,none,FALSE diff --git a/TestCSV/MacOS/error/category-store.csv b/TestCSV/MacOS/error/category-store.csv new file mode 100644 index 0000000000..0f3cb2e86d --- /dev/null +++ b/TestCSV/MacOS/error/category-store.csv @@ -0,0 +1,5 @@ +"Name" +"food" +"toy" +"toy" +"transport" diff --git a/TestCSV/MacOS/error/expense-store.csv b/TestCSV/MacOS/error/expense-store.csv new file mode 100644 index 0000000000..05937a6cf2 --- /dev/null +++ b/TestCSV/MacOS/error/expense-store.csv @@ -0,0 +1,8 @@ +Description,Amount,Date,Category,Recurrence,Has Next Reccurence +buy dinner,15,29/10/2023,food,daily,FALSE +popmart,12,29/10/2023,toy,none,FALSE +grab,-100,29/10/2023,transport,none,FALSE +grab,20,asdas,transport,none,FALSE +grab,asdasd,29/10/2023,transport,none,FALSE +grab,20,29/10/2023,transport,none,asdasds +grab,20,29/10/2023,transport,none,FALSE diff --git a/TestCSV/MacOS/error/goal-store.csv b/TestCSV/MacOS/error/goal-store.csv new file mode 100644 index 0000000000..7dc4f43c58 --- /dev/null +++ b/TestCSV/MacOS/error/goal-store.csv @@ -0,0 +1,5 @@ +Description,Amount +car,1000 +ps5,-1203123 +ps5,1000 +ps5,1500 diff --git a/TestCSV/MacOS/error/income-store.csv b/TestCSV/MacOS/error/income-store.csv new file mode 100644 index 0000000000..ee413c3665 --- /dev/null +++ b/TestCSV/MacOS/error/income-store.csv @@ -0,0 +1,8 @@ +Description,Amount,Date,Goal,Recurrence,Has Next Recurrence +part-time job,1000,29/10/2023,car,none,FALSE +allowance,500,29/10/2023,car,monthly,FALSE +sell stuff,-1,29/10/2023,ps5,none,FALSE +sell stuff,50,asdasd,ps5,none,FALSE +sell stuff,asdasd,29/10/2023,ps5,none,FALSE +sell stuff,50,29/10/2023,ps5,none,asdasdasd +sell stuff,50,29/10/2023,ps5,none,FALSE diff --git a/TestCSV/MacOS/valid/Transactions-all.csv b/TestCSV/MacOS/valid/Transactions-all.csv new file mode 100644 index 0000000000..7dc6960e11 --- /dev/null +++ b/TestCSV/MacOS/valid/Transactions-all.csv @@ -0,0 +1,7 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Income","part-time job","2023-10-29","1000.00","car",,"none" +"Income","allowance","2023-10-29","500.00","car",,"none" +"Income","sell stuff","2023-10-29","50.00","ps5",,"none" +"Expense","buy dinner","2023-10-29","15.00",,"food","monthly" +"Expense","popmart","2023-10-29","12.00",,"toy","none" +"Expense","grab","2023-10-29","20.00",,"transport","none" diff --git a/TestCSV/MacOS/valid/Transactions-in.csv b/TestCSV/MacOS/valid/Transactions-in.csv new file mode 100644 index 0000000000..687cdcd1a8 --- /dev/null +++ b/TestCSV/MacOS/valid/Transactions-in.csv @@ -0,0 +1,4 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Income","part-time job","2023-10-29","1000.00","car",,"none" +"Income","allowance","2023-10-29","500.00","car",,"none" +"Income","sell stuff","2023-10-29","50.00","ps5",,"none" diff --git a/TestCSV/MacOS/valid/Transactions-out.csv b/TestCSV/MacOS/valid/Transactions-out.csv new file mode 100644 index 0000000000..f1a9b6e7a1 --- /dev/null +++ b/TestCSV/MacOS/valid/Transactions-out.csv @@ -0,0 +1,4 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Expense","buy dinner","2023-10-29","15.00",,"food","monthly" +"Expense","popmart","2023-10-29","12.00",,"toy","none" +"Expense","grab","2023-10-29","20.00",,"transport","none" diff --git a/TestCSV/MacOS/valid/category-store.csv b/TestCSV/MacOS/valid/category-store.csv new file mode 100644 index 0000000000..3ebdbbb422 --- /dev/null +++ b/TestCSV/MacOS/valid/category-store.csv @@ -0,0 +1,4 @@ +"Name" +"food" +"toy" +"transport" diff --git a/TestCSV/MacOS/valid/expense-store.csv b/TestCSV/MacOS/valid/expense-store.csv new file mode 100644 index 0000000000..5b32cb74a1 --- /dev/null +++ b/TestCSV/MacOS/valid/expense-store.csv @@ -0,0 +1,4 @@ +"Description","Amount","Date","Category","Recurrence","Has Next Recurrence" +"buy dinner","15.0","29/10/2023","food","monthly","false" +"popmart","12.0","29/10/2023","toy","none","false" +"grab","20.0","29/10/2023","transport","none","false" diff --git a/TestCSV/MacOS/valid/goal-store.csv b/TestCSV/MacOS/valid/goal-store.csv new file mode 100644 index 0000000000..0eeb5d5fc8 --- /dev/null +++ b/TestCSV/MacOS/valid/goal-store.csv @@ -0,0 +1,3 @@ +"Description","Amount" +"car","1000.0" +"ps5","1000.0" diff --git a/TestCSV/MacOS/valid/income-store.csv b/TestCSV/MacOS/valid/income-store.csv new file mode 100644 index 0000000000..c682d4b986 --- /dev/null +++ b/TestCSV/MacOS/valid/income-store.csv @@ -0,0 +1,4 @@ +"Description","Amount","Date","Goal","Recurrence","Has Next Recurrence" +"part-time job","1000.0","29/10/2023","car","none","false" +"allowance","500.0","29/10/2023","car","monthly","false" +"sell stuff","50.0","29/10/2023","ps5","none","false" diff --git a/TestCSV/Windows/empty/category-store.csv b/TestCSV/Windows/empty/category-store.csv new file mode 100644 index 0000000000..df75801c5a --- /dev/null +++ b/TestCSV/Windows/empty/category-store.csv @@ -0,0 +1,5 @@ +Name +food + +transport +toy diff --git a/TestCSV/Windows/empty/expense-store.csv b/TestCSV/Windows/empty/expense-store.csv new file mode 100644 index 0000000000..ac22da15cc --- /dev/null +++ b/TestCSV/Windows/empty/expense-store.csv @@ -0,0 +1,10 @@ +Description,Amount,Date,Category,Recurrence,Has Next Reccurence +buy dinner,15,29/10/2023,food,daily,FALSE +popmart,12,29/10/2023,toy,none,FALSE +,20,29/10/2023,transport,none,FALSE +grab,,29/10/2023,transport,none,FALSE +grab,20,,transport,none,FALSE +grab,20,29/10/2023,,none,FALSE +grab,20,29/10/2023,transport,,FALSE +grab,20,29/10/2023,transport,none, +grab,20,29/10/2023,transport,none,FALSE diff --git a/TestCSV/Windows/empty/goal-store.csv b/TestCSV/Windows/empty/goal-store.csv new file mode 100644 index 0000000000..82fc41a4c8 --- /dev/null +++ b/TestCSV/Windows/empty/goal-store.csv @@ -0,0 +1,5 @@ +Description,Amount +car,1000 +,1000 +ps5, +ps5,1000 diff --git a/TestCSV/Windows/empty/income-store.csv b/TestCSV/Windows/empty/income-store.csv new file mode 100644 index 0000000000..b9854f568a --- /dev/null +++ b/TestCSV/Windows/empty/income-store.csv @@ -0,0 +1,10 @@ +Description,Amount,Date,Goal,Recurrence,Has Next Recurrence +part-time job,1000,29/10/2023,car,none,FALSE +allowance,500,29/10/2023,car,monthly,FALSE +,50,29/10/2023,ps5,none,FALSE +sell stuff,,29/10/2023,ps5,none,FALSE +sell stuff,50,,ps5,none,FALSE +sell stuff,50,29/10/2023,,none,FALSE +sell stuff,50,29/10/2023,ps5,,FALSE +sell stuff,50,29/10/2023,ps5,none, +sell stuff,50,29/10/2023,ps5,none,FALSE diff --git a/TestCSV/Windows/error/category-store.csv b/TestCSV/Windows/error/category-store.csv new file mode 100644 index 0000000000..0f3cb2e86d --- /dev/null +++ b/TestCSV/Windows/error/category-store.csv @@ -0,0 +1,5 @@ +"Name" +"food" +"toy" +"toy" +"transport" diff --git a/TestCSV/Windows/error/expense-store.csv b/TestCSV/Windows/error/expense-store.csv new file mode 100644 index 0000000000..05937a6cf2 --- /dev/null +++ b/TestCSV/Windows/error/expense-store.csv @@ -0,0 +1,8 @@ +Description,Amount,Date,Category,Recurrence,Has Next Reccurence +buy dinner,15,29/10/2023,food,daily,FALSE +popmart,12,29/10/2023,toy,none,FALSE +grab,-100,29/10/2023,transport,none,FALSE +grab,20,asdas,transport,none,FALSE +grab,asdasd,29/10/2023,transport,none,FALSE +grab,20,29/10/2023,transport,none,asdasds +grab,20,29/10/2023,transport,none,FALSE diff --git a/TestCSV/Windows/error/goal-store.csv b/TestCSV/Windows/error/goal-store.csv new file mode 100644 index 0000000000..7dc4f43c58 --- /dev/null +++ b/TestCSV/Windows/error/goal-store.csv @@ -0,0 +1,5 @@ +Description,Amount +car,1000 +ps5,-1203123 +ps5,1000 +ps5,1500 diff --git a/TestCSV/Windows/error/income-store.csv b/TestCSV/Windows/error/income-store.csv new file mode 100644 index 0000000000..ee413c3665 --- /dev/null +++ b/TestCSV/Windows/error/income-store.csv @@ -0,0 +1,8 @@ +Description,Amount,Date,Goal,Recurrence,Has Next Recurrence +part-time job,1000,29/10/2023,car,none,FALSE +allowance,500,29/10/2023,car,monthly,FALSE +sell stuff,-1,29/10/2023,ps5,none,FALSE +sell stuff,50,asdasd,ps5,none,FALSE +sell stuff,asdasd,29/10/2023,ps5,none,FALSE +sell stuff,50,29/10/2023,ps5,none,asdasdasd +sell stuff,50,29/10/2023,ps5,none,FALSE diff --git a/TestCSV/Windows/valid/Transactions-all.csv b/TestCSV/Windows/valid/Transactions-all.csv new file mode 100644 index 0000000000..50618275b4 --- /dev/null +++ b/TestCSV/Windows/valid/Transactions-all.csv @@ -0,0 +1,7 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Income","part-time job","2023-10-29","1000.00","car",,"none" +"Income","allowance","2023-10-29","500.00","car",,"none" +"Income","sell stuff","2023-10-29","50.00","ps5",,"none" +"Expense","buy dinner","2023-10-29","15.00",,"food","monthly" +"Expense","popmart","2023-10-29","12.00",,"toy","none" +"Expense","grab","2023-10-29","20.00",,"transport","none" diff --git a/TestCSV/Windows/valid/Transactions-in.csv b/TestCSV/Windows/valid/Transactions-in.csv new file mode 100644 index 0000000000..ff35aa5f2b --- /dev/null +++ b/TestCSV/Windows/valid/Transactions-in.csv @@ -0,0 +1,4 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Income","part-time job","2023-10-29","1000.00","car",,"none" +"Income","allowance","2023-10-29","500.00","car",,"none" +"Income","sell stuff","2023-10-29","50.00","ps5",,"none" diff --git a/TestCSV/Windows/valid/Transactions-out.csv b/TestCSV/Windows/valid/Transactions-out.csv new file mode 100644 index 0000000000..fb9998b915 --- /dev/null +++ b/TestCSV/Windows/valid/Transactions-out.csv @@ -0,0 +1,4 @@ +"Type","Description","Date","Amount","Goal","Category","Recurrence" +"Expense","buy dinner","2023-10-29","15.00",,"food","monthly" +"Expense","popmart","2023-10-29","12.00",,"toy","none" +"Expense","grab","2023-10-29","20.00",,"transport","none" diff --git a/TestCSV/Windows/valid/category-store.csv b/TestCSV/Windows/valid/category-store.csv new file mode 100644 index 0000000000..5e7c107e0f --- /dev/null +++ b/TestCSV/Windows/valid/category-store.csv @@ -0,0 +1,4 @@ +"Name" +"food" +"toy" +"transport" diff --git a/TestCSV/Windows/valid/expense-store.csv b/TestCSV/Windows/valid/expense-store.csv new file mode 100644 index 0000000000..1f642f697f --- /dev/null +++ b/TestCSV/Windows/valid/expense-store.csv @@ -0,0 +1,4 @@ +"Description","Amount","Date","Category","Recurrence","Has Next Recurrence" +"buy dinner","15.0","29/10/2023","food","monthly","false" +"popmart","12.0","29/10/2023","toy","none","false" +"grab","20.0","29/10/2023","transport","none","false" diff --git a/TestCSV/Windows/valid/goal-store.csv b/TestCSV/Windows/valid/goal-store.csv new file mode 100644 index 0000000000..44c2c352ef --- /dev/null +++ b/TestCSV/Windows/valid/goal-store.csv @@ -0,0 +1,3 @@ +"Description","Amount" +"car","1000.0" +"ps5","1000.0" diff --git a/TestCSV/Windows/valid/income-store.csv b/TestCSV/Windows/valid/income-store.csv new file mode 100644 index 0000000000..6f676f3553 --- /dev/null +++ b/TestCSV/Windows/valid/income-store.csv @@ -0,0 +1,4 @@ +"Description","Amount","Date","Goal","Recurrence","Has Next Recurrence" +"part-time job","1000.0","29/10/2023","car","none","false" +"allowance","500.0","29/10/2023","car","monthly","false" +"sell stuff","50.0","29/10/2023","ps5","none","false" diff --git a/build.gradle b/build.gradle index ea82051fab..cde0021669 100644 --- a/build.gradle +++ b/build.gradle @@ -1,46 +1,49 @@ -plugins { - id 'java' - id 'application' - id 'checkstyle' - id 'com.github.johnrengelman.shadow' version '7.1.2' -} - -repositories { - mavenCentral() -} - -dependencies { - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' - testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' -} - -test { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed" - - showExceptions true - exceptionFormat "full" - showCauses true - showStackTraces true - showStandardStreams = false - } -} - -application { - mainClass.set("seedu.duke.Duke") -} - -shadowJar { - archiveBaseName.set("duke") - archiveClassifier.set("") -} - -checkstyle { - toolVersion = '10.2' -} - -run{ - standardInput = System.in -} +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '7.1.2' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation group: 'com.opencsv', name: 'opencsv', version: '5.8' + implementation group: 'commons-io', name: 'commons-io', version: '2.15.0' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("seedu.duke.Duke") +} + +shadowJar { + archiveBaseName.set("duke") + archiveClassifier.set("") +} + +checkstyle { + toolVersion = '10.2' +} + +run{ + standardInput = System.in + enableAssertions = true; +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..f213acfed7 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) | Itay Refaely | [Github](https://github.com/itayrefaely) | [Portfolio](team/itayrefaely) +![](https://via.placeholder.com/100.png?text=Photo) | Chan Choon Siang | [Github](https://github.com/ChoonSiang) | [Portfolio](team/choonsiang) +![](https://via.placeholder.com/100.png?text=Photo) | Jonathan Tan | [Github](https://github.com/Jonoans) | [Portfolio](team/jonoans) +![](https://via.placeholder.com/100.png?text=Photo) | Jason Song Jun Jie | [Github](https://github.com/sRanay) | [Portfolio](team/sranay.md) +![](https://via.placeholder.com/100.png?text=Photo) | Hoo Jun Hong | [Github](https://github.com/hooami) | [Portfolio](team/hooami) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..d5769310b7 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,37 +2,547 @@ ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +1. [OpenCSV](https://mvnrepository.com/artifact/com.opencsv/opencsv) +2. [Apache Common IO](https://mvnrepository.com/artifact/commons-io/commons-io) -## Design & implementation +## Design -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Architecture +The bulk of the app's work is done by the following five components: +- `UI`: The UI of the App. +- `Parser`: Formats the user's input. +- `Command`: Command's logic and execution. +- `Storage`: Storage of data of the App. +- `StateManager`: Common source of truth for program. + +![Architecture Diagram](./images/ArchitectureDiagram.png "Architecture Diagram") + +### UI component + +![UI Sequence Diagram](./images/cs2113-ui-sequence.png "UI Sequence Diagram") +![UI Class Diagram](./images/ui-class-diagramv2.png "UI Class Diagram") + +The `UI` consists of a `Scanner` and an `OutputStream` object. Together, these objects abstract the functionalities of +obtaining user input and providing feedback (output printed in terminal UI). The `UI` component provides a simple +interface for other components to interact with the user. + +The `UI` component: +- provides a method to obtain user input. +- provide methods to print output in tabular format + +### Parser component + +The `Parser` functionality is to take in a user input, parse it and return the relevant `Command` object based on +the input. + +How the `Parser` works: +1. When the user input any string, it will be passed to a newly constructed `Parser` object. +2. The `parse` function in the `Parser` will be called to extract the command word, description and arguments of the +command if any. +3. These parsed details will then be passed to the relevant `Command` object based on the command word. +4. Afterward, the `Command` object will be passed back to `Main` for execution. + +Note: The `Parser` will not do any validation of arguments or description of the command. Those will be handled by the +respective `Command` object. + +![Parser Sequence Diagram](./images/ParserSequence.png "Parser Sequence Diagram") + + +### Command component + +The Command component consists of the individual command objects (listed in table below) and an abstract +class `Command`. The `Command` component is responsible for executing the commands after it has been parsed by `Parser`. \ +All error handling is handled here and any errors/output would be passed to the `UI` component for printing and +formatting of the output. + +| Command Class | Purpose | +|--------------------------|------------------------------------------------| +| AddExpenseCommand | Add a new Expense transaction | +| AddIncomeCommand | Add a new Income transaction | +| CategoryCommand | Add/Remove a Category (used for expense) | +| ExitCommand | Exit the program | +| GoalCommand | Add/Remove a Goal (used for income) | +| HelpCommand | Gives usage format information to the user | +| ListCommand | Lists all incoming/outgoing transactions | +| ExportCommand | Exports transactions data into CSV FIle | +| RemoveTransactionCommand | Deletes a transaction | +| EditTransactionCommand | Edits an income/expense transaction | +| SummaryCommand | Summarise the total income/expense transaction | + +### Storage component +The `Storage` functionality is to load data from the storage files (`category-store.csv` , `expense-store.csv`, `goal-store.csv`, `income-store.csv`) into the application. It will also store any data while the application is running. + +![Storage Class Diagram](./images/cs2113-storage-class.png "Storage Class Diagram") + +The `Storage` component: +- Loads previous state of application when the program is booted up +- Skips any rows that are invalid during the validation phase +- Saves to storage file after each command is completed +- Uses `CsvWriter` and `CsvReader` class to read and write to the storage files. +- `CsvWriter` and `CsvReader` uses `CSVWriter` and `CSVReader` respectively from OpenCSV library to write and read from CSV Files + +### StateManager component +The `StateManager` component provides the program with a single source of truth. `StateManager`'s design follows the singleton design pattern, allowing +only a single instance to be declared throughout the program. Thus, the constructor is explicitly set to private - this is by design. + +In order to get the instance of `StateManager` in the program, `StateManager#getStateManager()` should be used. + +The `StateManager` component keeps tracking of the income, expenses, goals and categories added to the program. Each entity +is tracked with a corresponding `ArrayList`. `StateManager` also provides the following general utility methods for easy retrieval of data: +- `Storage#addEntity(Entity)` - Add an instance of entity to be tracked +- `Storage#getEntity(int): Entity` - Gets an instance of the entity by index (0-based). +- `Storage#removeEntity>(Entity)` - Removes the provided entity from the corresponding `ArrayList` +- `Storage#removeEntity(int)` - Removes the entity by index (0-based) +- `Storage#getAllEntity(): ArrayList` + +**Note:** Entity is a placeholder for `Income`, `Expense`, `Goal` and `Category`. + +The `StateManager` also contains other methods for managing objects in the state. However, we will not delve into these +more application-specific methods in this section. + +## Common Classes +### Income Class +Income class is used to store information about the savings of the user. It is implemented by aggregating +Transaction and Goal classes. Each income is linked to one transaction and goal. The goal specifies a target to which the user +is saving towards. + +![Income Class Diagram](./images/IncomeClassDiagram.png "Income Class Diagram") + +### Expense Class +Expense class is used to store information about the spending of the user. It is implemented by aggregating +Transaction and Category classes. Each expense is linked to one transaction and category. The category is used for +grouping related expenditures such as Food, Transport, School Fees, etc. + +![Expense Class Diagram](./images/ExpenseClassDiagram.png "Expense Class Diagram") + +## Implementation + +### Transaction tracking feature + +Transaction tracking is a core functionality in the program. This feature includes two commands `in` and `out` which gives users +the ability to add income or expenses respectively. Also, the user is able to associate an income entry with a goal or have an expense +entry be associated to a category of expenditure. + +The following functionalities are implemented in `AddIncomeCommand` and `AddExpenseCommand` and its' parent class `AddTransactionCommand`. + +An example of the usage of the `in` and `out` command can be found in the [User Guide](https://ay2324s1-cs2113-w12-3.github.io/tp/UserGuide.html). + +When a user attempts to add an income or expense entry, the user's input will be validated first to ensure that: +- Description, used to denote information about the transaction entry, is not blank +- Amount is a decimal value that is non-negative. + - 0 is allowed - Possible use case would be to add a placeholder entry that can be later edited to reflect actual amount. +- Date, if provided, will be verified to ensure validity (`ddMMyyyy` format). +- Recurrence, if provided, is of a valid value (case-insensitive) + - Valid values are `daily`, `weekly`, `monthly` and `none`. + - Date, if provided, is additionally verified to ensure that it is not more than or equal to 1 period in the past, relative to current system time. + +If any of the above validation fails, an error message relevant to the failure will be printed to inform the user and hint the corrections required. + +For attempts to add an income entry, the command will additionally verify that the goal (if specified) already exists in the program. Otherwise, +an error will be returned. + +Once a user's inputs are verified and deemed valid, a `Transaction` object is prepared through a call to `AddTransactionCommand#prepareTransaction()`. The returned `Transaction` object is encapsulated together with a corresponding `Goal` or `Category` object in an `Income` or `Expense` object as required. + +These prepared objects are then encapsulated in a corresponding `Income` or `Expense` object and added to the program using `StateManager#addIncome(Income)` or `StateManager#addExpense(Expense)`. + +Given below is an example usage scenario and how the transaction tracking feature behaves at each step. + +Step 1. The user launches the application for the first time. There will be no transactions in the program. + +Step 2. The user executes `in pocket money /amount 100` to add an income transaction with description set to `pocket money` and amount set to `100`. Since no date, goal or recurrence are explicitly stated, they are set to `Uncategorised`, the system's current date and `none` respectively. + +Step 3. An income entry is creating corresponding to the above parameters and a success message containing the added transaction is printed. + +Below is the sequence diagram for the transaction tracking feature. Specifically, the sequence for adding an income entry. The sequence +diagram for adding expense will be omitted because it is largely similar to the sequence for adding an income with slight differences. + +![Add income entry sequence diagram](./images/transaction-tracking-sequence.png "Add income entry sequence diagram") + +### Export feature + +The export feature is facilitated by `CsvWriter` class which uses a third party library called OpenCSV. It implements the following operation: +- `exportTransactionData` - Converts each Transaction into an Array to be stored into the CSV File +- `exportIncomeData` - Exports all income transactions only +- `exportExpenseData` - Export all expense transactions only + +Given below is an example usage scenario and how the export features behaves at each step. + +Step 1. The user launches the application for the first time. There would be no transactions available to be exported. + +Step 2. The user executes `in part-time job /amount 500 /goal car` to create a transaction with the description of `part-time job`, with the `amount` set to `500` and `goal` set to `car` and stores it in the program + +Step 3. So when the user executes `export`, it will get all the transactions that the program stored and exports to a CSV file called `Transactions.csv` + +However, if the user wishes to export only the income or expense transactions, the user could enter `export /type in` or `export /type out` respectively. + +Below is the sequence diagrams for the export feature. + +![Export feature sequence diagram](./images/export-feature-sequence.png "Export feature sequence diagram") + +![Export income data sequence diagram](./images/export-feature-sequence-income-data.png "Export income data sequence diagram") + +![Export expense data sequence diagram](./images/export-feature-sequence-expense-data.png "Export expense data sequence diagram") + +![Extract transaction data sequence diagram](./images/export-feature-sequence-extract.png "Extract transaction data sequence diagram") + +### Goal Feature + +The goal feature is facilitated by `GoalCommand`, which extends `Command`. Based on the argument, either `/add` or `/remove`, +the user can add a new goal or remove an existing goal. The main purpose of the goal feature is to tag each income transaction with +a goal, such that the user can have a clear idea of how to plan his savings toward his personal goals. + +If `/add` is provided, the command will validate the amount via +`GoalCommand#validateAmount()` to ensure that the amount argument is present and valid. Then, `GoalCommand#addGoal` will be +called to add the goal if the goal does not exist yet, else it will output an error message to inform the user that the goal already exist. + +Else if `/remove` is provided, the command will call `GoalCommand#removeGoal` to check if the goal exists and remove it. Else, it will output +an error message to inform the user that the goal does not exist. + +Given below is an example usage scenario and how the goal feature behaves. + +Step 1. The user launches the application for the first time. There will be no goal available. + +Step 2. The user executes `goal /add car /amount 100000`, which will add a `car` goal, with the amount set to `100000`. + +Step 3. The user executes `goal /remove car`, which will remove the newly added `car` goal. + +Below is the sequence diagram for the goal feature. + +![Goal feature sequence diagram](./images/goal-feature-sequence.png "Goal feature sequence diagram") + +### Category Feature + +The category feature is facilitated by `CategoryCommand`, which extends `Command`. Based on the argument, either `/add` or `/remove`, +the user can add a new category or remove an existing category. The main purpose of the category feature is to tag each expense transaction with +a category, such that the user can categorise his spending. + +If `/add` is provided, `CategoryCommand#addCategory` will be +called to add the category if the category does not exist yet, else it will output an error message to inform the user that the category already exist. + +Else if `/remove` is provided, the command will call `CategoryCommand#removeCategory` to check if the category exists and remove it. Else, it will output +an error message to inform the user that the category does not exist. + +Given below is an example usage scenario and how the category feature behaves. + +Step 1. The user launches the application for the first time. There will be no category available. + +Step 2. The user executes `category /add food`, which will add a `food` category. + +Step 3. The user executes `category /remove food`, which will remove the newly added `food` category. + +Below is the sequence diagram for the category feature. + +![Category feature sequence diagram](./images/category-feature-sequence.png "Category feature sequence diagram") + +### Delete transaction feature + +The delete transaction feature is facilitated by `RemoveTransactionCommand`, which extends `Command`. Based on the `/type` argument value, +either the income or expense transaction will be removed. The transaction to be removed is based on the index supplied by the user, as shown +when listing the income/expense transaction. + +The command will call `RemoveTransactionCommand#removeTransaction()` which will get the maximum number of transaction from `RemoveTransactionCommand#getTransactionMaxSize()`, +then parse the index supplied by the user and ensure is a valid integer using `RemoveTransactionCommand#parseIdx()`. This parsed index will then be used to remove the +transaction from the `StateManager`. Afterward, `RemoveTransactionCommand#printSuccess()` will be called to print a success message + to inform the user that the transaction has been removed. + +Given below is an example usage scenario and how the delete transaction feature behaves. + +Step 1. The user launches the application for the first time. There will be no transaction available. + +Step 2. The user input `out dinner /amount 10` to add expense transaction. + +Step 3. The user input `delete 1 /type out`. This will remove the first expense transaction, which is +the transaction just added by the user. + +Below is the sequence diagram for the delete transaction feature. + +![Delete transaction feature sequence diagram](./images/delete-transaction-feature-sequence.png "Delete transaction feature sequence diagram") + +### Edit transaction feature + +The edit transaction feature is facilitated by `EditTransactionCommand`, which extends `Command`. Based on the `/type` argument value, +either the income or expense transaction will be edited. User can edit the description, amount, goal (for income transaction) and category (for expense transaction). +Date and recurrence of the transaction cannot be edited. + +The command will first call `EditTransactionCommand#throwIfInvalidDescOrArgs()` to check that valid arguments and value are supplied by the user. Afterward, `EditTransactionCommand#editTransaction()` +will get the maximum number of transaction from `EditTransactionCommand#getTransactionMaxSize()`, +then parse the index supplied by the user and ensure is a valid integer using `EditTransactionCommand#parseIdx()`. This parsed index will then be used to update the +transaction from the `StateManager`. Afterward, `EditTransactionCommand#printSuccess()` will be called to print a success message +to inform the user that the transaction has been updated with the new values. + +Given below is an example usage scenario and how the edit transaction feature behaves. + +Step 1. The user launches the application for the first time. There will be no transaction available. + +Step 2. The user input `out dinner /amount 10 /category food` to add expense transaction. + +Step 3. The user input `edit 1 /type out /description lunch /amount 12 /category essentials`. This will edit the first expense transaction, which is +the transaction just added by the user. The new description will be `lunch`, amount `12` and category `essentials`. + +### List feature +The list feature is facilitated by `ListCommand`, which extends `Command`. Depending on user input, the user would +be able to either view a summary of all current goals/categories and the progress towards a goal, or a list of all +income or expense transactions, with options to filter the list to obtain a more specific view. + +The command would first call `ListCommand#validateInput()`, which would validate if the user input is correct. If it is, +the program would then call `ListCommand#listTypeHandler()` to determine which path to take. This feature has 2 sub-features. + +#### Sub-Feature 1: Listing of goals and categories + +After the program reaches `ListCommand#listTypeHandler()`, it would call `ListCommand#printTypeStatus()` to prepare a list +of either goals or categories depending on the user input. The program would then call either `UI#printGoalsStatus()` +or `UI#printCategoryStatus()`. + +#### Sub-Feature 2: Listing of transactions + +After the program reaches `ListCommand#listTypeHandler()`, depending on the user input to print either income or expenses, the program would first call `ListCommand#checkInArgs()` to validate the arguments and then `ListCommand#listIncome()`. +Alternatively, it would call `ListCommand#checkOutArgs()` followed by `ListCommand#listExpenses`. +If filters are specified, the program may either call `ListCommand#filterGoal()`, `ListCommand#filterIncome()` to filter the income transaction list, or +`ListCommand#filterCategory()` and `ListCommand#filterExpense()` for filtering of the expense transaction list. Finally, it would call `ListCommand#printList()`, which would +call `UI#listTransactions()` to print the transactions list. + + +Given below is an example usage scenario: + +Step 1. This is made with the assumption that the user has added a few transactions to the program. + +Step 2. The user inputs `list /goal` to list all goals in the program + +Step 3. The program would calculate how much does each goal current have. + +Step 4. The program would then return a list of all goals that has some progress towards it, followed by uncategorised goals, +where transactions were added without a goal listed, and a list of unused goals. + +Below is the ListCommand sequence diagram: +![List Sequence Diagram](./images/ListSequenceDiagram.png "List Sequence Diagram") + +### Summary feature + +The summary feature is facilitated by `SummaryCommand`, which extends `Command`. Based on the arguments, `/day`, `/week` or `/month`, +the user can choose to filter and summarise the total sum of the income or expense transactions by the current day, week, or month. + +The command will first call `SummaryCommand#getFilter()` to get the filter type indicated by the user if any. Then, based on the `/type` argument value supplied by the user, `SummaryCommand#printSummary` will +call either `SummaryCommand#getIncomeSummary()` or `SummaryCommand#getExpenseSummary()`, which will get the filtered income or expense arraylist from `SummaryCommand#filterIncome()` or `SummaryCommand#filterExpense()`. + +The filtered arraylist will then be looped to sum up the total amount. This total amount will be passed to `SummaryCommand#getSummaryMsg()` to format the message to be printed by `ui`. + +Given below is an example usage scenario and how the summary feature behaves. + +Step 1. Assume that the user has been using the program for some time. There will be a few income and expense transactions available. + +Step 2. The user input `summary /type in /day`. + +Step 3. The program will filter all the income transaction by the current date, and sum up the total amount. + +Step 4. The total amount will be output. + +Below is the sequence diagram for the summary feature. + +![Summary feature sequence diagram](./images/summary-feature-sequence.png "Delete transaction feature sequence diagram") ## Product scope + ### Target user profile -{Describe the target user profile} + Users who prefer a CLI interface over a GUI and want to better manage their finances to gauge their financial health. ### Value proposition -{Describe the value proposition: what problem does it solve?} +Personal finance tracker to make it easy for users to track and manage their saving/spending, \ +and view a summary of their daily/weekly/monthly transactions. ## 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|user| add a new income source | can keep track of my allowances and part-time job earnings | +|v1.0|user| add an expense | can monitor my purchases and stay within my budget | +|v1.0|user| delete a transaction | remove any duplicate or unwanted entries from my expenses | +|v1.0|user| view a list of all my transactions | review my income and expenses | +|v2.0|user| export financial data to a CSV file | use it for client presentations and analysis | +|v2.0|user| set up recurring transactions for mortgage payments and utility bulls | easily track and budget for regular home expenses | +|v2.0|user| set financial goals, such as saving for a down payment on a house | stay motivated and track my progress towards home ownership. | +|v2.0|user| categorise my spending | to group similar spendings together | +|v2.0|user| edit my transaction | to rectify any mistakes I made when inputing the transaction details | +|v2.0|user| view a summary of my income and expense transactions | know my current saving and spending | ## Non-Functional Requirements -{Give non-functional requirements} +- The program work on any mainstream OS with Java 11. +- The program should provide a consistent experience across the different platforms as far as possible. +- The program should be able to work locally without internet connectivity. +- The program should be intuitive to use. ## Glossary -* *glossary item* - Definition +| Terms | Definition | +|---------------|----------------------------------------------| +| Mainstream OS | Windows, Linux, Unix, OS-X | + ## Instructions for manual testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +Listed below are the steps to test the program manually. + +### Launching and exiting the program +1. Launching program + 1. Download the jar file and store it in a new folder. + 2. Run the jar file using `java -jar FinText.jar` (Where FinText.jar is the jar file name) + 3. You will be greeted with a welcome message. +2. Exiting the program + 1. Input `bye` to exit the program safely. + +### Adding an income transaction +1. Adding an income transaction. + 1. Test case: `in angbao /amount 100`
+ Expected: An income transaction is being tracked with the description `angbao`, amount `100.00`, date will be the + current date, goal `Uncategorised` and recurrence `none`. +2. Adding an income transaction with a specific date. + 1. Test case: `in angbao /amount 100 /date 10112023`
+ Expected: An income transaction is being tracked with the description `angbao`, amount `100.00`, date + `2023-11-10`, goal `Uncategorised` and recurrence `none`. +3. Adding an income transaction with a specific date and goal. + 1. Prerequisite: A goal `car` must be added first. + 2. Test case: `in angbao /amount 100 /date 10112023 /goal car`
+ Expected: An income transaction is being tracked with the description `angbao`, amount `100.00`, date + `2023-11-10`, goal `car` and recurrence `none`. +4. Adding an income transaction with a specific date, goal and recurring monthly. + 1. Prerequisite: A goal `car` must be added first. + 2. Test case: `in salary /amount 3000 /date 10112023 /goal car /recurrence monthly`
+ Expected: An income transaction is being tracked with the description `salary`, amount `3000.00`, date + `2023-11-10`, goal `car` and recurrence `monthly`. + +### Adding an expense transaction +1. Adding an expense transaction. + 1. Test case: `out dinner /amount 10`
+ Expected: An expense transaction is being tracked with the description `dinner`, amount `10.00`, date will be the + current date, category `Uncategorised` and recurrence `none`. +2. Adding an expense transaction with a specific date. + 1. Test case: `out dinner /amount 10 /date 10112023`
+ Expected: An expense transaction is being tracked with the description `dinner`, amount `10.00`, date + `2023-11-10`, category `Uncategorised` and recurrence `none`. +3. Adding an expense transaction with a specific date and category. + 1. Test case: `out dinner /amount 10 /date 10112023 /category food`
+ Expected: An expense transaction is being tracked with the description `dinner`, amount `10.00`, date + `2023-11-10`, category `food` and recurrence `none`. +4. Adding an expense transaction with a specific date, category and recurring daily. + 1. Test case: `out dinner /amount 10 /date 10112023 /category food /recurrence daily`
+ Expected: An expense transaction is being tracked with the description `dinner`, amount `10.00`, date + `2023-11-10`, category `food` and recurrence `daily`. + +### Deleting a transaction +1. Deleting an income transaction + 1. Prerequisite: Ensure there is at least 1 income transaction added. + 2. Test Case: `delete 1 /type in`
+ Expected: The first income shown when listing income transactions is deleted. + 3. Test Case: `delete x /type in` (Where `x` is a number greater than the total number of income transactions, or + less than 1, or is not a number)
+ Expected: A error message will be shown to input valid index. + 4. Test Case: `delete 1`
+ Expected: A error message will be shown to indicate the transaction type. +2. Deleting an expense transaction + 1. Prerequisite: Ensure there is at least 1 expense transaction added. + 2. Test Case: `delete 1 /type out`
+ Expected: The first expense shown when listing expense transactions is deleted. + 3. Test Case: `delete x /type in` (Where `x` is a number greater than the total number of expense transactions, or + less than 1, or is not a number)
+ Expected: A error message will be shown to input valid index. + 4. Test Case: `delete 1`
+ Expected: A error message will be shown to indicate the transaction type. + +### Listing transactions +1. Listing income transactions + 1. Prerequisite: Ensure there is at least 1 income transaction added. A goal `car` is added. + 2. Test Case: `list /type in`
+ Expected: All income transactions will be listed. + 3. Test Case: `list /type in /goal car`
+ Expected: Only income transactions with the goal `car` will be listed. + 4. Test Case: `list /type in /goal car /week`
+ Expected: Only income transactions with the goal `car` and within the current week will be listed. + +2. Listing expense transactions + 1. Prerequisite: Ensure there is at least 1 expense transaction added. + 2. Test Case: `list /type out`
+ Expected: All expense transactions will be listed. + 3. Test Case: `list /type out /category food`
+ Expected: Only expense transactions with the category `food` will be listed. + 4. Test Case: `list /type out /category food /week`
+ Expected: Only expense transactions with the category `food` and within the current week will be listed. + +### Adding goal +1. Adding goal + 1. Test Case: `goal /add car /amount 1000000`
+ Expected: Goal `car` is added with the amount `1000000`. + 2. Test Case: `goal /add car` (Missing `/amount`)
+ Expected: Error message is shown with the correct usage of the command. + +### Removing goal +1. Removing goal + 1. Prerequisite: Ensure that goal `car` is added. + 2. Test Case: `goal /remove car`
+ Expected: Goal `car` is removed. All income transaction with goal `car`, will be changed to goal `Uncategorised`. + +### Adding category +1. Adding category + 1. Test Case: `category /food`
+ Expected: Category `food` is added. + + +### Removing category +1. Removing category + 1. Prerequisite: Ensure that category `food` is added. + 2. Test Case: `category /remove food`
+ Expected: Category `food` is removed. All expense transaction with category `food`, will be changed to category `Uncategorised`. + +### Editing a transaction +1. Editing an income transaction + 1. Prerequisite: Ensure that there is at least one income transaction. `car` goal is added. + 2. Test Case: `edit 1 /type in /description New Edited Description`
+ Expected: The description of the first income transaction shown when listing income transactions will be changed to + `New Edited Description`. + 3. Test Case: `edit 1 /type in /amount 1234`
+ Expected: The amount of the first income transaction shown when listing income transactions will be changed to + `1234.00`. + 4. Test Case: `edit 1 /type in /description New Edited Description /amount 1234 /goal car`
+ Expected: The first income transaction will be changed to description `New Edited Description`, amount `1234.00`, goal `car`. +2. Editing an expense transaction + 1. The output will be similar to editing income transaction, with type `out`. Instead of `goal`, `category` is edited instead. + +### Summarise the transaction total +1. Summarise income transactions + 1. Prerequisite: Ensure that there is at least one income transaction. + 2. Test Case: `summary /type in`
+ Expected: Show the total sum of income transactions. + 3. Test Case: `summary /type in /day`
+ Expected: Show the total sum of income transactions that are transacted in the current day. + 4. Test Case: `summary /type in /week`
+ Expected: Show the total sum of income transactions that are transacted in the current week. + 5. Test Case: `summary /type in /month`
+ Expected: Show the total sum of income transactions that are transacted in the current month. +2. Summarise expense transactions + 1. Prerequisite: Ensure that there is at least one expense transaction. + 2. Test Case: `summary /type out`
+ Expected: Show the total sum of expense transactions. + 3. Test Case: `summary /type out /day`
+ Expected: Show the total sum of expense transactions that are transacted in the current day. + 4. Test Case: `summary /type out /week`
+ Expected: Show the total sum of expense transactions that are transacted in the current week. + 5. Test Case: `summary /type out /month`
+ Expected: Show the total sum of expense transactions that are transacted in the current month. + +### Exporting transaction +1. Exporting transactions + 1. Test Case: `export`
+ Expected: All transactions will be exported to a file `Transactions.csv` + 2. Test Case: `export /type in`
+ Expected: Only income transactions will be exported to a file `Transactions.csv` + 3. Test Case: `export /type out`
+ Expected: Only expense transactions will be exported to a file `Transactions.csv` + +### Help +1. Help + 1. Test Case: `help`
+ Expected: Show all the available commands and description of each command + 2. Test Case: `help x` (Where `x` is the command)
+ Expected: Show the usage and options of the command. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..0f12b32ede 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ -# Duke +# FinText -{Give product intro here} +FinText is a **Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track and manage +their saving/spending,** and view a summary of their daily/weekly/monthly transactions. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..3c1ea0d4dd 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,322 @@ -# User Guide +# FinText User Guide ## Introduction -{Give a product intro} +FinText is a **Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track and manage +their saving/spending,** and view a summary of their daily/weekly/monthly transactions. Application Data will be stored into a folder called `data`. + +* [Quick Start](#quick-start) +* [Features](#features) + * [Viewing Help: `help`](#viewing-help-help) + * [Adding an income entry: `in`](#adding-an-income-entry-in) + * [Adding an expense entry: `out`](#adding-an-expense-entry-out) + * [Delete Transaction: `delete`](#delete-transaction-delete) + * [List Transactions: `list`](#list-transactions-list) + * [Add/Remove Goal: `goal`](#addremove-a-goal-goal) + * [Add/Remove Category: `category`](#addremove-a-category-category) + * [Export Transactions: `export`](#export-transactions-export) + * [Edit Transactions: `edit`](#edit-transactions-edit) + * [Transaction Summary: `summary`](#transaction-summary-summary) + * [End Program: `bye`](#end-program-bye) +* [Command Summary](#command-summary) + ## Quick Start -{Give steps to get started quickly} +1. Ensure that you have Java 11 installed on your computer. +2. Download the latest version of `FinText` from [here](https://github.com/AY2324S1-CS2113-W12-3/tp/releases). +3. Run the program by `java -jar FinText.jar` -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +## Features -## Features +> * `UPPER_CASE` denotes user-supplied parameters, and arguments with square brackets
e.g. `[/date DATE]` denote + optional arguments, while arguments not in square brackets are mandatory. +> * Any text e.g. `DESCRIPTION` has to come before arguments.
+ `in Salary /amount 500 /goal Savings` is a valid command, while `in /amount 500 /goal Savings Salary` is not a valid + command. +> * Arguments can be in any order.
+ e.g. if a command has the arguments `/amount AMOUNT /goal GOAL`, `/goal GOAL /amount AMOUNT` is acceptable as well. +> * Argument names are case-sensitive, while argument values are case-insensitive.
+ e.g. `/type` and `/Type` are different argument. `/type in` and `/type IN` will indicate the `in` transaction type. +> * Additional supplied arguments or unnecessary description will be simply ignored. +> * User is intentionally not restricted to input future or past date to the `/date DATE` argument to allow for flexibility in managing their transactions. +> * Duplicate arguments are not accepted by the program. A message will be shown in such cases. +> * On MacOS, `Ctrl-c` and `Ctrl-d` will end the program safely and print the bye message. +> * On Windows, `Ctrl-c` will end the program safely and print the bye message. -{Give detailed description of each feature} +### Viewing Help: `help` +Shows a list of all the commands available to the user. -### Adding a todo: `todo` -Adds a new item to the list of todo items. +User can also view more details of a command. -Format: `todo n/TODO_NAME d/DEADLINE` +Format: `help COMMAND` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +**Usage Example:** -Example of usage: +`help in` - Shows details on how to use the `in` command. -`todo n/Write the rest of the User Guide d/next week` +`help delete` - Shows details on how to use the `delete` command. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` -## FAQ +### Adding an income entry: `in` +Adds an income towards a goal. -**Q**: How do I transfer my data to another computer? +Format: `in DESCRIPTION /amount AMOUNT [/goal GOAL] [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]` -**A**: {your answer here} +* `DESCRIPTION` is case-sensitive, while the arguments are not. +* `AMOUNT` must be more than or equal to 0 and less than 10 million, it can contain at most 2 decimal points. +* `DATE` must be in format `DDMMYYYY` + * If `RECURRENCE` is specified, date must not be earlier than or equal to 1 period in the past (can be in the future). + * i.e. If `RECURRENCE` is weekly, date specified must not be more than 6 days in the past. +* `RECURRENCE` is a string that indicates whether the income added is recurring.
+ Possible values are `none`, `daily`, `weekly` and `monthly`. If this option is not specified, recurrence defaults to `none`. +* If `GOAL` is specified, it must either be `Uncategorised` or the goal must already exist beforehand. -## Command Summary +**Usage Example:** + +`in part-time job /amount 500`
+Adds an income entry for 'part-time job' with an amount of 500. As goal is not specified, a default goal `Uncategorised` would be assigned to it. + +`in red packet money /amount 50 /goal PS5 /date 18092023`
+Adds an income entry that happened on 18 Sept 2023 for 'red packet money' for an amount of 50 towards +a goal called 'PS5'. + +`in pocket money saved /amount 25 /goal savings /recurrence weekly`
+Adds an income entry for 'pocket money saved' for an amount of 25 towards +a goal called 'savings' which recurs weekly. + +**Sample Output** +``` +Nice! The following income has been tracked: +Description Date Amount Goal Recurrence +Salary 2023-11-14 300.00 Holiday none +``` + +### Adding an expense entry: `out` +Adds an expense for a category. + +Format: `out DESCRIPTION /amount AMOUNT [/category CATEGORY] [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]` + +* `DESCRIPTION` is case-sensitive, while the arguments are not. +* `AMOUNT` must be more than or equal to 0 and less than 10 million, it can contain at most 2 decimal points. +* `DATE` must be in format `DDMMYYYY` + * If `RECURRENCE` is specified, date must not be earlier than or equal to 1 period in the past (can be in the future). + * i.e. If `RECURRENCE` is weekly, date specified must not be more than 6 days in the past. +* `RECURRENCE` is a string that indicates whether the expense added is recurring.
+ Possible values are `none`, `daily`, `weekly` and `monthly`. If this option is not specified, recurrence defaults to `none`. +* If `CATEGORY` was not created previously, a category would automatically be created for it. + +**Usage Example:** + +`out dinner /amount 10.50 /category food`
+Adds an expense entry for 'dinner' with an amount of 10.50 towards the 'food' category. + +`out pokemon card pack /amount 10.50 /category food /date 18092023`
+Adds an expense entry that happened on 18 Sept 2023 for 'pokemon card pack' for an amount of 10.50 towards +the 'game' category. + +`out spotify /amount 9 /category entertainment /recurrence monthly`
+Adds an expense entry for 'pokemon card pack' for an amount of 9 towards +the 'entertainment' category which recurs monthly. + +**Sample Output** +``` +Nice! The following expense has been tracked: +Description Date Amount Category Recurrence +11/11 Purchase 2023-11-14 500.00 Uncategorised none +``` + +### Delete Transaction: `delete` +Delete a specific transaction based on the index in the list. + +Format: `delete INDEX /type (in | out)` +* `/type` only accepts `in` or `out`. +* `INDEX` is based on the ID from the `list` command. + + +**Usage Example:** + +`delete 1 /type in` - Deletes the first income entry. + +`delete 2 /type out` - Deletes the second expense entry. + + +### List Transactions: `list` +Shows a sorted list of all added transactions based on type, with filters for goals, categories and recurrence, or shows a list of current goals and categories. + +Formats: + +`list (goal | category)` + +`list /type (in | out) [/goal GOAL] [/category CATEGORY] [/week] [/month]` + +* Deletion has to be based on the ID of the transaction without any filters (e.g. only `list /type in` or `list /type out`). +* User must only specify either `/week` or `/month`. If both are specified, then `/week` will take priority. +* The list that would be printed will be sorted in descending order by date. +* If `list goal` or `list category` is used, there must not be any other arguments that come after that. +* If arguments are specified, such as `list /type in`, there should not be anything before the argument. (`list goal /type in` would be considered an invalid command) +* The maximum supported goal progress percentage is `99999999.99%`, if exceeded, the goal progress percentage will be truncated +* For 'Uncategorised' goal, there would not be any progress shown as there is no target amount allowed. + +**Usage Example:** + +`list /type in` - List all income transactions + +`list /type out` - List all expense transactions + +`list /type in /goal Travel` - List all income transactions with the 'Travel' goal + +`list /type out /category Food` - List all expense transactions with the 'Food' category + +`list /type in /week` - List all income transactions in the current week + +`list /type in /month` - List all income transactions in the current month -{Give a 'cheat sheet' of commands here} +`list goal` - Lists all current goals and the progress towards each goal + +`list category` - Lists all current categories and the spending for each category + +**Sample Output:** +``` +Alright! Displaying 3 transactions. +====================================== IN TRANSACTIONS ====================================== +ID Description Date Amount Goal Recurrence +1 part-time job 2023-10-31 500.00 car none +2 pocket money saved 2023-10-31 25.00 PS5 weekly +3 red packet money 2023-09-18 50.00 PS5 none +====================================== IN TRANSACTIONS ====================================== +``` +``` +==================================== Goals Status ==================================== +Name Amount Progress +Savings 300.00/500.00 [============ ] 60.00% +Uncategorised 300.00 + +Unused Goals: +Goal Target Amount +Tuition 300.00 +==================================== Goals Status ==================================== +``` +### Add/Remove a goal: `goal` +Creates or deletes a user's goal (used for income) + +Format: `goal [/add GOAL /amount AMOUNT] [/remove GOAL]` +* Only either `/add` or `/remove` can be provided. They should not be provided together. +* `GOAL` is case-insensitive +* `/add GOAL` has to be accompanied with `/amount AMOUNT` +* `AMOUNT` has to be a value of at least 0.01 and less than 10 million, and it can contain at most 2 decimal points. + +### Add/Remove a category: `category` +Creates or deletes a user's category (used for expenses) + +Format: `category [/add CATEGORY] [/remove CATEGORY]` +* Only either `/add` or `/remove` can be provided. They should not be provided together. +* `CATEGORY` is case-insensitive. + + +### Export Transactions: `export` +Exports all transaction data into a CSV file called `Transactions.csv` + +Format: `export [/type (in | out)]` +* If `/type` is not specified, by default it will extract **ALL** transactions. +* In any scenario where any error is encountered when exporting the transactions, the message displayed will be `Cannot create file`. + +**Usage Example:** + +`export /type in` - Export all in transactions + +`export /type out` - Export all out transactions + +### Edit Transactions: `edit` +Edits an existing transaction. + +Format: `edit INDEX /type (in | out) [/description DESCRIPTION] [/amount AMOUNT] [/goal GOAL] [/category CATEGORY]` + +- User must specify /type option to edit either a transaction under income or expense. +- User must specify a valid income/expense transaction index. +- User must only specify at least either `/description`, `/amount`, `/goal` (if editing an income transaction) or `/category` (if editing an expense transaction). +- User cannot edit the date field. +- User cannot edit the recurrence field. +- In case of editing a goal, it must exist beforehand. +- The same constraints that apply when adding income/expenses also apply here. + +**Usage Example:** + +`edit 1 /type in /description allowance` - Edits the description of the first income transaction to be "allowance". + +`edit 2 /type in /goal ps5` - Edits the goal of the second income transaction to be ps5. + +`edit 2 /type out /amount 10` - Edits the amount of the second expense transaction to be 10. + +`edit 2 /type out /description grab /amount 10 /category transport` - Edits the second expense transaction description to `grab`, amount to `10`, category to `transport`. + +**Sample Output** +``` +> User: list /type in +Alright! Displaying 1 transaction. +=========================================== IN TRANSACTIONS =========================================== +ID Description Date Amount Goal Recurrence +1 Salary 2023-11-14 300.00 Holiday none +=========================================== IN TRANSACTIONS =========================================== +> User: edit 1 /type in /goal Uncategorised +Successfully edited income no.1 Salary +> User: list /type in +Alright! Displaying 1 transaction. +=========================================== IN TRANSACTIONS =========================================== +ID Description Date Amount Goal Recurrence +1 Salary 2023-11-14 300.00 Uncategorised none +=========================================== IN TRANSACTIONS =========================================== +``` + +### Transaction Summary: `summary` +Shows the summarised total of transactions. + +Format: `summary /type (in | out) [/day] [/week] [/month]` +* User must specify /type option to list either transactions added under income or expense +* If neither `/day`, `/week` or `/month` are shown, then all transactions under income or expense will be summarised according to the `/type` specified. +* User must only specify either /day or /week or /month. If multiple are specified, then they will take priority in the order of `day`, `week`, `month`. + * If both `/day` and `/week` are specified, then `/day` result will be shown. + * If `/week` and `/month` are specified, then `/week` result will be shown. + * If `/day`, `/week` and `/month` are all specified, then `/day` result will be shown. +* `/day` will filter the transactions to those of the current day. +* `/week` will filter the transactions to those in the current week. +* `/month` will filter the transactions to those in the current month. + +**Usage Example:** + +`summary /type in` - Shows the summarised total for income. + +`summary /type out` - Shows the summarised total for expense. + +`summary /type in /day` - Shows the summarised total for income of the current day. + +`summary /type out /week` - Shows the summarised total for expense in the current week. + +`summary /type out /month` - Shows the summarised total for expense in the current month. + +``` +> User: summary /type in +Good job! Total income so far: $300.00 +> User: summary /type out +Wise spending! Total expense so far: $500.00 +``` + + +### End Program: `bye` +Safely ends the program. + +## Command Summary -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Action | Format | Example | +|-------------------------|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| Help | `help` | | +| Adding an income entry | `in DESCRIPTION /amount AMOUNT /goal GOAL [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]` | `in part-time job /amount 500 /goal car` | +| Adding an expense entry | `out DESCRIPTION /amount AMOUNT /category CATEGORY [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]` | `out dinner /amount 10.50 /category food` | +| Delete Transaction | `delete INDEX /type (in | out)` | `delete 1 /type in` | +| List Transactions | `list /type (in | out) [/goal GOAL] [/category CATEGORY] [/week] [/month]` | `list /type in` | +| Add/Remove a Goal | `goal [/add GOAL /amount AMOUNT] [/remove GOAL]` | `goal /add PS5 /amount 600` | +| Add/Remove a Category | `category [/add CATEGORY] [/remove CATEGORY]` | `category /add Bills` | +| Export Transactions | `export [/type (in | out)]` | `export /type in` | +| Edit Transaction | `edit INDEX /type (in | out) (/description DESCRIPTION | /amount AMOUNT | /goal GOAL | /category CATEGORY)` | `edit 2 /type in /goal ps5` | +| Transaction Summary | `summary /type (in | out) [/day] [/week] [/month]` | `summary /type in /day` | +| End program | `bye` | | diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..21856664b5 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,4 @@ +remote_theme: pages-themes/cayman@v0.2.0 +plugins: + - jekyll-remote-theme +gem "github-pages", group: :jekyll_plugins \ No newline at end of file diff --git a/docs/diagrams/Architecture.puml b/docs/diagrams/Architecture.puml new file mode 100644 index 0000000000..b9a709b934 --- /dev/null +++ b/docs/diagrams/Architecture.puml @@ -0,0 +1,16 @@ +@startuml +skinparam componentStyle rectangle +actor user +file file +component { + user -d-> [UI] + [Main] -up-> [UI] + [Main] -d-> [Storage] + [UI] -> [Parser] + [Parser] -> [Command] + [Command] -> [Main] + [Command] <-> [StateManager] + [Storage] <-> [StateManager] + [Storage] -l-> file +} +@enduml \ No newline at end of file diff --git a/docs/diagrams/ExpenseClassDiagram.puml b/docs/diagrams/ExpenseClassDiagram.puml new file mode 100644 index 0000000000..4801804c3a --- /dev/null +++ b/docs/diagrams/ExpenseClassDiagram.puml @@ -0,0 +1,47 @@ +@startuml +!include Style.puml +hide circle +skinparam classAttributeIconSize 0 + +class Expense { + +Expense(transaction:Transaction, category:Category) + +getTransaction():Transaction + +getCategory():Category + +setTransaction(transaction:Transaction) + +setCategory(category:Category) + +generateNextRecurrence():Expense +} + +class Transaction { + - description:String + - amount:double + - date:LocalDate + - recurrence:TransactionRecurrence + - hasGeneratedNextRecurrence:boolean + +Transaction(description:String, amount:Double, date:LocalDate) + +getAmount():double + +setAmount(amount:double) + +getDate():LocalDate + +setDate(date:LocalDate) + +getDescription() + +setDescription(description:String) + +getRecurrence():TransactionRecurrence + +setRecurrence(recurrence:TransactionRecurrence) + +getHasGeneratedNextRecurrence():boolean + +setHasGeneratedNextRecurrence(boolean hasGeneratedNextRecurrence) + +shouldGenerateNextRecurrence():boolean + +generateNextRecurrence():Transaction +} + +class Category { + - name:String + +Category(name:String) + +getName():String + +setName(name:String) +} + +Expense "1" o--> "1" Transaction +Expense "*" o--> "1" Category + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/IncomeClassDiagram.puml b/docs/diagrams/IncomeClassDiagram.puml new file mode 100644 index 0000000000..d1a835b750 --- /dev/null +++ b/docs/diagrams/IncomeClassDiagram.puml @@ -0,0 +1,50 @@ +@startuml +!include Style.puml +hide circle +skinparam classAttributeIconSize 0 + +class Income { + +Income(transaction:Transaction, goal:Goal) + +getTransaction():Transaction + +getGoal():Goal + +setTransaction(transaction:Transaction) + +setGoal(goal:Goal) + +generateNextRecurrence():Income +} + +class Transaction { + - description:String + - amount:double + - date:LocalDate + - recurrence:TransactionRecurrence + - hasGeneratedNextRecurrence:boolean + +Transaction(description:String, amount:Double, date:LocalDate) + +getAmount():double + +setAmount(amount:double) + +getDate():LocalDate + +setDate(date:LocalDate) + +getDescription() + +setDescription(description:String) + +getRecurrence():TransactionRecurrence + +setRecurrence(recurrence:TransactionRecurrence) + +getHasGeneratedNextRecurrence():boolean + +setHasGeneratedNextRecurrence(boolean hasGeneratedNextRecurrence) + +shouldGenerateNextRecurrence():boolean + +generateNextRecurrence():Transaction +} + +class Goal { + - description:String + - amount:Double + +Goal(description:String, amount:double) + +getAmount():double + +setAmount(amount:double) + +getDescription():String + +setDescription(description:String) +} + +Income "1" o--> "1" Transaction +Income "*" o--> "1" Goal + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ListSequenceDiagram.png b/docs/diagrams/ListSequenceDiagram.png new file mode 100644 index 0000000000..8e1f794af1 Binary files /dev/null and b/docs/diagrams/ListSequenceDiagram.png differ diff --git a/docs/diagrams/ListSequenceDiagram.puml b/docs/diagrams/ListSequenceDiagram.puml new file mode 100644 index 0000000000..068c23489d --- /dev/null +++ b/docs/diagrams/ListSequenceDiagram.puml @@ -0,0 +1,124 @@ +@startuml +!define LOGIC_COLOR #7accff +!define LOGIC_COLOR_T1 #7777DB +!define LOGIC_COLOR_T2 #5252CE +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 + +participant ":ListCommand" order 1 +participant "StateManager" as s1 << class >> order 2 +participant ":StateManager" order 3 +participant ":Expense" as e2 order 6 +participant ":Category" as c order 7 +participant "ui:Ui" order 8 + +--> ":ListCommand": execute(ui: Ui) +activate ":ListCommand" #FFBBBB +":ListCommand" -> ":ListCommand": validateInput() +ref over ":ListCommand": validate user input +activate ":ListCommand"#FFBBBB +return +":ListCommand" -> ":ListCommand": listTypeHandler() +activate ":ListCommand" #FFBBBB +alt description != null && !description.isBlank() + ":ListCommand" -> ":ListCommand" : printTypeStatus() + activate ":ListCommand" #FFBBBB +alt description == "goal" + ":ListCommand" -> s1 : getStateManager() + activate s1 #FFBBBB + return :StateManager + ":ListCommand" -> ":StateManager" : getGoalsStatus() + activate ":StateManager" #FFBBBB + return map:HashMap + ":ListCommand" -> "ui:Ui" : printGoalsStatus(map) + activate "ui:Ui" #FFBBBB + return +else description == "category" + participant ":StateManager" as s1 << class >> + ":ListCommand" -> s1 : getStateManager() + activate s1 #FFBBBB + return :StateManager + ":ListCommand" -> ":StateManager" : getCategoriesStatus() + activate ":StateManager" #FFBBBB + return map:HashMap + ":ListCommand" -> "ui:Ui" : printCategoryStatus(map) + activate "ui:Ui" #FFBBBB + return +return +end +alt type == in + ":ListCommand" -> ":ListCommand" : checkInArgs() + activate ":ListCommand" #FFBBBB + return + ":ListCommand" -> ":ListCommand" : listIncome() + activate ":ListCommand" #FFBBBB + opt goal argument exists + ":ListCommand" -> ":ListCommand" : getArg("goal") + activate ":ListCommand" #FFBBBB + return filterGoal: String + end + ":ListCommand" -> s1 : getStateManager() + activate s1 #FFBBBB + return :StateManager + ":ListCommand" -> ":StateManager" : getAllIncomes() + activate ":StateManager" #FFBBBB + return incomeArray:ArrayList + opt week or month argument exists + ref over ":ListCommand": Filters incomeArray by week/month + end + loop through incomeArray + participant ":Income" as i2 order 4 + participant ":Goal" as g order 5 + ":ListCommand" -> i2 : getGoal() + activate i2 #FFBBBB + return :Goal + ":ListCommand" -> g : getDescription() + activate g #FFBBBB + return goal:String + alt filterGoal == null or filterGoal == goal + ref over ":ListCommand": Adds matching transactions to an ArrayList \n of ArrayList called printIncomes + end + end + ":ListCommand" -> "ui:Ui" : printList(printIncomes, "IN TRANSACTIONS") + activate "ui:Ui" #FFBBBB + return +else type == out + ":ListCommand" -> ":ListCommand" : checkOutArgs() + activate ":ListCommand" #FFBBBB + return + ":ListCommand" -> ":ListCommand" : listExpenses() + activate ":ListCommand" #FFBBBB + opt category argument exists + ":ListCommand" -> ":ListCommand" : getArg("category") + activate ":ListCommand" #FFBBBB + return filterCategory: String + end + ":ListCommand" -> s1 : getStateManager() + activate s1 #FFBBBB + return :StateManager + ":ListCommand" -> ":StateManager" : getAllExpenses() + activate ":StateManager" #FFBBBB + return expenseArray:ArrayList + opt week or month argument exists + ref over ":ListCommand": Filters expenseArray by week/month + end + loop through expenseArray + ":ListCommand" -> e2 : getCategory() + activate e2 #FFBBBB + return :Category + ":ListCommand" -> c : getDescription() + activate c #FFBBBB + return category:String + alt filterCategory == null or filterCategory == category + ref over ":ListCommand": Adds matching transactions to an ArrayList \n of ArrayList called printExpenses + end + end + ":ListCommand" -> "ui:Ui" : printList(printExpenses, "OUT TRANSACTIONS") + activate "ui:Ui" #FFBBBB + return +deactivate +return +end +return +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/ParserSequence.puml b/docs/diagrams/ParserSequence.puml new file mode 100644 index 0000000000..c540f239cc --- /dev/null +++ b/docs/diagrams/ParserSequence.puml @@ -0,0 +1,37 @@ +@startuml +!include Style.puml + +participant ":Main" as Main LOGIC_COLOR +participant ":Parser" as Parser LOGIC_COLOR +participant ":Command" as Command LOGIC_COLOR + +create Parser +Main -> Parser +activate Parser + +Main -> Parser : parse(userInput: String) + +Parser -> Parser : getCommandWord(userInput: String) +activate Parser +return commandWord: String + +Parser -> Parser : getDescription(userInput: String) +activate Parser +return description: String + +Parser -> Parser : getArguments(userInput: String) +activate Parser +return argsMap: HashMap + +Parser -> Parser : getCommand(commandWord: String, description: String, argsMap: HashMap) +activate Parser +create Command +Parser -> Command: Command(commandWord: String, description: String, argsMap: HashMap) +activate Command +return command: Command + +return command: Command + +return command: Command + +@enduml \ No newline at end of file diff --git a/docs/diagrams/Style.puml b/docs/diagrams/Style.puml new file mode 100644 index 0000000000..aaeee3333d --- /dev/null +++ b/docs/diagrams/Style.puml @@ -0,0 +1,5 @@ +!define LOGIC_COLOR #7accff +!define LOGIC_COLOR_T1 #7777DB +!define LOGIC_COLOR_T2 #5252CE +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 \ No newline at end of file diff --git a/docs/diagrams/category-feature-sequence.puml b/docs/diagrams/category-feature-sequence.puml new file mode 100644 index 0000000000..d418ce6825 --- /dev/null +++ b/docs/diagrams/category-feature-sequence.puml @@ -0,0 +1,92 @@ +@startuml +-> ":CategoryCommand": execute(ui: Ui) +activate ":CategoryCommand" #FFBBBB + +":CategoryCommand" -> ":CategoryCommand": validateInput() +activate ":CategoryCommand" #FFBBBB +ref over ":CategoryCommand": validate user's provided inputs +":CategoryCommand" --> ":CategoryCommand": inputType: String +deactivate + +alt inputType != null + alt inputType == "add" + ":CategoryCommand" -> ":CategoryCommand": getArg(ADD_COMMAND) + activate ":CategoryCommand" #FFBBBB + ":CategoryCommand" --> ":CategoryCommand": category: String + deactivate + + ":CategoryCommand" -> ":CategoryCommand": addCategory(category) + activate ":CategoryCommand" #FFBBBB + participant "StateManager" <> + ":CategoryCommand" -> "StateManager": getStateManager() + activate "StateManager" #FFBBBB + "StateManager" --> ":CategoryCommand": stateManager: StateManager + deactivate + ":CategoryCommand" -> "stateManager:StateManager": getCategoryIndex(category) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":CategoryCommand": index: int + deactivate + alt index != -1 + ref over ":CategoryCommand": throw error indicating category already exists + else index == -1 + create ":Category" + ":CategoryCommand" -> ":Category": new Category(category) + activate ":Category" #FFBBBB + ":Category" --> ":CategoryCommand": newCategory: Category + deactivate + ":CategoryCommand" -> "stateManager:StateManager": addCategory(newCategory) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":CategoryCommand" + deactivate + end + + ":CategoryCommand" -> ":Ui": print(successMessage) + activate ":Ui" #FFBBBB + ":Ui" --> ":CategoryCommand" + deactivate + + ":CategoryCommand" --> ":CategoryCommand" + deactivate + else inputType == "remove" + ":CategoryCommand" -> ":CategoryCommand": getArg(REMOVE_COMMAND) + activate ":CategoryCommand" #FFBBBB + ":CategoryCommand" --> ":CategoryCommand": category: String + deactivate + + ":CategoryCommand" -> ":CategoryCommand": removeCategory(category) + activate ":CategoryCommand" #FFBBBB + ":CategoryCommand" -> "StateManager": getStateManager() + activate "StateManager" #FFBBBB + "StateManager" --> ":CategoryCommand": stateManager: StateManager + deactivate + ":CategoryCommand" -> "stateManager:StateManager": getCategoryIndex(goalToRemove) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":CategoryCommand": index: int + deactivate + ":CategoryCommand" -> "stateManager:StateManager": getCategory(index) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":CategoryCommand": categoryToRemove: Category + deactivate + opt index != -1 + ":CategoryCommand" -> "stateManager:StateManager": unassignCategoryTransactions(categoryToRemove) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":CategoryCommand" + deactivate + ":CategoryCommand" -> "stateManager:StateManager": removeCategory(category) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":CategoryCommand": removedClassification: boolean + deactivate + end + opt index == -1 or removedClassification == false + ref over ":CategoryCommand": throw error indicating category removal failure + end + ":CategoryCommand" --> ":CategoryCommand" + deactivate + end +else inputType == null + ref over ":CategoryCommand": throw error indicating invalid input +end + +<-- ":CategoryCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/delete-transaction-feature-sequence.puml b/docs/diagrams/delete-transaction-feature-sequence.puml new file mode 100644 index 0000000000..f7e3142d5c --- /dev/null +++ b/docs/diagrams/delete-transaction-feature-sequence.puml @@ -0,0 +1,107 @@ +@startuml +-> ":RemoveTransactionCommand": execute(ui: UI) +activate ":RemoveTransactionCommand" #FFBBBB + +":RemoveTransactionCommand" -> ":RemoveTransactionCommand": throwIfInvalidDescOrArgs() +activate ":RemoveTransactionCommand" #FFBBBB +ref over ":RemoveTransactionCommand": validate user's provide inputs +":RemoveTransactionCommand" --> ":RemoveTransactionCommand" +deactivate + +":RemoveTransactionCommand" -> ":RemoveTransactionCommand": removeTransaction(ui) +activate ":RemoveTransactionCommand" #FFBBBB +":RemoveTransactionCommand" -> ":RemoveTransactionCommand": getArg("type") +activate ":RemoveTransactionCommand" #FFBBBB +":RemoveTransactionCommand" --> ":RemoveTransactionCommand": type: String +deactivate +":RemoveTransactionCommand" -> ":String": toLowerCase() +activate ":String" #FFBBBB +":String" --> ":RemoveTransactionCommand": type: String +deactivate + +":RemoveTransactionCommand" -> ":RemoveTransactionCommand": getTransactionMaxSize(type) +activate ":RemoveTransactionCommand" #FFBBBB +participant StateManager <> +":RemoveTransactionCommand" -> "StateManager": getStateManager() +activate "StateManager" #FFBBBB +"StateManager" --> ":RemoveTransactionCommand": stateManager: StateManager +deactivate +alt type == "in" + ":RemoveTransactionCommand" -> ":StateManager": getIncomesSize() + activate ":StateManager" #FFBBBB + ":StateManager" --> ":RemoveTransactionCommand": maxSize: int + deactivate +else type == "out" + ":RemoveTransactionCommand" -> ":StateManager": getExpensesSize() + activate ":StateManager" #FFBBBB + ":StateManager" --> ":RemoveTransactionCommand": maxSize: int + deactivate +end +":RemoveTransactionCommand" --> ":RemoveTransactionCommand": maxSize: int +deactivate + +":RemoveTransactionCommand" -> ":RemoveTransactionCommand": parseIdx(maxSize) +activate ":RemoveTransactionCommand" #FFBBBB +participant Integer <> +":RemoveTransactionCommand" -> ":RemoveTransactionCommand": getDescription() +activate ":RemoveTransactionCommand" #FFBBBB +":RemoveTransactionCommand" --> ":RemoveTransactionCommand": description: String +deactivate +":RemoveTransactionCommand" -> "Integer": parseInt(description) +activate "Integer" #FFBBBB +"Integer" --> ":RemoveTransactionCommand": index: int +deactivate +opt index < 1 || index > maxSize + ref over ":RemoveTransactionCommand": throw error indicating invalid index +end +":RemoveTransactionCommand" --> ":RemoveTransactionCommand": index: int +deactivate + +":RemoveTransactionCommand" -> "StateManager": getStateManager() +activate "StateManager" #FFBBBB +"StateManager" --> ":RemoveTransactionCommand": stateManager: StateManager +deactivate + +alt type == "in" + ":RemoveTransactionCommand" -> "stateManager:StateManager": getIncome(index) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":RemoveTransactionCommand": incomeEntry: Income + deactivate + ":RemoveTransactionCommand" -> ":Income": getDescription() + activate ":Income" #FFBBBB + ":Income" --> ":RemoveTransactionCommand": transactionDescription: String + deactivate + ":RemoveTransactionCommand" -> "stateManager:StateManager": removeIncome(incomeEntry) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":RemoveTransactionCommand": isSuccess: bool + deactivate +else type == "out" + ":RemoveTransactionCommand" -> "stateManager:StateManager": getExpense(index) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":RemoveTransactionCommand": expenseEntry: Expense + deactivate + ":RemoveTransactionCommand" -> ":Expense": getDescription() + activate ":Expense" #FFBBBB + ":Expense" --> ":RemoveTransactionCommand": transactionDescription: String + deactivate + ":RemoveTransactionCommand" -> "stateManager:StateManager": removeExpense(expenseEntry) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":RemoveTransactionCommand": isSuccess: bool + deactivate +end + +alt isSuccess + ":RemoveTransactionCommand" -> ":RemoveTransactionCommand": printSuccess(ui, transactionDescription, index+1) + activate ":RemoveTransactionCommand" #FFBBBB + ":RemoveTransactionCommand" --> ":RemoveTransactionCommand" + deactivate +else !isSuccess + ref over ":RemoveTransactionCommand": throw error indicating failure +end + +":RemoveTransactionCommand" --> ":RemoveTransactionCommand" +deactivate + +<-- ":RemoveTransactionCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/export-feature-sequence-expense-data.puml b/docs/diagrams/export-feature-sequence-expense-data.puml new file mode 100644 index 0000000000..13ca0fa374 --- /dev/null +++ b/docs/diagrams/export-feature-sequence-expense-data.puml @@ -0,0 +1,35 @@ +@startuml +mainframe **sd** export expense data +-> ":ExportCommand": exportExpenseData() +activate ":ExportCommand" #FFBBBB +loop for every Income in incomeArray + ":ExportCommand" -> ":Income": getTransaction() + activate ":Income" #FFBBBB + ":Income" --> ":ExportCommand": currentTransaction: Transaction + deactivate + create ":String[]" + ":ExportCommand" -> ":String[]": new String[DATA_LENGTH] + activate ":String[]" #FFBBBB + ":String[]" --> ":ExportCommand": row: String[] + deactivate + ":ExportCommand" -> ":Income": getCategory() + activate ":Income" #FFBBBB + ":Income" --> ":ExportCommand": category: Category + deactivate + ":ExportCommand" -> ":Category": getName() + activate ":Category" #FFBBBB + ":Category" --> ":ExportCommand": row[CATEGORY]: String + deactivate + ":ExportCommand" -> ":ExportCommand": extractTransactionData(currentTransaction, row) + activate ":ExportCommand" #FFBBBB + ref over ":ExportCommand": extract transaction data + ":ExportCommand" --> ":ExportCommand": row: String[] + deactivate + ":ExportCommand" -> "csvFile:CsvWriter": write(row) + activate "csvFile:CsvWriter" #FFBBBB + "csvFile:CsvWriter" --> ":ExportCommand" + deactivate +end +<-- ":ExportCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/export-feature-sequence-extract.puml b/docs/diagrams/export-feature-sequence-extract.puml new file mode 100644 index 0000000000..73aa14367c --- /dev/null +++ b/docs/diagrams/export-feature-sequence-extract.puml @@ -0,0 +1,37 @@ +@startuml +mainframe **sd** extract transaction data +-> ":ExportCommand": extractTransactionData(transaction, row) +activate ":ExportCommand" #FFBBBB + +":ExportCommand" -> ":Transaction": getDescription() +activate ":Transaction" #FFBBBB +":Transaction" --> ":ExportCommand": description: String +deactivate +":ExportCommand" -> ":Transaction": getDate() +activate ":Transaction" #FFBBBB +":Transaction" --> ":ExportCommand": dateObj: LocalDate +deactivate +":ExportCommand" -> ":LocalDate": toString() +activate ":LocalDate" #FFBBBB +":LocalDate" --> ":ExportCommand": date: String +deactivate +":ExportCommand" -> ":Transaction": getAmount() +activate ":Transaction" #FFBBBB +":Transaction" --> ":ExportCommand": amountDouble: Double +deactivate +":ExportCommand" -> ":Ui": formatAmount(amountDouble) +activate ":Ui" #FFBBBB +":Ui" --> ":ExportCommand": amount: String +deactivate +":ExportCommand" -> ":Transaction": getRecurrence() +activate ":Transaction" #FFBBBB +":Transaction" --> ":ExportCommand": recurrenceObj: TransactionRecurrence +deactivate +":ExportCommand" -> ":TransactionRecurrence": toString() +activate ":TransactionRecurrence" #FFBBBB +":TransactionRecurrence" --> ":ExportCommand": recurrence: String +deactivate + +<-- ":ExportCommand": row: String[] +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/export-feature-sequence-income-data.puml b/docs/diagrams/export-feature-sequence-income-data.puml new file mode 100644 index 0000000000..7257e7be78 --- /dev/null +++ b/docs/diagrams/export-feature-sequence-income-data.puml @@ -0,0 +1,35 @@ +@startuml +mainframe **sd** export income data +-> ":ExportCommand": exportIncomeData() +activate ":ExportCommand" #FFBBBB +loop for every Income in incomeArray + ":ExportCommand" -> ":Income": getTransaction() + activate ":Income" #FFBBBB + ":Income" --> ":ExportCommand": currentTransaction: Transaction + deactivate + create ":String[]" + ":ExportCommand" -> ":String[]": new String[DATA_LENGTH] + activate ":String[]" #FFBBBB + ":String[]" --> ":ExportCommand": row: String[] + deactivate + ":ExportCommand" -> ":Income": getGoal() + activate ":Income" #FFBBBB + ":Income" --> ":ExportCommand": goal: Goal + deactivate + ":ExportCommand" -> ":Goal": getDescription() + activate ":Goal" #FFBBBB + ":Goal" --> ":ExportCommand": row[GOAL]: String + deactivate + ":ExportCommand" -> ":ExportCommand": extractTransactionData(currentTransaction, row) + activate ":ExportCommand" #FFBBBB + ref over ":ExportCommand": extract transaction data + ":ExportCommand" --> ":ExportCommand": row: String[] + deactivate + ":ExportCommand" -> "csvFile:CsvWriter": write(row) + activate "csvFile:CsvWriter" #FFBBBB + "csvFile:CsvWriter" --> ":ExportCommand" + deactivate +end +<-- ":ExportCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/export-feature-sequence.puml b/docs/diagrams/export-feature-sequence.puml new file mode 100644 index 0000000000..dd7b40a192 --- /dev/null +++ b/docs/diagrams/export-feature-sequence.puml @@ -0,0 +1,68 @@ +@startuml +-> ":ExportCommand": execute(ui: Ui) +activate ":ExportCommand" #FFBBBB + +":ExportCommand" -> ":ExportCommand": checkType() +activate ":ExportCommand" #FFBBBB +":ExportCommand" -> ":ExportCommand": getArg(TYPE_ARG) +activate ":ExportCommand" #FFBBBB +":ExportCommand" --> ":ExportCommand": type: String +deactivate +":ExportCommand" --> ":ExportCommand": transactionType: TransactionType +deactivate + +alt transactionType != ERROR + ":ExportCommand" -> ":ExportCommand": writeHeader() + activate ":ExportCommand" #FFBBBB + ":ExportCommand" -> "csvFile:CsvWriter": write(HEADERS) + activate "csvFile:CsvWriter" #FFBBBB + "csvFile:CsvWriter" --> ":ExportCommand" + deactivate + ":ExportCommand" --> ":ExportCommand" + deactivate + ":ExportCommand" -> ":ExportCommand": exportData(transactionType) + activate ":ExportCommand" #FFBBBB + alt transactionType == IN + ":ExportCommand" -> ":ExportCommand": exportIncomeData() + activate ":ExportCommand" #FFBBBB + ref over ":ExportCommand": export income data + ":ExportCommand" --> ":ExportCommand" + deactivate + else transactionType == OUT + ":ExportCommand" -> ":ExportCommand": exportExpenseData() + activate ":ExportCommand" #FFBBBB + ref over ":ExportCommand": export expense data + ":ExportCommand" --> ":ExportCommand" + deactivate + else else + ":ExportCommand" -> ":ExportCommand": exportIncomeData() + activate ":ExportCommand" #FFBBBB + ref over ":ExportCommand": export income data + ":ExportCommand" --> ":ExportCommand" + deactivate + ":ExportCommand" -> ":ExportCommand": exportExpenseData() + activate ":ExportCommand" #FFBBBB + ref over ":ExportCommand": export expense data + ":ExportCommand" --> ":ExportCommand" + deactivate + end + ":ExportCommand" --> ":ExportCommand" + deactivate + ":ExportCommand" -> ":Ui": print(SUCCESSFUL_MSG) + activate ":Ui" #FFBBBB + ":Ui" --> ":ExportCommand" + deactivate + ":ExportCommand" -> "csvFile:CsvWriter": close() + activate "csvFile:CsvWriter" #FFBBBB + "csvFile:CsvWriter" --> ":ExportCommand" + deactivate +else else + ":ExportCommand" -> ":Ui": print(WRONG_TYPE_MSG) + activate ":Ui" #FFBBBB + ":Ui" --> ":ExportCommand" + deactivate +end + +<-- ":ExportCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/goal-feature-sequence.puml b/docs/diagrams/goal-feature-sequence.puml new file mode 100644 index 0000000000..3e8911aab8 --- /dev/null +++ b/docs/diagrams/goal-feature-sequence.puml @@ -0,0 +1,99 @@ +@startuml +-> ":GoalCommand": execute(ui: Ui) +activate ":GoalCommand" #FFBBBB + +":GoalCommand" -> ":GoalCommand": validateInput() +activate ":GoalCommand" #FFBBBB +ref over ":GoalCommand": validate user's provided inputs +":GoalCommand" --> ":GoalCommand": inputType: String +deactivate + +alt inputType != null + alt inputType == "add" + ":GoalCommand" -> ":GoalCommand": validateAmount() + activate ":GoalCommand" #FFBBBB + ":GoalCommand" --> ":GoalCommand" + deactivate + ":GoalCommand" -> ":GoalCommand": getArg(ADD_COMMAND) + activate ":GoalCommand" #FFBBBB + ":GoalCommand" --> ":GoalCommand": goalName: String + deactivate + participant "Parser" <> + ":GoalCommand" -> ":GoalCommand": getArg(AMOUNT) + activate ":GoalCommand" #FFBBBB + ":GoalCommand" --> ":GoalCommand": amountStr: String + deactivate + ":GoalCommand" -> "Parser": parseNonNegativeDouble(amountStr) + activate "Parser" #FFBBBB + "Parser" --> ":GoalCommand": amount: Double + deactivate + + ":GoalCommand" -> ":GoalCommand": addGoal(goalName, amount) + activate ":GoalCommand" #FFBBBB + participant "StateManager" <> + ":GoalCommand" -> "StateManager": getStateManager() + activate "StateManager" #FFBBBB + "StateManager" --> ":GoalCommand": stateManager: StateManager + deactivate + alt goal does already not exist + create ":Goal" + ":GoalCommand" -> ":Goal": new Goal(goalName, amount) + activate ":Goal" #FFBBBB + ":Goal" --> ":GoalCommand": goal: Goal + deactivate + ":GoalCommand" -> "stateManager:StateManager": addGoal(goal) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":GoalCommand" + deactivate + ":GoalCommand" --> ":GoalCommand" + deactivate + ":GoalCommand" -> ":Ui": print(successMessage) + activate ":Ui" #FFBBBB + ":Ui" --> ":GoalCommand" + deactivate + else goal already exists + ref over ":GoalCommand": throw error indicating goal exists + end + else inputType == "remove" + ":GoalCommand" -> ":GoalCommand": getArg(REMOVE_COMMAND) + activate ":GoalCommand" #FFBBBB + ":GoalCommand" --> ":GoalCommand": goalName: String + deactivate + + ":GoalCommand" -> ":GoalCommand": removeGoal(goalName) + activate ":GoalCommand" #FFBBBB + ":GoalCommand" -> "StateManager": getStateManager() + activate "StateManager" #FFBBBB + "StateManager" --> ":GoalCommand": stateManager: StateManager + deactivate + ":GoalCommand" -> "stateManager:StateManager": getGoalIndex(goalToRemove) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":GoalCommand": index: int + deactivate + ":GoalCommand" -> "stateManager:StateManager": getGoal(index) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":GoalCommand": goalToRemove: Goal + deactivate + opt index != -1 + ":GoalCommand" -> "stateManager:StateManager": unassignGoalTransactions(goalToRemove) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":GoalCommand": + deactivate + ":GoalCommand" -> "stateManager:StateManager": removeGoal(goalToRemove) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":GoalCommand": removedGoal: boolean + deactivate + end + opt index == -1 or removedGoal == false + ref over ":GoalCommand": throw error indicating goal removal failure + end + ":GoalCommand" --> ":GoalCommand" + deactivate + end +else inputType == null + ref over ":GoalCommand": throw error indicating invalid input +end + +<-- ":GoalCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/storage-class-diagram.puml b/docs/diagrams/storage-class-diagram.puml new file mode 100644 index 0000000000..5bcf71fa41 --- /dev/null +++ b/docs/diagrams/storage-class-diagram.puml @@ -0,0 +1,76 @@ +@startuml +hide circle +skinparam classAttributeIconSize 0 +class Storage { + - {static} GOAL_STORAGE_FILENAME:String = "goal-store.csv" + - {static} CATEGORY_STORAGE_FILENAME:String = "category-store.csv" + - {static} INCOME_STORAGE_FILENAME:String = "income-store.csv" + - {static} EXPENSE_STORAGE_FILENAME:String = "expense-store.csv" + - {static} DATE_PATTERN:String = "dd/MM/yyyy" + - {static} FORMATTER:DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_PATTERN) + - {static} FAILED_CONVERT_TO_NON_NEG_DOUBLE:String = "Cannot convert amount into Double type in " + - {static} FAILED_CONVERT_TO_LOCALDATE:String = "Cannot convert date into LocalDate type in " + - {static} FAILED_CONVERT_BOOLEAN:String = "Cannot convert string into boolean type in " + - {static} GOAL_HEADER:String[] = {"Description", "Amount"} + - {static} CATEGORY_HEADER:String[] = {"Name"} + - {static} INCOME_HEADER:String[] = {"Description", "Amount", "Date", "Goal", "Recurrence", "Has Next Recurrence"} + - {static} EXPENSE_HEADER:String[] = {"Description", "Amount", "Date", "Category", "Recurrence", "Has Next Reccurence"} + - {static} DESCRIPTION:int = 0 + - {static} AMOUNT:int = 1 + - {static} DATE:int = 2 + - {static} GOAL:int = 3 + - {static} CATEGORY:int = 3 + - {static} RECURRENCE:int = 4 + - {static} HAS_NEXT_RECURRENCE:int = 5 + ---- + + Storage() + + validRow(row:String[]):boolean + + validDate(dateStr:String, fileName:String):LocalDate + + validBoolean(booleanStr:String):boolean + + convertToGoal(name:String):GOAL + + convertToCategory(name:String):Category + + prepareTransaction(description:String, amount:double, date:LocalDate, recurrence:String, hasRecurrence:String):Transaction + + loadGoal() + + loadCategory() + + loadIncome() + + loadExpense() + + load() + + saveGoal() + + saveCategory() + + saveIncome() + + saveExpense() + + save() +} + +class CsvReader { + + CsvReader(filePath:String) + + readLine():String[] + + close() +} + +package "com.opencsv" #DDDDDD { + class CSVReader { + + readNext():String[] + } + class CSVWriter { + + writeNext(data:String[]) + } +} + +class CsvWriter { + + CsvWriter(fullPath:String) + + CsvWriter(fullPath:String, isAppend:boolean) + + write(data:String[]) + + close() +} + +CsvReader ---> "0..1" CSVReader : > have +CsvWriter --> "0..1" CSVWriter : > have + +Storage --> "0..4" CsvReader : > uses +Storage --> "0..4" CsvWriter : > uses + +note left of "com.opencsv" + This is an 3rd Party Library (OpenCSV) +endnote +@enduml diff --git a/docs/diagrams/summary-feature-sequence.puml b/docs/diagrams/summary-feature-sequence.puml new file mode 100644 index 0000000000..2cdc9a7882 --- /dev/null +++ b/docs/diagrams/summary-feature-sequence.puml @@ -0,0 +1,116 @@ +@startuml +-> ":SummaryCommand": execute(ui: Ui) +activate ":SummaryCommand" #FFBBBB + +":SummaryCommand" -> ":SummaryCommand": throwIfInvalidDescOrArgs() +activate ":SummaryCommand" #FFBBBB +ref over ":SummaryCommand": validate user's provided inputs +":SummaryCommand" --> ":SummaryCommand" +deactivate + +":SummaryCommand" -> ":SummaryCommand": getFilter() +activate ":SummaryCommand" #FFBBBB +":SummaryCommand" -> ":SummaryCommand": getArgs() +activate ":SummaryCommand" #FFBBBB +":SummaryCommand" --> ":SummaryCommand": args: HashMap +deactivate +":SummaryCommand" --> ":SummaryCommand" +deactivate + +":SummaryCommand" -> ":SummaryCommand": printSummary() +activate ":SummaryCommand" #FFBBBB +":SummaryCommand" -> ":SummaryCommand": getArg(TYPE_ARG) +activate ":SummaryCommand" #FFBBBB +":SummaryCommand" --> ":SummaryCommand": type: String +deactivate +alt type == TYPE_IN + ":SummaryCommand" -> ":SummaryCommand": getIncomeSummary() + activate ":SummaryCommand" #FFBBBB + participant StateManager <> + ":SummaryCommand" -> "StateManager": getStateManager() + activate StateManager #FFBBBB + "StateManager" --> ":SummaryCommand": stateManager: StateManager + deactivate + ":SummaryCommand" -> "stateManager:StateManager": getAllIncomes() + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":SummaryCommand": incomeArray: ArrayList + deactivate + alt incomeArray not empty + opt filterByDay || filterByWeek || filterByMonth + ":SummaryCommand" -> ":SummaryCommand": filterIncome(incomeArray) + activate ":SummaryCommand" #FFBBBB + ref over ":SummaryCommand": filter incomes by day, week or month, if multiple stated, the former order takes precedence + ":SummaryCommand" --> ":SummaryCommand": incomeArray: ArrayList + deactivate + end + loop every income in incomeArray + ":SummaryCommand" -> ":Income": getTransaction() + activate ":Income" #FFBBBB + ":Income" --> ":SummaryCommand": transaction: Transaction + deactivate + ":SummaryCommand" -> ":Transaction": getAmount() + activate ":Transaction" #FFBBBB + ":Transaction" --> ":SummaryCommand": amount: double + deactivate + end + else else + ref over ":SummaryCommand": throw error indicating empty incomes + end + ":SummaryCommand" --> ":SummaryCommand": totalSum: double + deactivate + + ":SummaryCommand" -> ":SummaryCommand": getSummaryMsg(TYPE_IN, totalSum) + activate ":SummaryCommand" #FFBBBB + ":SummaryCommand" --> ":SummaryCommand": msg: String + deactivate +else type == TYPE_OUT + ":SummaryCommand" -> ":SummaryCommand": getExpenseSummary() + activate ":SummaryCommand" #FFBBBB + ":SummaryCommand" -> "StateManager": getStateManager() + activate StateManager #FFBBBB + "StateManager" --> ":SummaryCommand": stateManager: StateManager + deactivate + ":SummaryCommand" -> "stateManager:StateManager": getAllExpenses() + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":SummaryCommand": expenseArray: ArrayList + deactivate + alt expenseArray not empty + opt filterByDay || filterByWeek || filterByMonth + ":SummaryCommand" -> ":SummaryCommand": filterExpense(expenseArray) + activate ":SummaryCommand" #FFBBBB + ref over ":SummaryCommand": filter expenses by day, week or month, if multiple stated, the former order takes precedence + ":SummaryCommand" --> ":SummaryCommand": expenseArray: ArrayList + deactivate + end + loop every expense in expenseArray + ":SummaryCommand" -> ":Expense": getTransaction() + activate ":Expense" #FFBBBB + ":Expense" --> ":SummaryCommand": transaction: Transaction + deactivate + ":SummaryCommand" -> ":Transaction": getAmount() + activate ":Transaction" #FFBBBB + ":Transaction" --> ":SummaryCommand": amount: double + deactivate + end + else else + ref over ":SummaryCommand": throw error indicating empty expenses + end + ":SummaryCommand" --> ":SummaryCommand": totalSum: double + deactivate + + ":SummaryCommand" -> ":SummaryCommand": getSummaryMsg(TYPE_OUT, totalSum) + activate ":SummaryCommand" #FFBBBB + ":SummaryCommand" --> ":SummaryCommand": msg: String + deactivate +end +":SummaryCommand" -> ":Ui": print(msg) +activate ":Ui" #FFBBBB +":Ui" --> ":SummaryCommand" +deactivate + +":SummaryCommand" --> ":SummaryCommand" +deactivate + +<-- ":SummaryCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/transaction-tracking-sequence.puml b/docs/diagrams/transaction-tracking-sequence.puml new file mode 100644 index 0000000000..1888c85c41 --- /dev/null +++ b/docs/diagrams/transaction-tracking-sequence.puml @@ -0,0 +1,89 @@ +@startuml +-> ":AddIncomeCommand": execute(ui: Ui) +activate ":AddIncomeCommand" #FFBBBB +":AddIncomeCommand" -> ":AddIncomeCommand": throwIfInvalidDescOrArgs() +ref over ":AddIncomeCommand": validate user's provided inputs +activate ":AddIncomeCommand" #FFBBBB +":AddIncomeCommand" --> ":AddIncomeCommand" +deactivate + +":AddIncomeCommand" -> ":AddIncomeCommand": prepareTransaction() +activate ":AddIncomeCommand" #FFBBBB +create ":Transaction" +":AddIncomeCommand" -> ":Transaction": new Transaction(description, amount, date) +activate ":Transaction" #FFBBBB +":Transaction" --> ":AddIncomeCommand": transaction: Transaction +deactivate + +":AddIncomeCommand" --> ":AddIncomeCommand": transaction: Transaction +deactivate + +":AddIncomeCommand" -> ":AddIncomeCommand": addNewIncome(transaction) +activate ":AddIncomeCommand" #FFBBBB + +":AddIncomeCommand" -> ":AddIncomeCommand": handleGoal() +activate ":AddIncomeCommand" #FFBBBB + +":AddIncomeCommand" -> ":AddIncomeCommand": getArg(GOAL_ARG) +activate ":AddIncomeCommand" #FFBBBB +":AddIncomeCommand" --> ":AddIncomeCommand": goalArg: String +deactivate + +participant "StateManager" <> +":AddIncomeCommand" -> "StateManager": getStateManager() +activate "StateManager" #FFBBBB +"StateManager" --> ":AddIncomeCommand": stateManager: StateManager +deactivate + +alt goal == null or goal == "Uncategorised" + ":AddIncomeCommand" -> "stateManager:StateManager": getUncategorisedGoal() + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":AddIncomeCommand": goal: Goal + deactivate +else goal exists + ":AddIncomeCommand" -> "stateManager:StateManager": getGoalIndex(goalArg) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":AddIncomeCommand": index: int + deactivate + ":AddIncomeCommand" -> "stateManager:StateManager": getGoal(index) + activate "stateManager:StateManager" #FFBBBB + "stateManager:StateManager" --> ":AddIncomeCommand": goal: Goal + deactivate +end +":AddIncomeCommand" --> ":AddIncomeCommand": goal: Goal +deactivate + +create ":Income" +":AddIncomeCommand" -> ":Income": new Income(transaction, goal) +activate ":Income" #FFBBBB +":Income" --> ":AddIncomeCommand": income: Income +deactivate +":AddIncomeCommand" -> "StateManager": getStateManager() +activate "StateManager" #FFBBBB +"StateManager" --> ":AddIncomeCommand": stateManager: StateManager +deactivate +":AddIncomeCommand" -> "stateManager:StateManager": addIncome(income) +activate "stateManager:StateManager" #FFBBBB +"stateManager:StateManager" --> ":AddIncomeCommand" +deactivate +":AddIncomeCommand" --> ":AddIncomeCommand": income: Income +deactivate + +":AddIncomeCommand" -> ":AddIncomeCommand": printSuccess(ui, income) +activate ":AddIncomeCommand" #FFBBBB +":AddIncomeCommand" --> ":AddIncomeCommand" +deactivate + +":AddIncomeCommand" -> "StateManager": getStateManager() +activate "StateManager" #FFBBBB +"StateManager" --> ":AddIncomeCommand": stateManager: StateManager +deactivate + +":AddIncomeCommand" -> "stateManager:StateManager": sortIncomes() +activate "stateManager:StateManager" #FFBBBB +"stateManager:StateManager" --> ":AddIncomeCommand" +deactivate + +<-- ":AddIncomeCommand" +deactivate +@enduml \ No newline at end of file diff --git a/docs/diagrams/ui-class-diagram.puml b/docs/diagrams/ui-class-diagram.puml new file mode 100644 index 0000000000..b39b790f27 --- /dev/null +++ b/docs/diagrams/ui-class-diagram.puml @@ -0,0 +1,45 @@ +@startuml +hide circle +skinparam classAttributeIconSize 0 + +class Ui { + {static} + COLUMN_WIDTH: int = 10 + {static} + LIST_COLUMN_WIDTH: int = 30 + + + Ui() + + Ui(outputstream: OutputStream) + + printTableRow(rowValues: ArrayList): void + + printTableRow(rowValues: ArrayList, headers: String[]): void + + printTableRow(rowValues: ArrayList, headers: String[], customWidths: Integer[]): void + + printTableRows(rows: ArrayList>): void + + printTableRows(rows: ArrayList>, headers: String[]): void + + printTableRows(rows: ArrayList>, headers: String[], customWidths: Integer[]): void + + printTableHeader(headers: String[], customWidths: Integer[]) + + formatAmount(value: Double): String + + print(value: String): String + + printGreeting(): String + + printBye(): String + + readUserInput(): String + + close(): void +} + +package "java.io" #DDDDDD { + class "{abstract}\nOutputStream" { + + abstract write(b: int): void + + write(b: byte[]): void + + flush(): void + } +} + + +package "java.util" #DDDDDD { + class Scanner { + + Scanner(i: InputStream) + + nextLine(): String + + close(): void + } +} + +Ui -[dashed]-> "{abstract}\nOutputStream" :> uses +Ui --> Scanner :> uses +@enduml \ No newline at end of file diff --git a/docs/diagrams/ui-class-diagramv2.png b/docs/diagrams/ui-class-diagramv2.png new file mode 100644 index 0000000000..db5f028d38 Binary files /dev/null and b/docs/diagrams/ui-class-diagramv2.png differ diff --git a/docs/diagrams/ui-class-diagramv2.puml b/docs/diagrams/ui-class-diagramv2.puml new file mode 100644 index 0000000000..f09ae678ef --- /dev/null +++ b/docs/diagrams/ui-class-diagramv2.puml @@ -0,0 +1,49 @@ +@startuml +hide circle +skinparam classAttributeIconSize 0 + +class Ui { + {static} + COLUMN_WIDTH: int = 10 + {static} + TYPE_COLUMN_WIDTH: int = 20 + {static} + LIST_COLUMN_WIDTH: int = 30 + + + Ui() + + Ui(outputstream: OutputStream) + + printTableRow(rowValues: ArrayList): void + + printTableRow(rowValues: ArrayList, headers: String[]): void + + printTableRow(rowValues: ArrayList, headers: String[], customWidths: Integer[]): void + + printTableRows(rows: ArrayList>): void + + printTableRows(rows: ArrayList>, headers: String[]): void + + printTableRows(rows: ArrayList>, headers: String[], customWidths: Integer[]): void + + printTableHeader(headers: String[], customWidths: Integer[]) + + formatAmount(value: Double): String + + print(value: String): String + + printGreeting(): String + + printBye(): String + + readUserInput(): String + + listTransactions(list: ArrayList>, headers: String[], headerMessage: String): void + + printGoalsStatus(goalsMap: HashMap): void + + printCategoryStatus(categoryMap: HashMap): void + + close(): void +} + +package "java.io" #DDDDDD { + class "{abstract}\nOutputStream" { + + abstract write(b: int): void + + write(b: byte[]): void + + flush(): void + } +} + + +package "java.util" #DDDDDD { + class Scanner { + + Scanner(i: InputStream) + + nextLine(): String + + close(): void + } +} + +Ui -[dashed]-> "{abstract}\nOutputStream" :> uses +Ui --> Scanner :> uses +@enduml \ No newline at end of file diff --git a/docs/diagrams/ui-sequence-diagram.puml b/docs/diagrams/ui-sequence-diagram.puml new file mode 100644 index 0000000000..124192bf0b --- /dev/null +++ b/docs/diagrams/ui-sequence-diagram.puml @@ -0,0 +1,43 @@ +@startuml +activate ":Main" #FFBBBB +":Main" -> ":UI": readUserInput() +activate ":UI" #FFBBBB + + +":UI" -> ":User": Gets user input +activate ":User" #FFBBBB +":User" --> ":UI": userInput: String +deactivate +":UI" --> ":Main": userInput: String +deactivate + +create ":Parser" +":Main" -> ":Parser": new Parser() +activate ":Parser" #FFBBBB +":Parser" --> ":Main": p: Parser +deactivate ":Parser" + +":Main" -> ":Parser": p.parse(userInput: String) +activate ":Parser" #FFBBBB +create ":Command" +":Parser" -> ":Command": new Command() +activate ":Command" #FFBBBB +":Command" --> ":Parser": c: Command +deactivate +":Parser" --> ":Main": command: Command +destroy ":Parser" +note over ":Parser": ":Parser" lifeline\nends here + +":Main" -> ":Command": command.execute(ui: UI) +activate ":Command" #FFBBBB +":Command" -> ":UI": Calls instance methods to print to UI +activate ":UI" #FFBBBB +":UI" -> ":User": Prints to terminal UI +activate ":User" #FFBBBB +":User" --> ":UI" +deactivate +":UI" --> ":Command" +deactivate +":Command" --> ":Main" +destroy ":Command" +@enduml \ No newline at end of file diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png new file mode 100644 index 0000000000..698938c8e4 Binary files /dev/null and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/ExpenseClassDiagram.png b/docs/images/ExpenseClassDiagram.png new file mode 100644 index 0000000000..4a8571f07c Binary files /dev/null and b/docs/images/ExpenseClassDiagram.png differ diff --git a/docs/images/IncomeClassDiagram.png b/docs/images/IncomeClassDiagram.png new file mode 100644 index 0000000000..fa19eeb73e Binary files /dev/null and b/docs/images/IncomeClassDiagram.png differ diff --git a/docs/images/ListSequenceDiagram.png b/docs/images/ListSequenceDiagram.png new file mode 100644 index 0000000000..507846f6e4 Binary files /dev/null and b/docs/images/ListSequenceDiagram.png differ diff --git a/docs/images/ParserSequence.png b/docs/images/ParserSequence.png new file mode 100644 index 0000000000..c1b1c7cfc1 Binary files /dev/null and b/docs/images/ParserSequence.png differ diff --git a/docs/images/category-feature-sequence.png b/docs/images/category-feature-sequence.png new file mode 100644 index 0000000000..c69740582f Binary files /dev/null and b/docs/images/category-feature-sequence.png differ diff --git a/docs/images/cs2113-storage-class.png b/docs/images/cs2113-storage-class.png new file mode 100644 index 0000000000..06ad2c3ca3 Binary files /dev/null and b/docs/images/cs2113-storage-class.png differ diff --git a/docs/images/cs2113-ui-sequence-original.png b/docs/images/cs2113-ui-sequence-original.png new file mode 100644 index 0000000000..cf6add66ad Binary files /dev/null and b/docs/images/cs2113-ui-sequence-original.png differ diff --git a/docs/images/cs2113-ui-sequence.png b/docs/images/cs2113-ui-sequence.png new file mode 100644 index 0000000000..53a60ce0d5 Binary files /dev/null and b/docs/images/cs2113-ui-sequence.png differ diff --git a/docs/images/delete-transaction-feature-sequence.png b/docs/images/delete-transaction-feature-sequence.png new file mode 100644 index 0000000000..a8558decc2 Binary files /dev/null and b/docs/images/delete-transaction-feature-sequence.png differ diff --git a/docs/images/export-feature-sequence-expense-data.png b/docs/images/export-feature-sequence-expense-data.png new file mode 100644 index 0000000000..cf2a9b3f3e Binary files /dev/null and b/docs/images/export-feature-sequence-expense-data.png differ diff --git a/docs/images/export-feature-sequence-extract.png b/docs/images/export-feature-sequence-extract.png new file mode 100644 index 0000000000..e60122ee0f Binary files /dev/null and b/docs/images/export-feature-sequence-extract.png differ diff --git a/docs/images/export-feature-sequence-income-data.png b/docs/images/export-feature-sequence-income-data.png new file mode 100644 index 0000000000..212339f56c Binary files /dev/null and b/docs/images/export-feature-sequence-income-data.png differ diff --git a/docs/images/export-feature-sequence.png b/docs/images/export-feature-sequence.png new file mode 100644 index 0000000000..422bc29953 Binary files /dev/null and b/docs/images/export-feature-sequence.png differ diff --git a/docs/images/goal-feature-sequence.png b/docs/images/goal-feature-sequence.png new file mode 100644 index 0000000000..ca2859fac7 Binary files /dev/null and b/docs/images/goal-feature-sequence.png differ diff --git a/docs/images/summary-feature-sequence.png b/docs/images/summary-feature-sequence.png new file mode 100644 index 0000000000..3033f23b6f Binary files /dev/null and b/docs/images/summary-feature-sequence.png differ diff --git a/docs/images/transaction-tracking-sequence.png b/docs/images/transaction-tracking-sequence.png new file mode 100644 index 0000000000..1a6ad638c2 Binary files /dev/null and b/docs/images/transaction-tracking-sequence.png differ diff --git a/docs/images/ui-class-diagram.png b/docs/images/ui-class-diagram.png new file mode 100644 index 0000000000..2e682546cb Binary files /dev/null and b/docs/images/ui-class-diagram.png differ diff --git a/docs/images/ui-class-diagramv2.png b/docs/images/ui-class-diagramv2.png new file mode 100644 index 0000000000..6bc3b261a3 Binary files /dev/null and b/docs/images/ui-class-diagramv2.png differ diff --git a/docs/team/choonsiang.md b/docs/team/choonsiang.md new file mode 100644 index 0000000000..202da11887 --- /dev/null +++ b/docs/team/choonsiang.md @@ -0,0 +1,45 @@ +# Chan Choon Siang's Project Portfolio Page + +## Project: FinText + +FinText is a **Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track and manage +their saving/spending,** and view a summary of their daily/weekly/monthly transactions. + +Given below are my contributions to the project. + +- New Feature: Added a Parser to parse user command and arguments. + - What it does: The parser will first parse the user input to separate the command, description and arguments. +Based on those, it will create and return the relevant Command object. + - Justification: This will allow for the separation of user input into the information needed for the Command to run. + - Highlights: The arguments input by the user does not need to be in a fixed order due to the use of HashMap to store + the argument and value associated with it. Duplicate arguments provided by the user will also be detected and not allowed. +- New Feature: Added a Remove transaction command. + - What it does: It will allow for the removal of the income/expense transaction. + - Justification: This will allow user to remove transactions that they no longer want to track or transactions with + errors in them. + - Highlights: It will ensure that only valid income/transaction index can be removed safely. +- New Feature: Added Summary command. + - What it does: This command will allow the user to view their total income/expense. + - Justification: This allows the user to have an overview of their current saving/spending habits. + - Highlights: The user can choose to view the total sum of the income/expense or can choose to filter by day/week/month + to monitor their transactions in a more detailed manner. +- Code contributed: [RepoSense Link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=choonsiang&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=ChoonSiang&tabRepo=AY2324S1-CS2113-W12-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +- Enhancements to existing features: + - Add feature to allow user to list transaction by week and month. (Pull request [#80](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/80)) + - Fixed storage location for files during unit tests overwriting the actual storage files. (Pull request [#91](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/91)) + - Modify EditTransactionCommand to edit multiple values at once. (Pull request [#167](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/167)) +- Contributions to the UG: + - Updated `list` command to show filtering by week and month. [#80](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/80) + - Added examples for `help` command. [#90](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/90) + - Updated on the program behavior when duplicate arguments are specified. [#134](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/134) + - Updated description for Edit Transaction feature to allow multiple arguments. [#168](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/168) +- Contributions to the DG: + - Added implementation details of `Parser`. [#54](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/54) + - Added details of `Income` and `Expense` classes, and their class diagram. [#82](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/82) + - Added Architecture diagram and sequence diagram for `Parser`. [#82](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/82) + - Added Manual tests instruction. [#158](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/158) + - Added implementation details for Goal, Category, Delete Transaction and Summary features. [#162](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/162) + - Added implementation details of Edit Transaction feature. [#168](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/168) +- Community: + - PR reviewed (with non-trivial review comments): [#11](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/11), [#32](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/32), [#81](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/81), [#137](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/137) + - Reported bugs and suggestions for other teams in the class(examples: [1](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/141), [2](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/156), [3](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/165), [4](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/161), [5](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/148), [6](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/143), [7](https://github.com/AY2324S1-CS2113-T18-3/tp/issues/140)) diff --git a/docs/team/hooami.md b/docs/team/hooami.md new file mode 100644 index 0000000000..8a85dca162 --- /dev/null +++ b/docs/team/hooami.md @@ -0,0 +1,48 @@ +--- +title: Jun Hong's Project Portfolio Page +--- + +### Project: FinText +FinText is a Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track and manage their saving/spending, and view a summary of their daily/weekly/monthly transactions. + +Given below are my contributions to the project. + +- **New Feature:** Added command to list transactions + * What it does: Allow the user to list goal/category status, e.g. view progress towards a certain goal, or how much they have spent on a certain category. It also allows the user to view all incoming/outgoing transactions + * Justification: This feature is essential as it allows a user to review what they have added for their income and spending. + * Highlights: The list command takes in information from commands developed by other developers and formats it into presents it in an easily-readable format to the user. +* **New Feature:** Added goal and category command + * What it does: Allows a user to create new goals/categories to track their saving/spending habits by grouping it into a group. It also allows them to delete previously created goals/categories. + * Justification: This feature would allow a user to set a goal amount for them to work towards. It would also let a user group their spending into various categories and view it using the list command. + * Highlights: When viewed using 'list goal', the program would show a user their progress towards all their goals. + + +* **Code Contributed:** [RepoSense Link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=hooami&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22) + +* **Enhancements to existing features:** + * Allowed a user to not specify a goal or category when adding income or expenses, instead assigning 'Uncategorised' + to it. [\#132](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/132) + * Improved input validation behaviour for ListCommand and Goal/CategoryCommand [\#136](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/136), [\#143](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/143) + * Abstracted some parts of the input validation for GoalCommand and CategoryCommand into a separate abstract class ClassificationCommand + to reduce code reuse [\#143](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/143) + +* **Documentation:** + * User Guide: + * Created initial UG v1.0 file in Markdown from previously created UG in docx format [\#32](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/32) + * Updated documentation for features 'list', 'goal' and 'category' [\#84](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/84) + * Added sample output for features [\#184](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/184) + * Developer Guide: + * Added information about 'Command' component of DG [\#55](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/55/) + * Added implementation details about ListCommand and sequence diagram for ListCommand [\#166](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/166) + * Updated class diagram for UI [\#166](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/166) + +* **Contribution to team-based tasks:** + * Actively participated in tutorial activities and gave feedback through project development + * Prepared the release of the JAR files for v1.0 and v2.0 + * Added Javadocs to AddIncomeCommand, AddExpenseCommand, AddTransactionCommand, CategoryCommand, ClassificationCommand, GoalCommand and ListCommand [\#171](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/171) + + +* **Review Contributions:** + * PRs Reviewed: [List](https://github.com/AY2324S1-CS2113-W12-3/tp/pulls?q=is%3Apr+reviewed-by%3Ahooami) +* **Community:** + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/141), [2](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/149), [3](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/171), [4](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/157), [5](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/205), [6](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/199), [7](https://github.com/AY2324S1-CS2113-T17-3/tp/issues/164)) diff --git a/docs/team/itayrefaely.md b/docs/team/itayrefaely.md new file mode 100644 index 0000000000..8298b91aa3 --- /dev/null +++ b/docs/team/itayrefaely.md @@ -0,0 +1,23 @@ +# Itay Refaely's Project Portfolio's Page + +## Project: FinText + +FinText is a Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track \ +and manage their spending, and generate daily/weekly/monthly reports to break down how they spend. + +Given below are my contributions to the project. + +- New Feature: Added a edit function to allow users to handle edit commands from the user. + - What it does: Allows the user to be able to edit income/expense transactions and their fields, excluding the date field. + - Justification: This allows the user to update transactions or fix mistakes/typos in previous commands in an easy fashion. + +- Code contributed: [RepoSense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=itayrefaely&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + +- Contributions to the UG: Updated documentation for edit command. + +- Contributions to the DG: Updated documentation of value proposition and target user profile: [#66](https://github.com/AY2324S1-CS2113-W12-3/tp/commit/b28d73b3f5231460f7cfe81907aec1f054d2bcf9) + +- Contributions to team-based tasks: played a major role in shaping the project idea, defining its goals, \ + identifying the problem to solve, and outlining the initial structure. + +- Community: Reported bugs and suggestions for other teams in the class (examples [#1](https://github.com/itayrefaely/ped/issues/1) [#2](https://github.com/itayrefaely/ped/issues/2) [#3](https://github.com/itayrefaely/ped/issues/3) [#4](https://github.com/itayrefaely/ped/issues/4)) diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/jonoans.md b/docs/team/jonoans.md new file mode 100644 index 0000000000..9c0d6159b1 --- /dev/null +++ b/docs/team/jonoans.md @@ -0,0 +1,52 @@ +--- +title: Jonathan's Project Portfolio Page +--- + +# Jonathan's Project Portfolio Page + +### Project: FinText + +FinText is a **Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track and manage +their saving/spending,** and view a summary of their daily/weekly/monthly transactions. + +Given below are my contributions to the project. + +* **New Feature**: Added command to add expenses + * What it does: Allows a user to add an expense entry to the program + * Justification: This allows the user to track his expenses as part of the set of product features set forth in the user stories. + * Highlights: Made a StateManager object which enforces singleton design pattern. This StateManager object is used to track program state and can be used by other developers. + Ensures only a single source of truth exists in the program. +* **New Feature**: Added table printing utilities in Ui class + * What it does: Allows other developers to use the same common functions to print output in a tabular format + * Justification: A lot of our output (transactions details etc.) are best displayed in a tabular format. Thus, a common function for use to print outputs in tabular format would be ideal. + * Highlights: The table printer is made to have adjustable parameters. These parameters include the column widths. The function automatically truncates values if the column content exceeds column width. +* **New Feature**: Implemented the underlying code for recurring transactions + * What it does: Allows a user to add a recurring transactions. New recurring entries will be created automatically when required. + * Justification: This allows the users to add recurring transactions and have the program automatically create future entries, saving the user the need to manually create entries. + * Highlights: Tried out using streams to filter transaction entries to find candidate transactions that need to have their recurring entries added. + +* **Code contributed**: [RepoSense link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=jonoans&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=Jonoans&tabRepo=AY2324S1-CS2113-W12-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Enhancements to existing features**: + * Fixed test cases for list command ([\#38](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/38)) + * Fixed failing tests due to inter-test dependencies ([\#59](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/59)) + * Tests were indirectly dependent on one another because state program state was not properly cleared after tests ran for different components. + * Fixed edge case for edit command ([\#177](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/177)) + +* **Documentation**: + * User Guide: + * Updated documentation for the features `in` and `out` to include information about `recurrence` option ([\#70](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/70), [\#131](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/131)) + * Developer Guide: + * Added information about `UI` class of program ([\#46](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/46), [\#79](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/79), [\#92](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/92)). + * Added section for `StateManager` and `in` and `out` commands ([\#154](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/154), [\#164](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/164)) + * Added non-functional requirements ([\#154](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/154)) + * Added sequence diagrams for DG ([\#173](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/173)) + +* **Contribution to team-based tasks**: + * Participate in tutorial activities, working with teammates to complete tutorial tasks. + * Participated in team discussions about product and gave feedback about proposed features and implementation. + +* **Community**: + * PR reviewed (with non-trivial review comments): [\#78](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/78), [\#85](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/85), [\#143](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/143), [\#166](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/166) + * All PRs reviewed: [PRs Reviewed](https://github.com/AY2324S1-CS2113-W12-3/tp/pulls?q=is%3Apr+reviewed-by%3AJonoans) + * Reported bugs and suggestions for other teams in the class (examples: [1](https://github.com/AY2324S1-CS2113-T18-4/tp/issues/86), [2](https://github.com/AY2324S1-CS2113-T18-4/tp/issues/92), [3](https://github.com/AY2324S1-CS2113-T18-4/tp/issues/116), [4](https://github.com/AY2324S1-CS2113-T18-4/tp/issues/122), [5](https://github.com/AY2324S1-CS2113-T18-4/tp/issues/97)) \ No newline at end of file diff --git a/docs/team/sranay.md b/docs/team/sranay.md new file mode 100644 index 0000000000..71908a58b7 --- /dev/null +++ b/docs/team/sranay.md @@ -0,0 +1,38 @@ +# Jason Song's Project Portfolio Page + +## Project: FinText + +FinText is a **Command Line Interface (CLI)-based personal finance tracker to make it easy for users to track and manage +their saving/spending,** and view a summary of their daily/weekly/monthly transactions. Application Data will be stored into a folder called `data`. + +Given below are my contributions to the project. + +- New Feature: Added a storage function so that the user can load back their current state of the application after reopening it. + - What it does: Allows the user to be able to save the current state of the application and load back after reopening the application. + - Justification: This allows the user to not key in all the transactions details everytime they reopen the application. + - Highlights: Every new data added in any of the object will need to update the storage's logic flow. + - Credits: Uses [OpenCSV](https://mvnrepository.com/artifact/com.opencsv/opencsv) to store the storage files as CSV. +- New Feature: Added a help command that allows the user to see what are the available commands that can be used. + - What it does: Allows the user to see what are the commands available that can be used. + - Justification: This allows the user to know what are the available commands there are. + - Highlights: Every new command or flags added will require it to be updated. +- New Feature: Added a export command that allows the user to export all the transactions stored into a CSV File. + - What it does: Allows the user to export all the transaction data into a CSV File. + - Justification: This allows the user to see the summary of all the transactions that is being recorded. + - Highlights: Every new data added in Transaction object will need to update the command's logic flow. + - Credits: Uses [OpenCSV](https://mvnrepository.com/artifact/com.opencsv/opencsv) to store the storage files as CSV. +- Code contributed: [RepoSense Link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=sRanay&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=sRanay&tabRepo=AY2324S1-CS2113-W12-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=other~docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +- Enhancements to existing features: + - Help command for specific commands (Pull request [#11](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/11)) + - Export command to export only "in" or "out" transactions (Pull request [#63](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/63)) +- Contributions to the UG: + - Updated documentation for features `export`: [#69](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/69) +- Contributions to the DG: + - Added implmentation details of Storage component and its Class Diagram [#78](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/78) +- Contributions to team-based tasks: + - Updated the user stories for DG: [#41](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/41) +- Review/mentoring contributions: + - PRs reviewed: [Pull Request](https://github.com/AY2324S1-CS2113-W12-3/tp/pulls?q=is%3Apr+reviewed-by%3AsRanay) + - PRs reviewed with comments to fix potential issue with commit: [#85](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/85), [#82](https://github.com/AY2324S1-CS2113-W12-3/tp/pull/82) +- Contributions beyond the project teams: + - DG Review: CS2113-T18-3 ([Pull request](https://github.com/nus-cs2113-AY2324S1/tp/pull/22)) \ No newline at end of file diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 5c74e68d59..fc7aa9648c 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,21 +1,88 @@ package seedu.duke; -import java.util.Scanner; +import seedu.duke.classes.StateManager; +import seedu.duke.classes.TransactionRecurrence; +import seedu.duke.command.Command; +import seedu.duke.command.ExitCommand; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.storage.Storage; +import seedu.duke.ui.Ui; +/** + * The type Duke. + */ public class Duke { + + private static Ui ui; + private static Storage storage; + + public Duke() { + ui = new Ui(); + storage = new Storage(); + } + + public void load() { + try { + storage.load(); + syncTransactions(); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + public void save() throws DukeException { + syncTransactions(); + storage.save(); + } + + public void syncTransactions() { + TransactionRecurrence.generateRecurrentTransactions( + StateManager.getStateManager().getAllIncomes(), + StateManager.getStateManager().getAllExpenses() + ); + } + + + /** + * Gets the user input and execute the command based on the input. + */ + public void run() { + ui.printGreeting(); + String userInput; + boolean continueRunning = true; + while (continueRunning) { + System.out.print("> User: "); + + try { + userInput = ui.readUserInput(); + Command command = new Parser().parse(userInput); + command.execute(ui); + + if (command instanceof ExitCommand) { + continueRunning = false; + } + save(); + + } catch (DukeException e) { + System.out.println(e.getMessage()); + } catch (Exception e) { + System.out.println("Oops unexpected error occurred."); + } + } + ui.close(); + } + /** * Main entry-point for the java.duke.Duke application. */ public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.print("\n"); + ui.printBye(); + })); + Duke duke = new Duke(); + duke.load(); + duke.run(); } } diff --git a/src/main/java/seedu/duke/classes/Category.java b/src/main/java/seedu/duke/classes/Category.java new file mode 100644 index 0000000000..7391222f2a --- /dev/null +++ b/src/main/java/seedu/duke/classes/Category.java @@ -0,0 +1,17 @@ +package seedu.duke.classes; + +public class Category { + private String name; + + public Category(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/seedu/duke/classes/Expense.java b/src/main/java/seedu/duke/classes/Expense.java new file mode 100644 index 0000000000..55f6012380 --- /dev/null +++ b/src/main/java/seedu/duke/classes/Expense.java @@ -0,0 +1,42 @@ +package seedu.duke.classes; + +public class Expense { + private Transaction transaction; + private Category category; + + public Expense(Transaction transaction, Category category) { + this.transaction = transaction; + this.category = category; + } + + public Transaction getTransaction() { + return transaction; + } + + public void setTransaction(Transaction transaction) { + this.transaction = transaction; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + /** + * Generate next recurrent entry for expense + * + * @return Generated expense if entry should be generated, + * otherwise returns {@code null} + */ + public Expense generateNextRecurrence() { + Transaction nextTransaction = transaction.generateNextRecurrence(); + if (nextTransaction == null) { + return null; + } + + return new Expense(nextTransaction, category); + } +} diff --git a/src/main/java/seedu/duke/classes/Goal.java b/src/main/java/seedu/duke/classes/Goal.java new file mode 100644 index 0000000000..148f38aa57 --- /dev/null +++ b/src/main/java/seedu/duke/classes/Goal.java @@ -0,0 +1,28 @@ +package seedu.duke.classes; + +public class Goal { + private String description; + private double amount; + + public Goal(String description, double amount) { + this.description = description; + this.amount = amount; + } + + public double getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/src/main/java/seedu/duke/classes/Income.java b/src/main/java/seedu/duke/classes/Income.java new file mode 100644 index 0000000000..6b4ae0542e --- /dev/null +++ b/src/main/java/seedu/duke/classes/Income.java @@ -0,0 +1,42 @@ +package seedu.duke.classes; + +public class Income { + private Transaction transaction; + private Goal goal; + + public Income(Transaction transaction, Goal goal) { + this.transaction = transaction; + this.goal = goal; + } + + public Transaction getTransaction() { + return transaction; + } + + public void setTransaction(Transaction transaction) { + this.transaction = transaction; + } + + public Goal getGoal() { + return goal; + } + + public void setGoal(Goal goal) { + this.goal = goal; + } + + /** + * Generate next recurrent entry for income + * + * @return Generated income if entry should be generated, + * otherwise returns {@code null} + */ + public Income generateNextRecurrence() { + Transaction nextTransaction = transaction.generateNextRecurrence(); + if (nextTransaction == null) { + return null; + } + + return new Income(nextTransaction, goal); + } +} diff --git a/src/main/java/seedu/duke/classes/StateManager.java b/src/main/java/seedu/duke/classes/StateManager.java new file mode 100644 index 0000000000..7a4e2d1cf4 --- /dev/null +++ b/src/main/java/seedu/duke/classes/StateManager.java @@ -0,0 +1,225 @@ +package seedu.duke.classes; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.stream.IntStream; + +public class StateManager { + public static final String UNCATEGORISED_CLASS = "Uncategorised"; + private static StateManager stateManager = null; + private final ArrayList goals = new ArrayList<>(); + private final Goal uncategorisedGoal = new Goal(UNCATEGORISED_CLASS, 0); + private final ArrayList categories = new ArrayList<>(); + private final Category uncategorisedCategory = new Category(UNCATEGORISED_CLASS); + private final ArrayList incomes = new ArrayList<>(); + private final ArrayList expenses = new ArrayList<>(); + + + private StateManager() { + } + + public static StateManager getStateManager() { + if (stateManager == null) { + stateManager = new StateManager(); + } + return stateManager; + } + + public static void clearStateManager() { + stateManager = new StateManager(); + } + + public void addGoal(Goal goal) { + assert goal != null; + goals.add(goal); + } + + public Goal getGoal(int idx) { + if (idx < 0 || idx >= goals.size()) { + return null; + } + return goals.get(idx); + } + + public boolean removeGoal(Goal goal) { + assert goal != null; + return goals.remove(goal); + } + + public boolean removeGoal(int idx) { + Goal goal = getGoal(idx); + if (goal == null) { + return false; + } + + return removeGoal(goal); + } + + public Goal getUncategorisedGoal() { + return uncategorisedGoal; + } + + public void addCategory(Category category) { + assert category != null; + categories.add(category); + } + + public Category getCategory(int idx) { + if (idx < 0 || idx >= categories.size()) { + return null; + } + return categories.get(idx); + } + + public Category getUncategorisedCategory() { + return uncategorisedCategory; + } + + public boolean removeCategory(Category category) { + assert category != null; + return categories.remove(category); + } + + public boolean removeCategory(int idx) { + Category category = getCategory(idx); + if (category == null) { + return false; + } + + return removeCategory(category); + } + + public void addIncome(Income income) { + assert income != null; + incomes.add(income); + } + + public Income getIncome(int idx) { + if (idx < 0 || idx >= incomes.size()) { + return null; + } + return incomes.get(idx); + } + + public boolean removeIncome(Income income) { + assert income != null; + return incomes.remove(income); + } + + public boolean removeIncome(int idx) { + Income income = getIncome(idx); + if (income == null) { + return false; + } + + return removeIncome(income); + } + + public void addExpense(Expense expense) { + assert expense != null; + expenses.add(expense); + } + + public Expense getExpense(int idx) { + if (idx < 0 || idx >= expenses.size()) { + return null; + } + return expenses.get(idx); + } + + public boolean removeExpense(Expense expense) { + assert expense != null; + return expenses.remove(expense); + } + + public boolean removeExpense(int idx) { + Expense expense = getExpense(idx); + if (expense == null) { + return false; + } + + return removeExpense(expense); + } + + public ArrayList getAllIncomes() { + return incomes; + } + + public ArrayList getAllExpenses() { + return expenses; + } + + public ArrayList getAllCategories() { + return categories; + } + + public ArrayList getAllGoals() { + return goals; + } + + public int getIncomesSize() { + return incomes.size(); + } + + public int getExpensesSize() { + return expenses.size(); + } + + public int getCategoryIndex(String categoryToCheck) { + return IntStream.range(0, categories.size()) + .filter(i -> categories.get(i).getName().equalsIgnoreCase(categoryToCheck)) + .findFirst() + .orElse(-1); + } + + public int getGoalIndex(String goalToCheck) { + return IntStream.range(0, goals.size()) + .filter(i -> goals.get(i).getDescription().equalsIgnoreCase(goalToCheck)) + .findFirst() + .orElse(-1); + } + + public void sortIncomes() { + Comparator dateComparator = Comparator.comparing((Income i) -> i.getTransaction().getDate(), + Comparator.reverseOrder()); + incomes.sort(dateComparator); + } + + public void sortExpenses() { + Comparator dateComparator = Comparator.comparing((Expense e) -> e.getTransaction().getDate(), + Comparator.reverseOrder()); + expenses.sort(dateComparator); + } + + public HashMap getGoalsStatus() { + HashMap map = new HashMap<>(); + for (Income i : incomes) { + Goal key = i.getGoal(); + map.put(key, map.getOrDefault(key, 0.0) + i.getTransaction().getAmount()); + } + return map; + } + + public HashMap getCategoriesStatus() { + HashMap map = new HashMap<>(); + for (Expense e : expenses) { + Category key = e.getCategory(); + map.put(key, map.getOrDefault(key, 0.0) + e.getTransaction().getAmount()); + } + return map; + } + + public void unassignCategoryTransactions(Category category) { + expenses.stream() + .filter(e -> e.getCategory() == category) + .forEach(e -> e.setCategory(uncategorisedCategory)); + } + + public void unassignGoalTransactions(Goal goal) { + incomes.stream() + .filter(g -> g.getGoal() == goal) + .forEach(g -> g.setGoal(uncategorisedGoal)); + } + +} diff --git a/src/main/java/seedu/duke/classes/Transaction.java b/src/main/java/seedu/duke/classes/Transaction.java new file mode 100644 index 0000000000..4a848ae941 --- /dev/null +++ b/src/main/java/seedu/duke/classes/Transaction.java @@ -0,0 +1,92 @@ +package seedu.duke.classes; + +import java.time.LocalDate; + +public class Transaction { + private String description; + private Double amount; + private LocalDate date; + private TransactionRecurrence recurrence; + private boolean hasGeneratedNextRecurrence = false; + + public Transaction(String description, Double amount, LocalDate date) { + if (date == null) { + date = LocalDate.now(); + } + + this.description = description; + this.amount = amount; + this.date = date; + this.recurrence = TransactionRecurrence.NONE; + } + + public Double getAmount() { + return amount; + } + + public void setAmount(Double amount) { + this.amount = amount; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public TransactionRecurrence getRecurrence() { + return recurrence; + } + + public void setRecurrence(TransactionRecurrence recurrence) { + this.recurrence = recurrence; + } + + public boolean getHasGeneratedNextRecurrence() { + return hasGeneratedNextRecurrence; + } + + public void setHasGeneratedNextRecurrence(boolean hasGeneratedNextRecurrence) { + this.hasGeneratedNextRecurrence = hasGeneratedNextRecurrence; + } + + /** + * Checks if next recurrent entry should be generated. + * + * @return {@code true} if should be generated, otherwise {@code false} + */ + public boolean shouldGenerateNextRecurrence() { + if (getRecurrence() == TransactionRecurrence.NONE || getHasGeneratedNextRecurrence()) { + return false; + } + + return !TransactionRecurrence.getNextRecurrenceDate(getRecurrence(), getDate()).isAfter(LocalDate.now()); + } + + /** + * Generate next recurrent entry for transaction + * + * @return Generated transaction if entry should be generated, + * otherwise returns {@code null} + */ + public Transaction generateNextRecurrence() { + if (!shouldGenerateNextRecurrence()) { + return null; + } + + LocalDate nextDate = TransactionRecurrence.getNextRecurrenceDate(getRecurrence(), getDate()); + Transaction nextTransaction = new Transaction(getDescription(), getAmount(), nextDate); + nextTransaction.setRecurrence(getRecurrence()); + return nextTransaction; + } +} diff --git a/src/main/java/seedu/duke/classes/TransactionRecurrence.java b/src/main/java/seedu/duke/classes/TransactionRecurrence.java new file mode 100644 index 0000000000..3727e40f83 --- /dev/null +++ b/src/main/java/seedu/duke/classes/TransactionRecurrence.java @@ -0,0 +1,167 @@ +package seedu.duke.classes; + +import java.time.LocalDate; +import java.util.ArrayList; + +public enum TransactionRecurrence { + NONE, DAILY, WEEKLY, MONTHLY; + + private static final String NONE_STR = "none"; + private static final String DAILY_STR = "daily"; + private static final String WEEKLY_STR = "weekly"; + private static final String MONTHLY_STR = "monthly"; + + public static TransactionRecurrence getRecurrence(String recurrence) { + assert recurrence != null; + String cleanedRecurrence = recurrence.strip().toLowerCase(); + switch (cleanedRecurrence) { + case NONE_STR: + return TransactionRecurrence.NONE; + case DAILY_STR: + return TransactionRecurrence.DAILY; + case WEEKLY_STR: + return TransactionRecurrence.WEEKLY; + case MONTHLY_STR: + return TransactionRecurrence.MONTHLY; + default: + return null; + } + } + + /** + * Gets next date at which a recurrent transaction should occur + * + * @param recurrence {@link TransactionRecurrence} enum indicating recurrence type + * @param current date of the transaction + * @return date of next occurrence + */ + public static LocalDate getNextRecurrenceDate(TransactionRecurrence recurrence, LocalDate current) { + switch (recurrence) { + case DAILY: + return current.plusDays(1); + case WEEKLY: + return current.plusWeeks(1); + case MONTHLY: + return current.plusMonths(1); + default: + return current; + } + } + + /** + * Generates a list of recurrent incomes for a given income + * + * @param income Income object to generate recurrent transactions for + * @return List of generated recurrent transactions + */ + public static ArrayList generateRecurrentIncomes(Income income) { + ArrayList recurrentIncomes = new ArrayList<>(); + while (true) { + Income recurrentIncome = income.generateNextRecurrence(); + if (recurrentIncome == null) { + break; + } + + income.getTransaction().setHasGeneratedNextRecurrence(true); + recurrentIncomes.add(recurrentIncome); + income = recurrentIncome; + } + return recurrentIncomes; + } + + /** + * Generate a list of recurrent incomes for the list of incomes provided + * + * @param incomes List of incomes to generate recurrent transactions for + * @return List of generated recurrent transactions + */ + public static ArrayList generateRecurrentIncomes(ArrayList incomes) { + ArrayList recurrentIncomes = new ArrayList<>(); + incomes.parallelStream().filter(income -> filterTransaction(income.getTransaction())) + .sequential() + .forEach(income -> recurrentIncomes.addAll(generateRecurrentIncomes(income))); + + recurrentIncomes.sort((Income a, Income b) -> { + LocalDate aDate = a.getTransaction().getDate(); + LocalDate bDate = b.getTransaction().getDate(); + return aDate.compareTo(bDate); + }); + + return recurrentIncomes; + } + + /** + * Generates a list of recurrent expenses for a given expense + * + * @param expense Expense object to generate recurrent transactions for + * @return List of generated recurrent expenses + */ + public static ArrayList generateRecurrentExpenses(Expense expense) { + ArrayList recurrentExpenses = new ArrayList<>(); + while (true) { + Expense recurrentExpense = expense.generateNextRecurrence(); + if (recurrentExpense == null) { + break; + } + + expense.getTransaction().setHasGeneratedNextRecurrence(true); + recurrentExpenses.add(recurrentExpense); + expense = recurrentExpense; + } + return recurrentExpenses; + } + + /** + * Generate a list of recurrent expenses for the list of expenses provided + * + * @param expenses List of expenses to generate recurrent transactions for + * @return List of generated recurrent transactions + */ + public static ArrayList generateRecurrentExpenses(ArrayList expenses) { + ArrayList recurrentExpenses = new ArrayList<>(); + expenses.parallelStream().filter(expense -> filterTransaction(expense.getTransaction())) + .sequential() + .forEach(expense -> recurrentExpenses.addAll(generateRecurrentExpenses(expense))); + + recurrentExpenses.sort((Expense a, Expense b) -> { + LocalDate aDate = a.getTransaction().getDate(); + LocalDate bDate = b.getTransaction().getDate(); + return aDate.compareTo(bDate); + }); + + return recurrentExpenses; + } + + /** + * Updates the provided lists with newly generated recurrent transactions + * + * @param incomes List of incomes to update + * @param expenses List of expenses to update + */ + public static void generateRecurrentTransactions(ArrayList incomes, ArrayList expenses) { + assert incomes != null; + assert expenses != null; + incomes.addAll(generateRecurrentIncomes(incomes)); + expenses.addAll(generateRecurrentExpenses(expenses)); + } + + @Override + public String toString() { + switch (this) { + case NONE: + return NONE_STR; + case DAILY: + return DAILY_STR; + case WEEKLY: + return WEEKLY_STR; + case MONTHLY: + return MONTHLY_STR; + default: + return null; + } + } + + private static boolean filterTransaction(Transaction t) { + return t.shouldGenerateNextRecurrence(); + } +} diff --git a/src/main/java/seedu/duke/classes/TypePrint.java b/src/main/java/seedu/duke/classes/TypePrint.java new file mode 100644 index 0000000000..761db4f9f9 --- /dev/null +++ b/src/main/java/seedu/duke/classes/TypePrint.java @@ -0,0 +1,50 @@ +package seedu.duke.classes; + +public class TypePrint { + private String description; + private double currentAmount; + private double targetAmount; + + public TypePrint(String description, double currentAmount) { + this(description, currentAmount, 0.0); + } + + public TypePrint(String description, double currentAmount, double targetAmount) { + this.description = description; + this.currentAmount = currentAmount; + this.targetAmount = targetAmount; + } + + public String getDescription() { + return description; + } + + public boolean targetAmountExists() { + return targetAmount > 0.0; + } + + public String getCurrentAmount() { + return String.format("%.2f", currentAmount); + } + + public String getTargetAmount() { + return String.format("%.2f", targetAmount); + } + + public String getAmount() { + if (targetAmount == 0.0) { + return getCurrentAmount(); + } else { + return getCurrentAmount() + "/" + getTargetAmount(); + } + } + + public Double getPercentage() { + if (targetAmount == 0.0) { + return null; + } else { + return currentAmount / targetAmount * 100; + } + + } +} diff --git a/src/main/java/seedu/duke/command/AddExpenseCommand.java b/src/main/java/seedu/duke/command/AddExpenseCommand.java new file mode 100644 index 0000000000..1d7d666c98 --- /dev/null +++ b/src/main/java/seedu/duke/command/AddExpenseCommand.java @@ -0,0 +1,93 @@ +package seedu.duke.command; + +import seedu.duke.classes.Category; +import seedu.duke.classes.Expense; +import seedu.duke.classes.StateManager; +import seedu.duke.classes.Transaction; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.util.ArrayList; +import java.util.HashMap; + +public class AddExpenseCommand extends AddTransactionCommand { + private static final String CATEGORY_ARG = "category"; + private static final String[] HEADERS = {"Description", "Date", "Amount", "Category", "Recurrence"}; + + private static final String SUCCESS_PRINT = "Nice! The following expense has been tracked:"; + private static final String MISSING_CATEGORY = "Category cannot be empty..."; + + public AddExpenseCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to format output. + * @throws DukeException if user input is invalid. + */ + @Override + public void execute(Ui ui) throws DukeException { + throwIfInvalidDescOrArgs(); + Transaction transaction = prepareTransaction(); + Expense expense = addNewExpense(transaction); + printSuccess(ui, expense); + StateManager.getStateManager().sortExpenses(); + } + + /** + * Adds a new expense to the Expense arraylist in StateManager + * @param transaction transaction to add to Expense object + * @return Expense object to be used for printing in printSuccess + * @throws DukeException if category is invalid, or any issue is encountered when adding expense + */ + private Expense addNewExpense(Transaction transaction) throws DukeException { + Category category = handleCategory(); + Expense expense = new Expense(transaction, category); + StateManager.getStateManager().addExpense(expense); + return expense; + } + + /** + * Print successful addition of expense transaction message + * @param ui Ui class for printing + * @param expense expense transaction to print + */ + private void printSuccess(Ui ui, Expense expense) { + Transaction transaction = expense.getTransaction(); + ArrayList printValues = new ArrayList<>(); + printValues.add(transaction.getDescription()); + printValues.add(transaction.getDate().toString()); + printValues.add(ui.formatAmount(transaction.getAmount())); + printValues.add(expense.getCategory().getName()); + printValues.add(expense.getTransaction().getRecurrence().toString()); + ui.print(SUCCESS_PRINT); + ui.printTableRow(printValues, HEADERS, HEADERS_WIDTH); + } + + /** + * Validates user input for the /category argument and retrieves/add a category object + * @return category of the transaction + * @throws DukeException if category user input is invalid + */ + private Category handleCategory() throws DukeException { + StateManager state = StateManager.getStateManager(); + String category = getArg(CATEGORY_ARG); + if (category == null) { + return state.getUncategorisedCategory(); + } else if (category.equalsIgnoreCase(StateManager.UNCATEGORISED_CLASS)) { + return state.getUncategorisedCategory(); + } else if (category.isBlank()) { + throw new DukeException(MISSING_CATEGORY); + } + int index = state.getCategoryIndex(category); + if (index == -1) { + Category categoryToAdd = new Category(category); + state.addCategory(categoryToAdd); + return categoryToAdd; + } else { + return state.getCategory(index); + } + } +} diff --git a/src/main/java/seedu/duke/command/AddIncomeCommand.java b/src/main/java/seedu/duke/command/AddIncomeCommand.java new file mode 100644 index 0000000000..37d17d7d6a --- /dev/null +++ b/src/main/java/seedu/duke/command/AddIncomeCommand.java @@ -0,0 +1,91 @@ +package seedu.duke.command; + +import seedu.duke.classes.Goal; +import seedu.duke.classes.Income; +import seedu.duke.classes.StateManager; +import seedu.duke.classes.Transaction; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.util.ArrayList; +import java.util.HashMap; + +public class AddIncomeCommand extends AddTransactionCommand { + private static final String GOAL_ARG = "goal"; + private static final String[] HEADERS = {"Description", "Date", "Amount", "Goal", "Recurrence"}; + + private static final String SUCCESS_PRINT = "Nice! The following income has been tracked:"; + private static final String MISSING_GOAL = "Goal cannot be empty..."; + + public AddIncomeCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Executes the command + * @param ui Ui class that is used to format output + * @throws DukeException if user input is invalid + */ + @Override + public void execute(Ui ui) throws DukeException { + throwIfInvalidDescOrArgs(); + Transaction transaction = prepareTransaction(); + Income income = addNewIncome(transaction); + printSuccess(ui, income); + StateManager.getStateManager().sortIncomes(); + } + + /** + * Adds a new Income to the Income arraylist in StateManager + * @param transaction transaction to add to Income object + * @return Income object to be used for printing in printSuccess + * @throws DukeException if income is invalid, or any issue is encountered when adding income + */ + private Income addNewIncome(Transaction transaction) throws DukeException { + Goal goal = handleGoal(); + Income income = new Income(transaction, goal); + StateManager.getStateManager().addIncome(income); + return income; + } + + /** + * Print successful addition of income transaction message + * @param ui Ui class for printing + * @param income income transaction to print + */ + private void printSuccess(Ui ui, Income income) { + Transaction transaction = income.getTransaction(); + ArrayList printValues = new ArrayList<>(); + printValues.add(transaction.getDescription()); + printValues.add(transaction.getDate().toString()); + printValues.add(ui.formatAmount(transaction.getAmount())); + printValues.add(income.getGoal().getDescription()); + printValues.add(income.getTransaction().getRecurrence().toString()); + ui.print(SUCCESS_PRINT); + ui.printTableRow(printValues, HEADERS, HEADERS_WIDTH); + } + + /** + * Validates user input for the /category argument and retrieves an income object + * @return goal of the transaction + * @throws DukeException if goal user input is invalid + */ + private Goal handleGoal() throws DukeException { + StateManager state = StateManager.getStateManager(); + String goal = getArg(GOAL_ARG); + if (goal == null) { + return state.getUncategorisedGoal(); + } else if (goal.equalsIgnoreCase(StateManager.UNCATEGORISED_CLASS)) { + return state.getUncategorisedGoal(); + } else if (goal.isBlank()) { + throw new DukeException(MISSING_GOAL); + } + int index = state.getGoalIndex(goal); + if (index == -1) { + String failedGoalMessage = "Please add '" + goal + "' as a goal first."; + throw new DukeException(failedGoalMessage); + } else { + return state.getGoal(index); + } + } +} diff --git a/src/main/java/seedu/duke/command/AddTransactionCommand.java b/src/main/java/seedu/duke/command/AddTransactionCommand.java new file mode 100644 index 0000000000..d73cdd1b34 --- /dev/null +++ b/src/main/java/seedu/duke/command/AddTransactionCommand.java @@ -0,0 +1,130 @@ +package seedu.duke.command; + +import seedu.duke.classes.Transaction; +import seedu.duke.classes.TransactionRecurrence; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.time.LocalDate; +import java.util.HashMap; + +public abstract class AddTransactionCommand extends Command { + protected static final Integer[] HEADERS_WIDTH = { + Ui.LIST_COLUMN_WIDTH, Ui.COLUMN_WIDTH, Ui.COLUMN_WIDTH, Ui.TYPE_WIDTH, Ui.COLUMN_WIDTH + }; + protected static final String AMOUNT_ARG = "amount"; + protected static final String DATE_ARG = "date"; + protected static final String RECURRENCE_ARG = "recurrence"; + private static final String MISSING_DESC = "Description cannot be empty..."; + private static final String MISSING_AMOUNT = "Amount cannot be empty..."; + private static final String BAD_AMOUNT = "Invalid amount value specified..."; + private static final String BAD_DATE = "Invalid date specified..."; + private static final String BAD_RECURRENCE = "Invalid recurrence period specified..."; + private static final String BAD_RECURRENCE_DATE = "Cannot specify date for recurring transaction" + + " to be larger than 1 period in the past..."; + private boolean isValidated = false; + + protected AddTransactionCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Prepares a transaction object based on user input. + * @return Transaction object to be added to either Expense or Income + */ + protected Transaction prepareTransaction() { + assert isValidated; + + String description = getDescription(); + Double amount = Parser.parseNonNegativeDouble(getArg(AMOUNT_ARG)); + LocalDate date = Parser.parseDate(getArg(DATE_ARG)); + Transaction transaction = new Transaction(description, amount, date); + + String recurrenceValue = getArg(RECURRENCE_ARG); + if (recurrenceValue != null) { + TransactionRecurrence recurrence = TransactionRecurrence.getRecurrence(recurrenceValue); + transaction.setRecurrence(recurrence); + } + return transaction; + } + + /** + * Validates user input + * @throws DukeException if user input is invalid + */ + protected void throwIfInvalidDescOrArgs() + throws DukeException { + assert getDescription() != null; + assert getArgs() != null; + throwIfEmptyDesc(); + throwIfInvalidAmount(); + throwIfInvalidDate(); + throwIfInvalidRecurrence(); + isValidated = true; + } + + /** + * Throws error if user does not enter input in description field + * @throws DukeException if user does not enter any input in description + */ + private void throwIfEmptyDesc() throws DukeException { + if (getDescription().isBlank()) { + throw new DukeException(MISSING_DESC); + } + } + + /** + * Throws an exception if /amount is invalid + * @throws DukeException when no amount is entered or invalid amount + */ + + private void throwIfInvalidAmount() throws DukeException { + String amountArg = getArg(AMOUNT_ARG); + if (amountArg == null || amountArg.isBlank()) { + throw new DukeException(MISSING_AMOUNT); + } + + Double amount = Parser.parseNonNegativeDouble(amountArg); + if (amount == null) { + throw new DukeException(BAD_AMOUNT); + } + } + + /** + * Throws exception if date is in an invalid format + * @throws DukeException if user input for date is invalid + */ + private void throwIfInvalidDate() throws DukeException { + String date = getArg(DATE_ARG); + LocalDate parsedDate = Parser.parseDate(date); + if (date != null && parsedDate == null) { + throw new DukeException(BAD_DATE); + } + } + + /** + * Validate if user has entered correct input for recurrence + * @throws DukeException if user input is incorrect + */ + private void throwIfInvalidRecurrence() throws DukeException { + String recurrence = getArg(RECURRENCE_ARG); + if (recurrence == null) { + return; + } + + TransactionRecurrence parsedRecurrence = TransactionRecurrence.getRecurrence(recurrence); + if (parsedRecurrence == null) { + throw new DukeException(BAD_RECURRENCE); + } + + String date = getArg(DATE_ARG); + LocalDate parsedDate = Parser.parseDate(date); + if (parsedRecurrence != TransactionRecurrence.NONE && parsedDate != null) { + LocalDate nextDate = TransactionRecurrence.getNextRecurrenceDate(parsedRecurrence, parsedDate); + if (!nextDate.isAfter(LocalDate.now())) { + throw new DukeException(BAD_RECURRENCE_DATE); + } + } + } +} diff --git a/src/main/java/seedu/duke/command/CategoryCommand.java b/src/main/java/seedu/duke/command/CategoryCommand.java new file mode 100644 index 0000000000..a5efe14366 --- /dev/null +++ b/src/main/java/seedu/duke/command/CategoryCommand.java @@ -0,0 +1,80 @@ +package seedu.duke.command; + +import seedu.duke.classes.Category; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.util.HashMap; + +public class CategoryCommand extends ClassificationCommand { + private static final String ADD_COMMAND = "add"; + private static final String REMOVE_COMMAND = "remove"; + private static final String INVALID_INPUT = "Your category input is empty/invalid :("; + + public CategoryCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to format output. + * @throws DukeException if user input is invalid. + */ + @Override + public void execute(Ui ui) throws DukeException { + String inputType = validateInput(); + if (inputType == null) { + errorMessage(INVALID_INPUT); + } + if (inputType.equals(ADD_COMMAND)) { + String category = getArg(ADD_COMMAND); + addCategory(category); + ui.print("Successfully added " + category + "!"); + } else if (inputType.equals(REMOVE_COMMAND)) { + String category = getArg(REMOVE_COMMAND); + removeCategory(category); + ui.print("Successfully removed " + category + "!"); + } + } + + + /** + * Adds a category to the category ArrayList in StateManager + * @param category category to add + * @throws DukeException if category already exists + */ + private void addCategory(String category) throws DukeException { + StateManager state = StateManager.getStateManager(); + if (state.getCategoryIndex(category) == -1) { + Category newCategory = new Category(category); + state.addCategory(newCategory); + } else { + String alreadyExists = "Failed to add '" + category + "' as it already exists!"; + throw new DukeException(alreadyExists); + } + + } + + /** + * Removes a category to the category ArrayList in StateManager + * @param category to remove + * @throws DukeException if category does not already exist + */ + private void removeCategory(String category) throws DukeException { + StateManager state = StateManager.getStateManager(); + int index = state.getCategoryIndex(category); + Category categoryToRemove = state.getCategory(index); + boolean removedClassification = false; + if (index != -1) { + state.unassignCategoryTransactions(categoryToRemove); + removedClassification = state.removeCategory(categoryToRemove); + } + if (!removedClassification) { + String failedRemoval = "Failed to remove '" + category + "' as it does not exist!"; + throw new DukeException(failedRemoval); + } + } + +} diff --git a/src/main/java/seedu/duke/command/ClassificationCommand.java b/src/main/java/seedu/duke/command/ClassificationCommand.java new file mode 100644 index 0000000000..2caf28a553 --- /dev/null +++ b/src/main/java/seedu/duke/command/ClassificationCommand.java @@ -0,0 +1,82 @@ +package seedu.duke.command; + +import seedu.duke.exception.DukeException; + +import java.util.HashMap; + +public abstract class ClassificationCommand extends Command { + private static final String ADD_COMMAND = "add"; + private static final String REMOVE_COMMAND = "remove"; + private static final String UNCATEGORISED = "uncategorised"; + + public ClassificationCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Validates user input + * @return type of command (add or remove) + * @throws DukeException if user input is invalid + */ + protected String validateInput() throws DukeException { + String invalidInput = ""; + if (getClass() == CategoryCommand.class) { + invalidInput = "Your category input is empty/invalid :("; + } else if (getClass() == GoalCommand.class) { + invalidInput = "Your goal input is empty/invalid :("; + } + + if (getArgs().isEmpty()) { + throw new DukeException(invalidInput); + } else if (getArgs().containsKey(ADD_COMMAND) && getArgs().containsKey(REMOVE_COMMAND)) { + errorMessage(invalidInput); + } + String arg; + if (getArgs().containsKey(ADD_COMMAND)) { + arg = getArg(ADD_COMMAND); + checkArg(arg, invalidInput, ADD_COMMAND); + return ADD_COMMAND; + } else if (getArgs().containsKey(REMOVE_COMMAND)) { + arg = getArg(REMOVE_COMMAND); + checkArg(arg, invalidInput, REMOVE_COMMAND); + return REMOVE_COMMAND; + } + return null; + } + + /** + * Validates arguments + * @param arg argument to validate + * @param invalidInput error message to print + * @param type whether argument is used for add or remove + * @throws DukeException if arg is either null, blank or called 'Uncategorised' + */ + private void checkArg(String arg, String invalidInput, String type) throws DukeException { + if (arg == null) { + errorMessage(invalidInput); + } else if (arg.isBlank()) { + errorMessage(invalidInput); + } else if (arg.equalsIgnoreCase(UNCATEGORISED)) { + String uncategorisedError = "As 'Uncategorised' is a default classification, you are unable " + + "to " + type + " it."; + throw new DukeException(uncategorisedError); + } + } + + /** + * Prints error message depending on whether GoalCommand or CategoryCommand called it + * @param message Error message to print + * @throws DukeException To display error to user + */ + protected void errorMessage(String message) throws DukeException { + String commonMessage = "Invalid input! Please refer to UG for correct usage"; + if (getClass() == CategoryCommand.class) { + commonMessage = "\nThe correct usage is 'category /add NAME' or 'category /remove NAME'"; + } else if (getClass() == GoalCommand.class) { + commonMessage = "\nThe correct usage is 'goal /add NAME /amount AMOUNT' or 'goal /remove NAME'"; + } + throw new DukeException(message + commonMessage); + } + + +} diff --git a/src/main/java/seedu/duke/command/Command.java b/src/main/java/seedu/duke/command/Command.java new file mode 100644 index 0000000000..e4f846eeaf --- /dev/null +++ b/src/main/java/seedu/duke/command/Command.java @@ -0,0 +1,37 @@ +package seedu.duke.command; + +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.util.HashMap; + +public abstract class Command { + private final String description; + private final HashMap args; + + public Command() { + description = null; + args = null; + } + + public Command(String description, HashMap args) { + this.description = description; + this.args = args; + } + + protected String getDescription() { + return description; + } + + protected String getArg(String key) { + assert args != null; + return args.get(key); + } + + protected HashMap getArgs() { + return args; + } + + public abstract void execute(Ui ui) throws DukeException; + +} diff --git a/src/main/java/seedu/duke/command/EditTransactionCommand.java b/src/main/java/seedu/duke/command/EditTransactionCommand.java new file mode 100644 index 0000000000..7f00a1b2a7 --- /dev/null +++ b/src/main/java/seedu/duke/command/EditTransactionCommand.java @@ -0,0 +1,272 @@ +package seedu.duke.command; + +import seedu.duke.classes.Expense; +import seedu.duke.classes.Goal; +import seedu.duke.classes.Category; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.util.HashMap; + +public class EditTransactionCommand extends Command { + + protected static final String AMOUNT_ARG = "amount"; + protected static final String DESCRIPTION_ARG = "description"; + protected static final String GOAL_ARG = "goal"; + protected static final String CATEGORY_ARG = "category"; + protected static final String DATE_ARG = "date"; + + private static final String MISSING_IDX = "Index cannot be empty..."; + private static final String INVALID_IDX = "Please enter a valid index."; + private static final String MISSING_TYPE = "Please indicate the transaction type."; + private static final String INVALID_TYPE = "Please indicate either /type in or /type out."; + private static final String MISSING_EDIT = "Please enter the attribute to edit"; + private static final String BAD_AMOUNT = "Invalid amount value specified."; + private static final String DATE_EDIT = "Date cannot be edited."; + private static final String TYPE_ARG = "type"; + private static final String TYPE_IN = "in"; + private static final String TYPE_OUT = "out"; + private static final String MISSING_GOAL = "Please enter the goal value."; + private static final String MISSING_CATEGORY = "Please enter the category value."; + private static final String MISSING_DESCRIPTION = "Please enter the description."; + + public EditTransactionCommand(String description, HashMap args) { + super(description, args); + } + + @Override + public void execute(Ui ui) throws DukeException { + throwIfInvalidDescOrArgs(); + editTransaction(ui); + } + + private void throwIfInvalidDescOrArgs() throws DukeException { + assert getDescription() != null; + assert getArgs() != null; + + checkIndex(); + checkType(); + if (getArg(TYPE_ARG).equalsIgnoreCase(TYPE_IN)) { + checkGoal(); + } else if (getArg(TYPE_ARG).equalsIgnoreCase(TYPE_OUT)) { + checkCategory(); + } + checkAmount(); + checkDescription(); + checkHasArgument(); + checkDate(); + } + + private void checkDate() throws DukeException { + if (getArg(DATE_ARG) != null && !getArg(DATE_ARG).isBlank()) { + throw new DukeException(DATE_EDIT); + } + } + + private void checkIndex() throws DukeException { + if (getDescription().isBlank()) { + throw new DukeException(MISSING_IDX); + } + String description = getDescription(); + if (!isInteger(description)) { + throw new DukeException(INVALID_IDX); + } + } + + private void checkType() throws DukeException { + String typeArg = getArg(TYPE_ARG); + if (typeArg == null) { + throw new DukeException(MISSING_TYPE); + } + + if (!(typeArg.equalsIgnoreCase(TYPE_IN) || typeArg.equalsIgnoreCase(TYPE_OUT))) { + throw new DukeException(INVALID_TYPE); + } + } + + private void checkGoal() throws DukeException { + if (!getArgs().containsKey(GOAL_ARG)) { + return; + } + + if (getArg(GOAL_ARG).isBlank()) { + throw new DukeException(MISSING_GOAL); + } + + String newGoalName = getArg(GOAL_ARG); + int newGoalIdx = StateManager.getStateManager().getGoalIndex(newGoalName); + if (newGoalIdx == -1 && !newGoalName.equalsIgnoreCase(StateManager.UNCATEGORISED_CLASS)) { + throw new DukeException("Please add " + newGoalName + " as a goal first."); + } + } + + private void checkCategory() throws DukeException { + if (!getArgs().containsKey(CATEGORY_ARG)) { + return; + } + + if (getArg(CATEGORY_ARG).isBlank()) { + throw new DukeException(MISSING_CATEGORY); + } + } + + private boolean isInteger(String description) { + try { + Integer.parseInt(description); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + private void checkAmount() throws DukeException { + if (getArg(AMOUNT_ARG) != null) { + if (getArg(AMOUNT_ARG).isBlank()) { + throw new DukeException(BAD_AMOUNT); + } + Double amount = Parser.parseNonNegativeDouble(getArg(AMOUNT_ARG)); + if (amount == null) { + throw new DukeException(BAD_AMOUNT); + } + } + } + + private void checkDescription() throws DukeException { + if (!getArgs().containsKey(DESCRIPTION_ARG)) { + return; + } + if (getArg(DESCRIPTION_ARG).isBlank()) { + throw new DukeException(MISSING_DESCRIPTION); + } + } + + private void checkHasArgument() throws DukeException { + boolean hasDescArg = getArgs().containsKey(DESCRIPTION_ARG); + boolean hasGoalArg = getArgs().containsKey(GOAL_ARG); + boolean hasCategoryArg = getArgs().containsKey(CATEGORY_ARG); + boolean hasAmountArg = getArgs().containsKey(AMOUNT_ARG); + boolean isInType = getArg(TYPE_ARG).equalsIgnoreCase(TYPE_IN); + boolean isOutType = getArg(TYPE_ARG).equalsIgnoreCase(TYPE_OUT); + if (!getArgs().containsKey(DESCRIPTION_ARG)) { + return; + } + if (getArg(DESCRIPTION_ARG).isBlank()) { + throw new DukeException(MISSING_DESCRIPTION); + } + + if (isInType && !(hasDescArg || hasAmountArg || hasGoalArg)) { + throw new DukeException(MISSING_EDIT); + } else if (isOutType && !(hasDescArg || hasAmountArg || hasCategoryArg)) { + throw new DukeException(MISSING_EDIT); + } + } + + private void editTransaction(Ui ui) throws DukeException { + String type = getArg(TYPE_ARG).toLowerCase(); + int maxSize = getTransactionMaxSize(type); + int idx = parseIdx(maxSize) - 1; //-1 due to 0 based indexing for arraylist + assert idx >= 0 : "Index should be a valid integer greater than 0"; + + String transactionDescription = ""; + if (type.equals(TYPE_IN)) { + editIncome(idx); + transactionDescription = StateManager.getStateManager().getIncome(idx) + .getTransaction().getDescription(); + + } else if (type.equals(TYPE_OUT)) { + editExpense(idx); + transactionDescription = StateManager.getStateManager().getExpense(idx) + .getTransaction().getDescription(); + } + + if (!transactionDescription.isBlank()) { + printSuccess(ui, transactionDescription, idx + 1); // idx + 1 for format to show to user + } + } + + private int getTransactionMaxSize(String type) { + int maxSize = 0; + if (type.equals(TYPE_IN)) { + maxSize = StateManager.getStateManager().getIncomesSize(); + } else if (type.equals(TYPE_OUT)) { + maxSize = StateManager.getStateManager().getExpensesSize(); + } + return maxSize; + } + + private int parseIdx(int maxSize) throws DukeException { + int index = Integer.parseInt(getDescription()); + if (index < 1 || index > maxSize) { + throw new DukeException(INVALID_IDX); + } + return index; + } + + private void printSuccess(Ui ui, String description, int idx) { + String type = getArg(TYPE_ARG).toLowerCase(); + String transactionType = type.equals(TYPE_IN) ? "income" : "expense"; + String msg = "Successfully edited " + transactionType + " no." + idx + " " + description; + ui.print(msg); + } + + private void handleGoalEdit(int idx) { + StateManager stateManager = StateManager.getStateManager(); + String newGoalDescription = getArg(GOAL_ARG); + + int newGoalIdx = stateManager.getGoalIndex(newGoalDescription); + Goal newGoal = stateManager.getGoal(newGoalIdx); + if (newGoal == null) { + assert newGoalDescription.equals(StateManager.UNCATEGORISED_CLASS); + newGoal = stateManager.getUncategorisedGoal(); + } + stateManager.getIncome(idx).setGoal(newGoal); + } + + private void handleCategoryEdit(int idx) { + StateManager stateManager = StateManager.getStateManager(); + String newCategoryDescription = getArg(CATEGORY_ARG); + Expense expense = stateManager.getExpense(idx); + if (newCategoryDescription.equalsIgnoreCase(StateManager.UNCATEGORISED_CLASS)) { + expense.setCategory(stateManager.getUncategorisedCategory()); + return; + } + + int newCategoryIdx = stateManager.getCategoryIndex(newCategoryDescription); + Category newCategory = stateManager.getCategory(newCategoryIdx); + if (newCategory == null) { + newCategory = new Category(newCategoryDescription); + } + expense.setCategory(newCategory); + } + + private void editIncome(int idx) { + if (getArgs().containsKey(DESCRIPTION_ARG)) { + StateManager.getStateManager().getIncome(idx) + .getTransaction().setDescription(getArg(DESCRIPTION_ARG)); + } + if (getArgs().containsKey(AMOUNT_ARG)) { + StateManager.getStateManager().getIncome(idx) + .getTransaction().setAmount(Parser.parseNonNegativeDouble(getArg(AMOUNT_ARG))); + } + if (getArgs().containsKey(GOAL_ARG)) { + handleGoalEdit(idx); + } + } + + private void editExpense(int idx) { + if (getArgs().containsKey(DESCRIPTION_ARG)) { + StateManager.getStateManager().getExpense(idx) + .getTransaction().setDescription(getArg(DESCRIPTION_ARG)); + } + if (getArgs().containsKey(AMOUNT_ARG)) { + StateManager.getStateManager().getExpense(idx) + .getTransaction().setAmount(Parser.parseNonNegativeDouble(getArg(AMOUNT_ARG))); + } + if (getArgs().containsKey(CATEGORY_ARG)) { + handleCategoryEdit(idx); + } + } + +} diff --git a/src/main/java/seedu/duke/command/ExitCommand.java b/src/main/java/seedu/duke/command/ExitCommand.java new file mode 100644 index 0000000000..7dbec977cf --- /dev/null +++ b/src/main/java/seedu/duke/command/ExitCommand.java @@ -0,0 +1,11 @@ +package seedu.duke.command; + +import seedu.duke.ui.Ui; + +public class ExitCommand extends Command { + + @Override + public void execute(Ui ui) { + //Does nothing as the ShutDownHook will print the Bye message + } +} diff --git a/src/main/java/seedu/duke/command/ExportCommand.java b/src/main/java/seedu/duke/command/ExportCommand.java new file mode 100644 index 0000000000..61295caece --- /dev/null +++ b/src/main/java/seedu/duke/command/ExportCommand.java @@ -0,0 +1,158 @@ +package seedu.duke.command; + +import seedu.duke.classes.Expense; +import seedu.duke.classes.Transaction; +import seedu.duke.classes.Income; +import seedu.duke.classes.StateManager; +import seedu.duke.csv.CsvWriter; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.util.ArrayList; +import java.util.HashMap; + +import static seedu.duke.storage.Storage.exportStorageFileName; + +public class ExportCommand extends Command { + enum TransactionType { + IN, OUT, ALL, ERROR + } + + private static final String SUCESSFUL_MSG = "Transaction Data extracted"; + private static final String TYPE_ARG = "type"; + private static final String WRONG_TYPE_MSG = "Wrong type entered. Please enter /type in, /type out or blank"; + private static final String[] HEADERS = {"Type", "Description", "Date", "Amount", "Goal", "Category", "Recurrence"}; + private static final int TYPE = 0; + private static final int DESCRIPTION = 1; + private static final int DATE = 2; + private static final int AMOUNT = 3; + private static final int GOAL = 4; + private static final int CATEGORY = 5; + private static final int RECURRENCE = 6; + private static final String EMPTY_DATA = null; + private static final int DATA_LENGTH = 7; + private static final String INCOME_STRING = "Income"; + private static final String EXPENSE_STRING = "Expense"; + private ArrayList incomeArray; + private ArrayList expenseArray; + private CsvWriter csvFile; + private Ui ui; + + public ExportCommand(String description, HashMap args) throws DukeException { + super(description, args); + this.incomeArray = StateManager.getStateManager().getAllIncomes(); + this.expenseArray = StateManager.getStateManager().getAllExpenses(); + this.csvFile = new CsvWriter(exportStorageFileName); + } + + /** + * Writes the header of the export CSV File. + */ + public void writeHeader() { + csvFile.write(HEADERS); + } + + /** + * Converts the transaction object into an Array to be able to store into the CSV File. + * + * @param transaction Transaction object to be converted. + * @param row Array where the data is stored in. + * @return Array containing the data from the Transaction object. + */ + public String[] extractTransactionData(Transaction transaction, String[] row) { + String description = transaction.getDescription(); + String date = transaction.getDate().toString(); + String amount = ui.formatAmount(transaction.getAmount()); + row[DESCRIPTION] = description; + row[DATE] = date; + row[AMOUNT] = amount; + row[RECURRENCE] = transaction.getRecurrence().toString(); + return row; + } + + /** + * Exports all Income Transactions and writes to the CSV File. + */ + public void exportIncomeData() { + for (Income i : this.incomeArray) { + Transaction currentTransaction = i.getTransaction(); + String[] row = new String[DATA_LENGTH]; + row[TYPE] = INCOME_STRING; + row[GOAL] = i.getGoal().getDescription(); + row[CATEGORY] = EMPTY_DATA; + this.csvFile.write(extractTransactionData(currentTransaction, row)); + } + } + + /** + * Exports all Expense Transactions and writes to the CSV File. + */ + public void exportExpenseData() { + for (Expense e : this.expenseArray) { + Transaction currentTransaction = e.getTransaction(); + String[] row = new String[DATA_LENGTH]; + row[TYPE] = EXPENSE_STRING; + row[GOAL] = EMPTY_DATA; + row[CATEGORY] = e.getCategory().getName(); + this.csvFile.write(extractTransactionData(currentTransaction, row)); + } + } + + /** + * Check which transaction to be exported. + * + * @return returns the correct transaction type to be exported. + */ + public TransactionType checkType() { + String type = getArg(TYPE_ARG); + if (type == null) { + return TransactionType.ALL; + } + if (type.equalsIgnoreCase("in")) { + return TransactionType.IN; + } + if (type.equalsIgnoreCase("out")) { + return TransactionType.OUT; + } + return TransactionType.ERROR; + } + + /** + * Export the right data to the CSV File + * + * @param type The type of transaction to be export. + */ + void exportData(TransactionType type) { + switch (type) { + case IN: + exportIncomeData(); + break; + case OUT: + exportExpenseData(); + break; + default: + exportIncomeData(); + exportExpenseData(); + } + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to print in table format. + * @throws DukeException If the file cannot be created during the exporting process. + */ + @Override + public void execute(Ui ui) throws DukeException { + this.ui = ui; + TransactionType transactionType = checkType(); + if (transactionType.equals(TransactionType.ERROR)) { + ui.print(WRONG_TYPE_MSG); + return; + } + writeHeader(); + exportData(transactionType); + ui.print(SUCESSFUL_MSG); + csvFile.close(); + } +} diff --git a/src/main/java/seedu/duke/command/GoalCommand.java b/src/main/java/seedu/duke/command/GoalCommand.java new file mode 100644 index 0000000000..c6a076ba95 --- /dev/null +++ b/src/main/java/seedu/duke/command/GoalCommand.java @@ -0,0 +1,102 @@ +package seedu.duke.command; + +import seedu.duke.classes.Goal; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.util.HashMap; + +public class GoalCommand extends ClassificationCommand { + private static final String ADD_COMMAND = "add"; + private static final String REMOVE_COMMAND = "remove"; + private static final String AMOUNT = "amount"; + private static final String INVALID_INPUT = "Your goal input is empty/invalid :("; + private static final String INVALID_AMOUNT = "Invalid amount value specified..."; + + public GoalCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to format output. + * @throws DukeException if user input is invalid. + */ + @Override + public void execute(Ui ui) throws DukeException { + String inputType = validateInput(); + if (inputType == null) { + errorMessage(INVALID_INPUT); + } + if (inputType.equals(ADD_COMMAND)) { + validateAmount(); + String goalName = getArg(ADD_COMMAND); + Double amount = Parser.parseNonNegativeDouble(getArg(AMOUNT)); + addGoal(goalName, amount); + ui.print("Successfully added " + goalName + "!"); + } else if (inputType.equals(REMOVE_COMMAND)) { + String goalName = getArg(REMOVE_COMMAND); + removeGoal(goalName); + ui.print("Successfully removed " + goalName + "!"); + } + } + + /** + * Validates if amount specified by user is correct + * @throws DukeException if amount is invalid + */ + private void validateAmount() throws DukeException { + String amount = getArg(AMOUNT); + if (amount == null || amount.isBlank()) { + errorMessage(INVALID_INPUT); + } + Double parsedAmt = Parser.parseNonNegativeDouble(amount); + if (parsedAmt == null) { + errorMessage(INVALID_AMOUNT); + } else if (parsedAmt == 0) { + errorMessage(INVALID_AMOUNT); + } + } + + /** + * Adds goal to StateManager + * @param goal name of goal to add + * @param amount goal amount + * @throws DukeException if goal already exists in list + */ + private void addGoal(String goal, double amount) throws DukeException { + StateManager state = StateManager.getStateManager(); + if (state.getGoalIndex(goal) == -1) { + Goal goalToAdd = new Goal(goal, amount); + state.addGoal(goalToAdd); + } else { + String alreadyExists = "Failed to add '" + goal + "' as it already exists!"; + throw new DukeException(alreadyExists); + } + + } + + /** + * Removes goal in StateManager + * @param goal name of goal to remove + * @throws DukeException if goal does not already exist in list + */ + private void removeGoal(String goal) throws DukeException { + StateManager state = StateManager.getStateManager(); + int index = state.getGoalIndex(goal); + Goal goalToRemove = state.getGoal(index); + boolean removedGoal = false; + if (index != -1) { + state.unassignGoalTransactions(goalToRemove); + removedGoal = state.removeGoal(goalToRemove); + } + if (!removedGoal) { + String failedRemoval = "Failed to remove '" + goal + "' as it does not exist!"; + throw new DukeException(failedRemoval); + } + } + +} diff --git a/src/main/java/seedu/duke/command/HelpCommand.java b/src/main/java/seedu/duke/command/HelpCommand.java new file mode 100644 index 0000000000..c9f43eb8a7 --- /dev/null +++ b/src/main/java/seedu/duke/command/HelpCommand.java @@ -0,0 +1,375 @@ +package seedu.duke.command; + +import java.util.ArrayList; +import java.util.HashMap; + +import seedu.duke.ui.Ui; + +public class HelpCommand extends Command { + private static final String LINE_DIVIDER = ""; + private static final String[] FULL_LIST_HEADERS = {"Command", "Description"}; + private static final String[] FLAG_DESCRIPTION_HEADERS = {"Option", "Description"}; + private static final Integer[] CUSTOM_COLUMN_WIDTH = {15, 1000}; + private static final String HELP_COMMAND = "help"; + private static final String HELP_DESCRIPTION = "Shows a list of all the commands available to the user"; + private static final String IN_COMMAND = "in"; + private static final String IN_DESCRIPTION = "Adds an income towards goal"; + private static final String IN_COMMAND_USAGE = " DESCRIPTION /amount AMOUNT [/goal GOAL] [/date DATE in DDMMYYYY]" + + " [/recurrence RECURRENCE]"; + private static final String[] IN_COMMAND_FLAGS = {"/amount", "/goal", "/date", "/recurrence"}; + private static final String[] IN_COMMAND_FLAGS_DESCRIPTION = {"Amount to be added", + "The goal to classify it under", + "Date of the transaction", + "Indicates whether the income" + + " added is recurring"}; + private static final String OUT_COMMAND = "out"; + private static final String OUT_DESCRIPTION = "Adds an expense for a category"; + private static final String OUT_COMMAND_USAGE = " DESCRIPTION /amount AMOUNT " + + "[/category CATEGORY] [/date DATE in DDMMYYYY]" + + " [/recurrence RECURRENCE]"; + private static final String[] OUT_COMMAND_FLAGS = {"/amount", "/category", "/date", "/recurrence"}; + private static final String[] OUT_COMMAND_FLAGS_DESCRIPTION = {"Amount to be deducted", + "The spending category to classify it under", + "Date of the transaction", + "Indicates whether the expense" + + " added is recurring"}; + private static final String DELETE_COMMAND = "delete"; + private static final String DELETE_DESCRIPTION = "Delete a specific transaction based on the index in the list"; + private static final String DELETE_COMMAND_USAGE = " INDEX /type (in | out)"; + private static final String[] DELETE_COMMAND_FLAGS = {"/type"}; + private static final String[] DELETE_COMMAND_FLAGS_DESCRIPTION = {"To set whether it is a in or out transaction"}; + private static final String LIST_COMMAND = "list"; + private static final String LIST_DESCRIPTION = "Shows a list of all added transactions based on type"; + private static final String LIST_COMMAND_USAGE_TRANSACTION = " /type (in | out) [/goal GOAL] [/category CATEGORY]" + + " [/week] [/month]"; + private static final String LIST_COMMAND_USAGE_GOALCAT = " (goal | category)"; + private static final String[] LIST_COMMAND_FLAGS = {"/type", "/goal", "/category", "/week", "/month"}; + private static final String[] LIST_COMMAND_FLAGS_DESCRIPTION = {"To set whether to display \"in\" or" + + " \"out\" transactions", + "The goal which it is classified under", + "The spending category which" + + " it is classified under", + "To filter the transactions to those in the " + + "current week", + "To filter the transactions to those in the " + + "current month"}; + private static final String EXPORT_COMMAND = "export"; + private static final String EXPORT_DESCRIPTION = "Exports the transactions stored into a CSV File. " + + "By Default, it will export ALL transactions"; + private static final String EXPORT_COMMAND_USAGE = " [/type (in | out)]"; + private static final String[] EXPORT_COMMAND_FLAGS = {"/type"}; + private static final String[] EXPORT_COMMAND_FLAGS_DESCRIPTION = {"To set whether to extract all" + + " \"in\" or \"out\" transactions"}; + private static final String GOAL_COMMAND = "goal"; + private static final String GOAL_DESCRIPTION = "Add or remove goals"; + private static final String GOAL_ADD_USAGE = " /add NAME /amount AMOUNT"; + private static final String GOAL_REMOVE_USAGE = " /remove NAME"; + private static final String[] GOAL_COMMAND_FLAGS = {"/add", "/amount", "/remove"}; + private static final String[] GOAL_COMMAND_FLAGS_DESCRIPTION = {"Name of goal to be added", + "The amount set for the goal", + "Name of goal to be removed"}; + private static final String CATEGORY_COMMAND = "category"; + private static final String CATEGORY_DESCRIPTION = "Create or delete a spending category"; + private static final String CATEGORY_ADD_USAGE = " /add NAME"; + private static final String CATEGORY_REMOVE_USAGE = " /remove NAME"; + private static final String[] CATEGORY_COMMAND_FLAGS = {"/add", "/remove"}; + private static final String[] CATEGORY_COMMAND_FLAGS_DESCRIPTION = {"Name of spending category to be created", + "Name of spending category to be deleted"}; + private static final String EDIT_COMMAND = "edit"; + private static final String EDIT_DESCRIPTION = "Edits an existing transaction"; + private static final String EDIT_COMMAND_USAGE = " INDEX /type (in | out) (/description DESCRIPTION | " + + "/amount AMOUNT | /goal GOAL | /category CATEGORY)"; + private static final String[] EDIT_COMMAND_FLAGS = {"/type", "/description", "/amount", "/goal", "/category"}; + private static final String[] EDIT_COMMAND_FLAGS_DESCRIPTION = {"To specify either in or out " + + "transaction to be edited", + "New description to be specified", + "New amount to be specified", + "New goal to be specified", + "New category to be specified"}; + private static final String SUMMARY_COMMAND = "summary"; + private static final String SUMMARY_DESCRIPTION = "Shows the summarised total of transactions"; + private static final String SUMMARY_COMMAND_USAGE = " /type (in | out) [/day] [/week] [/month]"; + private static final String[] SUMMARY_COMMAND_FLAGS = {"/type", "/day", "/week", "/month"}; + private static final String[] SUMMARY_COMMAND_FLAGS_DESCRIPTION = {"To specific either in or out transaction to " + + "be listed", + "To filter transactions to those of current day", + "To filter the transactions to those in the " + + "current week", + "To filter the transactions to those in the " + + "current month"}; + + private static final String BYE_COMMAND = "bye"; + private static final String BYE_DESCRIPTION = "Exits the program"; + private static final String USAGE_PREFIX = "Usage: "; + private static final String INVALID_COMMAND = "NO SUCH COMMAND"; + private ArrayList> helpList; + + public HelpCommand(String command, HashMap args) { + super(command, args); + helpList = new ArrayList>(); + } + + /** + * Adds command name and its description to the ArrayList. + * + * @param command Command name. + * @param description Description of the command. + * @return ArrayList that contains both the command name and its description. + */ + public ArrayList convertCommandList(String command, String description) { + ArrayList tableData = new ArrayList(); + tableData.add(command); + tableData.add(description); + return tableData; + } + + /** + * Returns the Arraylist that contains the command name and its description. + * + * @param commandName Name of the command. + * @param commandDescription Description for the command. + * @return ArrayList that contains both the command and its description. + */ + public ArrayList printCommandDescription(String commandName, String commandDescription) { + ArrayList commandDescriptionList = convertCommandList(commandName, commandDescription); + return commandDescriptionList; + } + + /** + * Add all the commands into helpList to be printed out. + */ + public void printFullList() { + this.helpList.add(printCommandDescription(HELP_COMMAND, HELP_DESCRIPTION)); + this.helpList.add(printCommandDescription(IN_COMMAND, IN_DESCRIPTION)); + this.helpList.add(printCommandDescription(OUT_COMMAND, OUT_DESCRIPTION)); + this.helpList.add(printCommandDescription(DELETE_COMMAND, DELETE_DESCRIPTION)); + this.helpList.add(printCommandDescription(LIST_COMMAND, LIST_DESCRIPTION)); + this.helpList.add(printCommandDescription(CATEGORY_COMMAND, CATEGORY_DESCRIPTION)); + this.helpList.add(printCommandDescription(GOAL_COMMAND, GOAL_DESCRIPTION)); + this.helpList.add(printCommandDescription(EXPORT_COMMAND, EXPORT_DESCRIPTION)); + this.helpList.add(printCommandDescription(EDIT_COMMAND, EDIT_DESCRIPTION)); + this.helpList.add(printCommandDescription(SUMMARY_COMMAND, SUMMARY_DESCRIPTION)); + this.helpList.add(printCommandDescription(BYE_COMMAND, BYE_DESCRIPTION)); + assert this.helpList != null; + } + + /** + * Crafts the help usage string. + * + * @return help usage string. + */ + public String helpUsage() { + return USAGE_PREFIX + HELP_COMMAND; + } + + /** + * Crafts the bye usage string. + * + * @return bye usage string. + */ + public String byeUsage() { + return USAGE_PREFIX + BYE_COMMAND; + } + + /** + * Crafts the in usage string. + * + * @return in usage string. + */ + public String inUsage() { + return USAGE_PREFIX + IN_COMMAND + IN_COMMAND_USAGE; + } + + /** + * Crafts the out usage string. + * + * @return out usage string. + */ + public String outUsage() { + return USAGE_PREFIX + OUT_COMMAND + OUT_COMMAND_USAGE; + } + + /** + * Crafts the delete usage string. + * + * @return delete usage string. + */ + public String deleteUsage() { + return USAGE_PREFIX + DELETE_COMMAND + DELETE_COMMAND_USAGE; + } + + /** + * Crafts the list usage string for Transactions. + * + * @return list usage string for Transaction. + */ + public String listTransactionUsage() { + return USAGE_PREFIX + LIST_COMMAND + LIST_COMMAND_USAGE_TRANSACTION; + } + /** + * Crafts the list usage string for Goal and Category. + * + * @return list usage string for Goal and Category. + */ + public String listGoalCategoryUsage() { + return USAGE_PREFIX + LIST_COMMAND + LIST_COMMAND_USAGE_GOALCAT; + } + + /** + * Crafts the export usage string. + * + * @return export usage string. + */ + public String exportUsage() { + return USAGE_PREFIX + EXPORT_COMMAND + EXPORT_COMMAND_USAGE; + } + + /** + * Crafts the category add string. + * + * @return category add usage string. + */ + public String categoryAddUsage() { + return USAGE_PREFIX + CATEGORY_COMMAND + CATEGORY_ADD_USAGE; + } + + /** + * Crafts the category remove usage string. + * + * @return category remove usage string. + */ + public String categoryRemoveUsage() { + return USAGE_PREFIX + CATEGORY_COMMAND + CATEGORY_REMOVE_USAGE; + } + + /** + * Crafts the goal add string. + * + * @return goal add usage string. + */ + public String goalAddUsage() { + return USAGE_PREFIX + GOAL_COMMAND + GOAL_ADD_USAGE; + } + + /** + * Crafts the goal remove usage string. + * + * @return goal remove usage string. + */ + public String goalRemoveUsage() { + return USAGE_PREFIX + GOAL_COMMAND + GOAL_REMOVE_USAGE; + } + + /** + * Crafts the edit usage string. + * + * @return edit usage string. + */ + public String editUsage() { + return USAGE_PREFIX + EDIT_COMMAND + EDIT_COMMAND_USAGE; + } + + /** + * Crafts the edit usage string. + * + * @return edit usage string. + */ + public String summaryUsage() { + return USAGE_PREFIX + SUMMARY_COMMAND + SUMMARY_COMMAND_USAGE; + } + + /** + * Converts the command flags and description into ArrayList and stores it into helpList. + * + * @param flags Flags for the command. + * @param description Description for the flags. + */ + public void convertIntoList(String[] flags, String[] description) { + for (int i = 0; i < flags.length; i++) { + ArrayList row = new ArrayList(); + row.add(flags[i]); + row.add(description[i]); + this.helpList.add(row); + } + } + + /** + * Prints all the commands and their description or the specific commands's flag and their description. + * + * @param ui Ui class that is used to print in table format. + */ + public void updateOutput(Ui ui) { + if (getDescription().isBlank()) { + printFullList(); + ui.printTableRows(this.helpList, FULL_LIST_HEADERS, CUSTOM_COLUMN_WIDTH); + return; + } + + switch (getDescription().toLowerCase()) { + case "help": + ui.print(helpUsage()); + break; + case "in": + ui.print(inUsage()); + convertIntoList(IN_COMMAND_FLAGS, IN_COMMAND_FLAGS_DESCRIPTION); + break; + case "out": + ui.print(outUsage()); + convertIntoList(OUT_COMMAND_FLAGS, OUT_COMMAND_FLAGS_DESCRIPTION); + break; + case "delete": + ui.print(deleteUsage()); + convertIntoList(DELETE_COMMAND_FLAGS, DELETE_COMMAND_FLAGS_DESCRIPTION); + break; + case "list": + ui.print(listGoalCategoryUsage()); + ui.print(listTransactionUsage()); + convertIntoList(LIST_COMMAND_FLAGS, LIST_COMMAND_FLAGS_DESCRIPTION); + break; + case "export": + ui.print(exportUsage()); + convertIntoList(EXPORT_COMMAND_FLAGS, EXPORT_COMMAND_FLAGS_DESCRIPTION); + break; + case "goal": + ui.print(goalAddUsage()); + ui.print(goalRemoveUsage()); + convertIntoList(GOAL_COMMAND_FLAGS, GOAL_COMMAND_FLAGS_DESCRIPTION); + break; + case "category": + ui.print(categoryAddUsage()); + ui.print(categoryRemoveUsage()); + convertIntoList(CATEGORY_COMMAND_FLAGS, CATEGORY_COMMAND_FLAGS_DESCRIPTION); + break; + case "bye": + ui.print(byeUsage()); + break; + case "edit": + ui.print(editUsage()); + convertIntoList(EDIT_COMMAND_FLAGS, EDIT_COMMAND_FLAGS_DESCRIPTION); + break; + case "summary": + ui.print(summaryUsage()); + convertIntoList(SUMMARY_COMMAND_FLAGS, SUMMARY_COMMAND_FLAGS_DESCRIPTION); + break; + default: + ui.print(INVALID_COMMAND); + break; + } + + if (!this.helpList.isEmpty()) { + ui.printTableRows(this.helpList, FLAG_DESCRIPTION_HEADERS, CUSTOM_COLUMN_WIDTH); + } + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to print in table format. + */ + @Override + public void execute(Ui ui) { + ui.print(LINE_DIVIDER); + updateOutput(ui); + ui.print(LINE_DIVIDER); + } +} diff --git a/src/main/java/seedu/duke/command/ListCommand.java b/src/main/java/seedu/duke/command/ListCommand.java new file mode 100644 index 0000000000..ebe95e7a82 --- /dev/null +++ b/src/main/java/seedu/duke/command/ListCommand.java @@ -0,0 +1,382 @@ +package seedu.duke.command; + +import seedu.duke.classes.Category; +import seedu.duke.classes.Expense; +import seedu.duke.classes.Goal; +import seedu.duke.classes.Income; +import seedu.duke.classes.StateManager; +import seedu.duke.classes.Transaction; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.HashMap; + +public class ListCommand extends Command { + private static final String INVALID_TYPE_FORMAT = "I'm sorry, you need to specify a type in the format " + + "'list /type in' or 'list /type out' to view transactions, or 'list goal' and 'list category'"; + private static final String INVALID_GOAL_FORMAT = "You have entered /goal, but you have entered an invalid goal"; + private static final String INVALID_CATEGORY_FORMAT = "You have entered /category, but you have entered an " + + "invalid category"; + private static final String EMPTY_LIST = "It appears that we have come up empty. Why not try adding some" + + " transactions first?"; + private static final String[] IN_HEADERS = {"ID", "Description", "Date", "Amount", "Goal", "Recurrence"}; + private static final String[] OUT_HEADERS = {"ID", "Description", "Date", "Amount", "Category", "Recurrence"}; + private static final String IN = "IN TRANSACTIONS"; + private static final String OUT = "OUT TRANSACTIONS"; + private static final String GOAL = "goal"; + private static final String CATEGORY = "category"; + private static final String TYPE = "type"; + private static final String WEEK = "week"; + private static final String MONTH = "month"; + private static final String UNCATEGORISED = "Uncategorised"; + private static final int INVALID_VALUE = -1; + private Ui ui; + + public ListCommand(String description, HashMap args) { + super(description, args); + } + /** + * Executes the command. + * + * @param ui Ui class that is used to format output. + * @throws DukeException if user input is invalid. + */ + @Override + public void execute(Ui ui) throws DukeException { + this.ui = ui; + validateInput(); + listTypeHandler(); + } + + /** + * Entry point for input validation + * @throws DukeException if user input is invalid + */ + private void validateInput() throws DukeException { + if (validateDescriptionInput()) { + return; + } + checkArgs(); + } + + /** + * Checks if the user has specified the program arguments correctly + * @throws DukeException if arguments specified is invalid + */ + private void checkArgs() throws DukeException { + if (getArgs().isEmpty()) { + errorMessage(INVALID_TYPE_FORMAT); + } + + if (!getArgs().containsKey(TYPE)) { + errorMessage(INVALID_TYPE_FORMAT); + } + + if (getArgs().containsKey(TYPE)) { + String type = getArg(TYPE); + if (!type.equalsIgnoreCase("in") && !type.equalsIgnoreCase("out")) { + errorMessage(INVALID_TYPE_FORMAT); + } + } + + if (getArgs().containsKey(GOAL) && getArgs().containsKey(CATEGORY)) { + String multipleTypesError = "You can't use both /goal and /category"; + errorMessage(multipleTypesError); + } + + } + + /** + * Checks if user has entered the correct input in the description field + * @return true if user has specified the 'goal' or 'category' in description + * @throws DukeException if user enters an invalid input + */ + private boolean validateDescriptionInput() throws DukeException { + if (getDescription() == null && getArgs().isEmpty()) { + String emptyListCommandError = "The list command must be accompanied with additional parameters"; + errorMessage(emptyListCommandError); + } + String description = getDescription(); + if (description.isBlank()) { + return false; + } + if (description.equalsIgnoreCase(GOAL) || description.equalsIgnoreCase(CATEGORY)) { + if (!getArgs().isEmpty()) { + String parametersPresentError = "There should not be any other options accompanied by " + + "'list goal' and 'list category'"; + errorMessage(parametersPresentError); + } + } else { + String invalidDescription = "The only valid description input is 'list goal' and 'list category'"; + errorMessage(invalidDescription); + } + return true; + } + + /** + * Creates a standardised error message + * @param message the intended message to add on + * @throws DukeException to print the error message and not proceed further + */ + + private void errorMessage(String message) throws DukeException { + String commonMessage = "\nFor correct usage, please refer to the UG or 'help list'"; + throw new DukeException(message + commonMessage); + } + + /** + * Identify what type of list the user wants + * @throws DukeException if any of the called functions throws an exception + */ + + private void listTypeHandler() throws DukeException { + String description = getDescription(); + if (description != null && !description.isBlank()) { + printTypeStatus(description); + return; + } + String type = getArg(TYPE); + assert type != null; + if (type.equalsIgnoreCase("in")) { + checkInArgs(); + listIncome(); + } else if (type.equalsIgnoreCase("out")) { + checkOutArgs(); + listExpenses(); + } + } + + /** + * Validate input when user enters 'list /type in' as input + * @throws DukeException when subsequent arguments are incorrect + */ + private void checkInArgs() throws DukeException { + if (getArgs().containsKey(CATEGORY)) { + errorMessage("'list /type in' should be used with /goal, not /category"); + } + + if (getArgs().containsKey(GOAL)) { + String goal = getArg(GOAL); + if (goal.isBlank()) { + errorMessage(INVALID_GOAL_FORMAT); + } else if (goal.equalsIgnoreCase(UNCATEGORISED)) { + return; + } + int result = StateManager.getStateManager().getGoalIndex(goal); + if (result == INVALID_VALUE) { + errorMessage(INVALID_GOAL_FORMAT); + } + } + } + + /** + * Validate input when user enters 'list /type out' as input + * @throws DukeException when subsequent arguments are incorrect + */ + private void checkOutArgs() throws DukeException { + if (getArgs().containsKey(GOAL)) { + errorMessage("'list /type out' should be used with /category, not /goal"); + } + if (getArgs().containsKey(CATEGORY)) { + String category = getArg(CATEGORY); + if (category.isBlank()) { + errorMessage(INVALID_CATEGORY_FORMAT); + } else if (category.equalsIgnoreCase(UNCATEGORISED)) { + return; + } + int result = StateManager.getStateManager().getCategoryIndex(category); + if (result == INVALID_VALUE) { + errorMessage(INVALID_CATEGORY_FORMAT); + } + } + } + + /** + * Determines whether to print a list of goals or categories + * @param description user's input in the description field + */ + private void printTypeStatus(String description) { + if (description.equalsIgnoreCase(GOAL)) { + HashMap map = StateManager.getStateManager().getGoalsStatus(); + ui.printGoalsStatus(map); + } else if (description.equalsIgnoreCase(CATEGORY)) { + HashMap map = StateManager.getStateManager().getCategoriesStatus(); + ui.printCategoryStatus(map); + } + } + + /** + * Prints list of transactions + * @param listArray list of transactions to print + * @param headerMessage message to print for the header + */ + private void printList(ArrayList> listArray, String headerMessage) { + if (headerMessage.equals(IN)) { + ui.listTransactions(listArray, IN_HEADERS, headerMessage); + } else if (headerMessage.equals(OUT)) { + ui.listTransactions(listArray, OUT_HEADERS, headerMessage); + } + + } + + /** + * Retrieves list of income transactions + * @throws DukeException when list of income transactions is empty + */ + private void listIncome() throws DukeException { + String filterGoal = null; + if (getArgs().containsKey(GOAL)) { + filterGoal = getArg(GOAL).toLowerCase(); + } + ArrayList incomeArray = StateManager.getStateManager().getAllIncomes(); + ArrayList> printIncomes = new ArrayList<>(); + if (incomeArray == null || incomeArray.isEmpty()) { + throw new DukeException(EMPTY_LIST); + } + + if (getArgs().containsKey(WEEK)) { + incomeArray = filterIncome(incomeArray, false); + } else if (getArgs().containsKey(MONTH)) { + incomeArray = filterIncome(incomeArray, true); + } + + int index = 1; + for (Income i : incomeArray) { + String goal = i.getGoal().getDescription(); + if (filterGoal == null || filterGoal.equalsIgnoreCase(goal)) { + ArrayList transaction = formatTransaction(i.getTransaction(), index, goal); + printIncomes.add(transaction); + index++; + } + } + printList(printIncomes, IN); + + } + + /** + * Prints list of expenses + * @throws DukeException when expense transaction list is empty + */ + private void listExpenses() throws DukeException { + String filterCategory = null; + if (getArgs().containsKey(CATEGORY)) { + filterCategory = getArg(CATEGORY).toLowerCase(); + } + ArrayList expenseArray = StateManager.getStateManager().getAllExpenses(); + ArrayList> printExpenses = new ArrayList<>(); + if (expenseArray == null || expenseArray.isEmpty()) { + throw new DukeException(EMPTY_LIST); + } + + if (getArgs().containsKey(WEEK)) { + expenseArray = filterExpense(expenseArray, false); + } else if (getArgs().containsKey(MONTH)) { + expenseArray = filterExpense(expenseArray, true); + } + + int index = 1; + for (Expense i : expenseArray) { + String category = i.getCategory().getName(); + if (filterCategory == null || filterCategory.equalsIgnoreCase(category)) { + ArrayList transaction = formatTransaction(i.getTransaction(), index, category); + printExpenses.add(transaction); + index++; + } + } + printList(printExpenses, OUT); + } + + /** + * Formats transactions into the proper format to print + * @param transaction transaction to format + * @param index index of transaction in the list + * @param typeName goal/category of the transaction + * @return The formatted transaction to print + */ + private ArrayList formatTransaction(Transaction transaction, int index, String typeName) { + ArrayList transactionStrings = new ArrayList<>(); + transactionStrings.add(String.valueOf(index)); + transactionStrings.add(transaction.getDescription()); + transactionStrings.add(transaction.getDate().toString()); + transactionStrings.add(String.valueOf(ui.formatAmount(transaction.getAmount()))); + transactionStrings.add(typeName); + transactionStrings.add(transaction.getRecurrence().toString()); + return transactionStrings; + } + + /** + * Returns filtered arraylist of income transactions. + * Filters the income transactions based on the filter indicated. + * + * @param transactionsArrayList arraylist of income transaction. + * @param filterByMonth boolean to indicate if filter by month, else filter by week. + * @return ArrayList of income transaction. + */ + private ArrayList filterIncome(ArrayList transactionsArrayList, boolean filterByMonth) { + ArrayList filteredArrayList = new ArrayList<>(); + for (Income transaction : transactionsArrayList) { + LocalDate transactionDate = transaction.getTransaction().getDate(); + if (!filterByMonth && isThisWeek(transactionDate)) { + filteredArrayList.add(transaction); + } else if (filterByMonth && isThisMonth(transactionDate)) { + filteredArrayList.add(transaction); + } + } + return filteredArrayList; + } + + /** + * Returns filtered arraylist of expense transactions. + * Filters the expense transactions based on the filter indicated. + * + * @param transactionsArrayList arraylist of expense transaction. + * @param filterByMonth boolean to indicate if filter by month, else filter by week. + * @return ArrayList of expense transaction. + */ + private ArrayList filterExpense(ArrayList transactionsArrayList, boolean filterByMonth) { + ArrayList filteredArrayList = new ArrayList<>(); + for (Expense transaction : transactionsArrayList) { + LocalDate transactionDate = transaction.getTransaction().getDate(); + if (!filterByMonth && isThisWeek(transactionDate)) { + filteredArrayList.add(transaction); + } else if (filterByMonth && isThisMonth(transactionDate)) { + filteredArrayList.add(transaction); + } + } + return filteredArrayList; + } + + /** + * Checks if the transaction date is in the current week + * @param transactionDate date of the transaction + * @return true if transaction date is within the current week, else false + */ + private boolean isThisWeek(LocalDate transactionDate) { + LocalDate currentDate = LocalDate.now(); + LocalDate startOfWeek = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate endOfWeek = currentDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + if (transactionDate.isBefore(startOfWeek) || transactionDate.isAfter(endOfWeek)) { + return false; + } + return true; + } + + /** + * Checks if the transaction date is in the current month + * @param transactionDate date of the transaction + * @return true if transaction date is within the current month, else false + */ + private boolean isThisMonth(LocalDate transactionDate) { + LocalDate currentDate = LocalDate.now(); + LocalDate startOfMonth = currentDate.withDayOfMonth(1); + LocalDate endOfMonth = currentDate.withDayOfMonth(currentDate.lengthOfMonth()); + if (transactionDate.isBefore(startOfMonth) || transactionDate.isAfter(endOfMonth)) { + return false; + } + return true; + } +} diff --git a/src/main/java/seedu/duke/command/RemoveTransactionCommand.java b/src/main/java/seedu/duke/command/RemoveTransactionCommand.java new file mode 100644 index 0000000000..8b5c0ef583 --- /dev/null +++ b/src/main/java/seedu/duke/command/RemoveTransactionCommand.java @@ -0,0 +1,155 @@ +package seedu.duke.command; + +import seedu.duke.classes.Expense; +import seedu.duke.classes.Income; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; +import seedu.duke.classes.StateManager; + +import java.util.HashMap; + + +public class RemoveTransactionCommand extends Command { + + private static final String MISSING_IDX = "Index cannot be empty..."; + private static final String INVALID_IDX = "Please enter a valid index."; + private static final String MISSING_TYPE = "Please indicate the transaction type."; + + private static final String INVALID_TYPE = "Please indicate either /type in or /type out."; + + private static final String ERROR_MSG = "Error encountered when removing transaction."; + private static final String TYPE_ARG = "type"; + private static final String TYPE_IN = "in"; + private static final String TYPE_OUT = "out"; + + + public RemoveTransactionCommand(String description, HashMap args) { + super(description, args); + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to print in standardised format. + * @throws DukeException if the file cannot be created during the exporting process. + */ + @Override + public void execute(Ui ui) throws DukeException { + throwIfInvalidDescOrArgs(); + removeTransaction(ui); + } + + private void throwIfInvalidDescOrArgs() throws DukeException { + assert getDescription() != null; + assert getArgs() != null; + + if (getDescription().isBlank()) { + throw new DukeException(MISSING_IDX); + } + String description = getDescription(); + if (!isInteger(description)) { + throw new DukeException(INVALID_IDX); + } + + String typeArg = getArg(TYPE_ARG); + if (typeArg == null) { + throw new DukeException(MISSING_TYPE); + } + + if (!(typeArg.equalsIgnoreCase(TYPE_IN) || typeArg.equalsIgnoreCase(TYPE_OUT))) { + throw new DukeException(INVALID_TYPE); + } + } + + /** + * Checks if the description is a valid integer. + * + * @param description the description of the user input. + * @return true if the input is a valid integer, else false. + */ + private boolean isInteger(String description) { + try { + Integer.parseInt(description); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + /** + * Removes the transaction from StateManager based on the type. + * Prints success message if successful. + * + * @param ui Ui class that is used to print in standardised format. + * @throws DukeException if the transaction cannot be removed. + */ + private void removeTransaction(Ui ui) throws DukeException { + String type = getArg(TYPE_ARG).toLowerCase(); + int maxSize = getTransactionMaxSize(type); + int idx = parseIdx(maxSize) - 1; //-1 due to 0 based indexing for arraylist + assert idx >= 0 : "Index should be a valid integer greater than 0"; + + boolean isSuccess = false; + String transactionDescription = ""; + if (type.equals(TYPE_IN)) { + Income incomeEntry = StateManager.getStateManager().getIncome(idx); + transactionDescription = incomeEntry.getTransaction().getDescription(); + isSuccess = StateManager.getStateManager().removeIncome(incomeEntry); + } else if (type.equals(TYPE_OUT)) { + Expense expenseEntry = StateManager.getStateManager().getExpense(idx); + transactionDescription = expenseEntry.getTransaction().getDescription(); + isSuccess = StateManager.getStateManager().removeExpense(idx); + } + if (!isSuccess) { + throw new DukeException(ERROR_MSG); + } + printSuccess(ui, transactionDescription, idx + 1); // idx + 1 for format to show to user + } + + /** + * Returns the total number of transaction based on the type. + * + * @param type type of transaction (in/out). + * @return int total transactions. + */ + private int getTransactionMaxSize(String type) { + int maxSize = 0; + if (type.equals(TYPE_IN)) { + maxSize = StateManager.getStateManager().getIncomesSize(); + } else if (type.equals(TYPE_OUT)) { + maxSize = StateManager.getStateManager().getExpensesSize(); + } + return maxSize; + } + + /** + * Returns the valid index. + * + * @param maxSize max number of transactions based on the type. + * @return int valid index. + * @throws DukeException if the index is not in range of the number of transactions. + */ + private int parseIdx(int maxSize) throws DukeException { + int index = Integer.parseInt(getDescription()); + if (index < 1 || index > maxSize) { + throw new DukeException(INVALID_IDX); + } + return index; + } + + /** + * Prints success message when the transaction is removed. + * + * @param ui Ui class that is used to print in standardised format. + * @param description description of the transaction. + * @param idx index of the transaction. + */ + private void printSuccess(Ui ui, String description, int idx) { + String type = getArg(TYPE_ARG).toLowerCase(); + String transactionType = type.equals(TYPE_IN) ? "income" : "expense"; + String msg = "Successfully removed " + transactionType + " no." + idx + ": " + description; + ui.print(msg); + } + + +} diff --git a/src/main/java/seedu/duke/command/SummaryCommand.java b/src/main/java/seedu/duke/command/SummaryCommand.java new file mode 100644 index 0000000000..7a7052be8c --- /dev/null +++ b/src/main/java/seedu/duke/command/SummaryCommand.java @@ -0,0 +1,223 @@ +package seedu.duke.command; + +import seedu.duke.classes.Expense; +import seedu.duke.classes.Income; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.ui.Ui; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.HashMap; + +public class SummaryCommand extends Command { + + private static final String TYPE_ARG = "type"; + private static final String TYPE_IN = "in"; + private static final String TYPE_OUT = "out"; + private static final String MISSING_TYPE = "Please indicate the transaction type."; + private static final String INVALID_TYPE = "Please indicate either /type in or /type out."; + private static final String EMPTY_LIST = "It appears that we have come up empty. Why not try adding some" + + " transactions first?"; + private static final String STARTING_INCOME_MSG = "Good job! Total income so far"; + private static final String STARTING_EXPENSE_MSG = "Wise spending! Total expense so far"; + private static final String DAY_ARG = "day"; + private static final String WEEK_ARG = "week"; + private static final String MONTH_ARG = "month"; + + private LocalDate currentDate; + private boolean filterByDay = false; + private boolean filterByWeek = false; + private boolean filterByMonth = false; + private Ui ui; + + public SummaryCommand(String description, HashMap args) { + super(description, args); + currentDate = LocalDate.now(); + } + + public SummaryCommand(String description, HashMap args, LocalDate currentDate) { + super(description, args); + this.currentDate = currentDate; + } + + /** + * Executes the command. + * + * @param ui Ui class that is used to print in standardised format. + * @throws DukeException if the file cannot be created during the exporting process. + */ + @Override + public void execute(Ui ui) throws DukeException { + this.ui = ui; + throwIfInvalidDescOrArgs(); + getFilter(); + printSummary(); + } + + private void throwIfInvalidDescOrArgs() throws DukeException { + assert getArgs() != null; + + String typeArg = getArg(TYPE_ARG); + if (typeArg == null) { + throw new DukeException(MISSING_TYPE); + } + + if (!(typeArg.equalsIgnoreCase(TYPE_IN) || typeArg.equalsIgnoreCase(TYPE_OUT))) { + throw new DukeException(INVALID_TYPE); + } + } + + private void getFilter() { + if (getArgs().containsKey(DAY_ARG)) { + filterByDay = true; + } else if (getArgs().containsKey(WEEK_ARG)) { + filterByWeek = true; + } else if (getArgs().containsKey(MONTH_ARG)) { + filterByMonth = true; + } + } + + /** + * Returns the total sum of the income transaction. + * + * @return double total income. + * @throws DukeException if there is no income transaction available. + */ + private double getIncomeSummary() throws DukeException { + ArrayList incomeArray = StateManager.getStateManager().getAllIncomes(); + if (incomeArray == null || incomeArray.isEmpty()) { + throw new DukeException(EMPTY_LIST); + } + if (filterByDay || filterByWeek || filterByMonth) { + incomeArray = filterIncome(incomeArray); + } + double totalSum = 0; + for (Income income : incomeArray) { + totalSum = totalSum + income.getTransaction().getAmount(); + } + + return totalSum; + } + + /** + * Returns filtered arraylist of income transactions. + * Filters the income transactions based on the filter indicated. + * + * @param transactionsArrayList arraylist of income transaction. + * @return ArrayList of income transaction. + */ + private ArrayList filterIncome(ArrayList transactionsArrayList) { + ArrayList filteredArrayList = new ArrayList<>(); + for (Income transaction : transactionsArrayList) { + LocalDate transactionDate = transaction.getTransaction().getDate(); + if (filterByDay && isSameDay(transactionDate)) { + filteredArrayList.add(transaction); + } else if (filterByWeek && isSameWeek(transactionDate)) { + filteredArrayList.add(transaction); + } else if (filterByMonth && isSameMonth(transactionDate)) { + filteredArrayList.add(transaction); + } + } + return filteredArrayList; + } + + /** + * Returns the total sum of the expense transaction. + * + * @return double total expense. + * @throws DukeException if there is no expense transaction available. + */ + private double getExpenseSummary() throws DukeException { + ArrayList expenseArray = StateManager.getStateManager().getAllExpenses(); + if (expenseArray == null || expenseArray.isEmpty()) { + throw new DukeException(EMPTY_LIST); + } + if (filterByDay || filterByWeek || filterByMonth) { + expenseArray = filterExpense(expenseArray); + } + double totalSum = 0; + for (Expense expense : expenseArray) { + totalSum = totalSum + expense.getTransaction().getAmount(); + } + + return totalSum; + } + + /** + * Returns filtered arraylist of expense transactions. + * Filters the expense transactions based on the filter indicated. + * + * @param transactionsArrayList arraylist of expense transaction. + * @return ArrayList of expense transaction. + */ + private ArrayList filterExpense(ArrayList transactionsArrayList) { + ArrayList filteredArrayList = new ArrayList<>(); + for (Expense transaction : transactionsArrayList) { + LocalDate transactionDate = transaction.getTransaction().getDate(); + if (filterByDay && isSameDay(transactionDate)) { + filteredArrayList.add(transaction); + } else if (filterByWeek && isSameWeek(transactionDate)) { + filteredArrayList.add(transaction); + } else if (filterByMonth && isSameMonth(transactionDate)) { + filteredArrayList.add(transaction); + } + } + return filteredArrayList; + } + + private boolean isSameDay(LocalDate transactionDate) { + return currentDate.isEqual(transactionDate); + } + + private boolean isSameWeek(LocalDate transactionDate) { + LocalDate startOfWeek = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate endOfWeek = currentDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + if (transactionDate.isBefore(startOfWeek) || transactionDate.isAfter(endOfWeek)) { + return false; + } + return true; + } + + private boolean isSameMonth(LocalDate transactionDate) { + LocalDate startOfMonth = currentDate.withDayOfMonth(1); + LocalDate endOfMonth = currentDate.withDayOfMonth(currentDate.lengthOfMonth()); + if (transactionDate.isBefore(startOfMonth) || transactionDate.isAfter(endOfMonth)) { + return false; + } + return true; + } + + private void printSummary() throws DukeException { + String msg = ""; + if (getArg(TYPE_ARG).equalsIgnoreCase(TYPE_IN)) { + double totalSum = getIncomeSummary(); + msg = getSummaryMsg(TYPE_IN, totalSum); + } else if (getArg(TYPE_ARG).equalsIgnoreCase(TYPE_OUT)) { + double totalSum = getExpenseSummary(); + msg = getSummaryMsg(TYPE_OUT, totalSum); + } + ui.print(msg); + } + + private String getSummaryMsg(String type, double totalSum) { + String msg = ""; + if (type.equalsIgnoreCase(TYPE_IN)) { + msg = STARTING_INCOME_MSG; + } else { + msg = STARTING_EXPENSE_MSG; + } + if (filterByDay) { + msg = msg + " for Today: $"; + } else if (filterByWeek) { + msg = msg + " for This Week: $"; + } else if (filterByMonth) { + msg = msg + " for This Month: $"; + } else { + msg = msg + ": $"; + } + return msg + ui.formatAmount(totalSum); + } +} diff --git a/src/main/java/seedu/duke/csv/CsvReader.java b/src/main/java/seedu/duke/csv/CsvReader.java new file mode 100644 index 0000000000..c4616f63e4 --- /dev/null +++ b/src/main/java/seedu/duke/csv/CsvReader.java @@ -0,0 +1,45 @@ +package seedu.duke.csv; + +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.exceptions.CsvValidationException; +import seedu.duke.exception.DukeException; + +import java.io.FileReader; +import java.io.IOException; + +public class CsvReader { + private CSVReader reader; + + public CsvReader(String filePath) throws DukeException { + try { + FileReader fileReader = new FileReader(filePath); + this.reader = new CSVReaderBuilder(fileReader).withSkipLines(1).build(); + } catch (IOException e) { + throw new DukeException(""); + } + } + + /** + * Reads a line in CSV File + * + * @return Array of String from a row in CSV File + * @throws DukeException if unable to read the file + */ + public String[] readLine() throws DukeException{ + try { + String[] line = reader.readNext(); + return line; + } catch (IOException | CsvValidationException e) { + throw new DukeException("Cannot read file"); + } + } + + public void close() throws DukeException { + try { + reader.close(); + } catch (IOException e) { + throw new DukeException("Error Closing File"); + } + } +} diff --git a/src/main/java/seedu/duke/csv/CsvWriter.java b/src/main/java/seedu/duke/csv/CsvWriter.java new file mode 100644 index 0000000000..15d773e78c --- /dev/null +++ b/src/main/java/seedu/duke/csv/CsvWriter.java @@ -0,0 +1,52 @@ +package seedu.duke.csv; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; + +import com.opencsv.CSVWriter; +import seedu.duke.exception.DukeException; + +public class CsvWriter { + + private CSVWriter writer; + + public CsvWriter(String fullPath) throws DukeException { + this(fullPath, false); + } + + public CsvWriter(String fullPath, boolean isAppend) throws DukeException { + try { + Writer fileWriter = new FileWriter(fullPath, isAppend); + this.writer = new CSVWriter(fileWriter, CSVWriter.DEFAULT_SEPARATOR, + CSVWriter.DEFAULT_QUOTE_CHARACTER, + CSVWriter.DEFAULT_ESCAPE_CHARACTER, + System.getProperty("line.separator")); + } catch (IOException e) { + throw new DukeException("Cannot create file"); + } + } + + /** + * Writes data to the CSV File + * + * @param data array of data to be written into the file + */ + public void write(String[] data) { + assert writer != null; + writer.writeNext(data); + } + + /** + * Close the CSV File + * + * @throws DukeException if unable to close the CSV File + */ + public void close() throws DukeException { + try { + writer.close(); + } catch (IOException e) { + throw new DukeException("Error Closing File"); + } + } +} diff --git a/src/main/java/seedu/duke/exception/DukeException.java b/src/main/java/seedu/duke/exception/DukeException.java new file mode 100644 index 0000000000..a60d91f983 --- /dev/null +++ b/src/main/java/seedu/duke/exception/DukeException.java @@ -0,0 +1,8 @@ +package seedu.duke.exception; + +public class DukeException extends Exception { + + public DukeException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/parser/Parser.java b/src/main/java/seedu/duke/parser/Parser.java new file mode 100644 index 0000000000..039de32bb5 --- /dev/null +++ b/src/main/java/seedu/duke/parser/Parser.java @@ -0,0 +1,246 @@ +package seedu.duke.parser; + +import seedu.duke.command.AddExpenseCommand; +import seedu.duke.command.AddIncomeCommand; +import seedu.duke.command.Command; +import seedu.duke.command.ExitCommand; +import seedu.duke.command.ListCommand; +import seedu.duke.command.RemoveTransactionCommand; +import seedu.duke.command.HelpCommand; +import seedu.duke.command.ExportCommand; +import seedu.duke.command.CategoryCommand; +import seedu.duke.command.GoalCommand; +import seedu.duke.command.SummaryCommand; +import seedu.duke.command.EditTransactionCommand; +import seedu.duke.exception.DukeException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Pattern; + + +public class Parser { + public static final String DATE_INPUT_PATTERN = "ddMMyyyy"; + public static final DateTimeFormatter DATE_INPUT_FORMATTER = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN); + private static final String SPACE_WITH_ARG_PREFIX = " /"; + private static final String ARG_PREFIX = "/"; + private static final String DELIM = " "; + private static final String EMPTY_STRING = ""; + private static final Pattern DBL_POS_PATTERN = Pattern.compile("^\\d*.?\\d{0,2}$"); + private static final Double DBL_POS_ZERO = 0.0; + private static final Double DBL_TEN_MILLION = 10_000_000.0; + private static final int SPLIT_LIMIT = 2; + + private static final String DUPLICATE_KEY_MSG = "Duplicate arguments detected. Refer to help for command usage."; + + public Parser() { + } + + + /** + * Parses user input into command, description and arguments hashmap. + * + * @param userInput the user input. + * @return Command to be executed. + * @throws DukeException if invalid command is supplied. + */ + public Command parse(String userInput) throws DukeException { + String trimmedInput = userInput.trim(); + + String commandWord = getCommandWord(trimmedInput); + String description = getDescription(trimmedInput); + HashMap argsMap = getArguments(trimmedInput); + + return getCommand(commandWord, description, argsMap); + } + + /** + * Instantiates the Command with description and hashmap of arguments. + * + * @param commandWord the command word + * @param description the description + * @param argsMap hashmap of arguments + * @return Command object + * @throws DukeException if invalid command is supplied. + */ + public Command getCommand(String commandWord, String description, + HashMap argsMap) throws DukeException { + switch (commandWord) { + case "bye": + return new ExitCommand(); + case "in": + return new AddIncomeCommand(description, argsMap); + case "out": + return new AddExpenseCommand(description, argsMap); + case "list": + return new ListCommand(description, argsMap); + case "delete": + return new RemoveTransactionCommand(description, argsMap); + case "help": + return new HelpCommand(description, argsMap); + case "export": + return new ExportCommand(description, argsMap); + case "category": + return new CategoryCommand(description, argsMap); + case "goal": + return new GoalCommand(description, argsMap); + case "summary": + return new SummaryCommand(description, argsMap); + case "edit": + return new EditTransactionCommand(description, argsMap); + default: + throw new DukeException("Sorry I do not understand your command"); + } + } + + /** + * Splits the user input and returns the command word. + * + * @param userInput the user input. + * @return String the command word. + */ + public String getCommandWord(String userInput) { + return userInput.split(DELIM, SPLIT_LIMIT)[0].toLowerCase(); + } + + /** + * Splits the user input and returns the description. + * Returns empty string if no description is found. + * + * @param userInput the user input. + * @return String the description. + */ + public String getDescription(String userInput) { + String[] splitInput = userInput.split(DELIM, SPLIT_LIMIT); + if (splitInput.length <= 1) { + return EMPTY_STRING; + } + String description = splitInput[1].split(SPACE_WITH_ARG_PREFIX, SPLIT_LIMIT)[0].trim(); + if (description.startsWith(ARG_PREFIX)) { + return EMPTY_STRING; + } + return description; + } + + /** + * Returns a hashmap of arguments from the user input. + * If no argument is supplied, empty hashmap will be returned. + * + * @param userInput the user input. + * @return HashMap of arguments. + * @throws DukeException if duplicate arguments exist. + */ + public HashMap getArguments(String userInput) throws DukeException { + String[] splitInput = userInput.split(SPACE_WITH_ARG_PREFIX, SPLIT_LIMIT); + HashMap argsMap = new HashMap<>(); + if (splitInput.length <= 1) { + return argsMap; + } + String[] spitArgs = splitInput[1].split(DELIM); + + String argName = spitArgs[0]; + ArrayList currentWords = new ArrayList<>(); + boolean hasArgValue = false; + for (int i = 1; i < spitArgs.length; i++) { + String word = spitArgs[i]; + if (word.startsWith(ARG_PREFIX)) { + checkIfKeyExist(argName, argsMap); + String argValue = convertArgValueListToString(currentWords); + argsMap.put(argName, argValue); + argName = word.substring(1); + currentWords.clear(); + hasArgValue = false; + } else { + currentWords.add(word); + hasArgValue = true; + } + } + if (!currentWords.isEmpty() || !argsMap.containsKey(argName) || !hasArgValue) { + checkIfKeyExist(argName, argsMap); + String argValue = convertArgValueListToString(currentWords); + argsMap.put(argName, argValue); + } + return argsMap; + } + + /** + * Converts arraylist of argument values to String separated + * by whitespace. + * + * @param argValues arraylist of argument values. + * @return String value. + */ + public String convertArgValueListToString(ArrayList argValues) { + if (argValues.isEmpty()) { + return EMPTY_STRING; + } + return String.join(DELIM, argValues).trim(); + } + + /** + * Checks if argument already exist. + * + * @param argName argument name. + * @param argsMap hashmap of arguments. + * @throws DukeException if the argument exists. + */ + public static void checkIfKeyExist(String argName, HashMap argsMap) throws DukeException { + if (argsMap.containsKey(argName)) { + throw new DukeException(DUPLICATE_KEY_MSG); + } + } + + /** + * Parses a double from string + * @param value String to be parsed + * @return parsed value if valid otherwise {@code null} + */ + public static Double parseDouble(String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Parses a double and ensures that the value is not negative and not larger than + * or equal to ten million. Additionally, enforces input only has at most 2 decimal + * places. + * @param value String to be parsed + * @return parsed value if valid otherwise {@code null} + */ + public static Double parseNonNegativeDouble(String value) { + Double parsedValue = parseDouble(value); + if (parsedValue == null + || !DBL_POS_PATTERN.matcher(value).matches() + || parsedValue.compareTo(DBL_POS_ZERO) < 0 + || parsedValue.compareTo(DBL_TEN_MILLION) >= 0 + ) { + return null; + } + + return parsedValue; + } + + /** + * Parses a date (in {@value DATE_INPUT_PATTERN} format) from string + * @param value Date string to be parsed + * @return LocalDate value if valid otherwise {@code null} + */ + public static LocalDate parseDate(String value) { + if (value == null) { + return null; + } + + try { + return LocalDate.parse(value, DATE_INPUT_FORMATTER); + } catch (DateTimeParseException exception) { + return null; + } + } + +} diff --git a/src/main/java/seedu/duke/storage/Storage.java b/src/main/java/seedu/duke/storage/Storage.java new file mode 100644 index 0000000000..597cb73069 --- /dev/null +++ b/src/main/java/seedu/duke/storage/Storage.java @@ -0,0 +1,426 @@ +package seedu.duke.storage; + +import seedu.duke.classes.Income; +import seedu.duke.classes.Expense; +import seedu.duke.classes.StateManager; +import seedu.duke.classes.Goal; +import seedu.duke.classes.Category; +import seedu.duke.classes.Transaction; +import seedu.duke.classes.TransactionRecurrence; +import seedu.duke.csv.CsvWriter; +import seedu.duke.exception.DukeException; +import seedu.duke.csv.CsvReader; +import seedu.duke.parser.Parser; + +import java.io.File; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; + +public class Storage { + + public static String exportStorageFileName; + private static final String DATE_PATTERN = "dd/MM/yyyy"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); + private static final String FAILED_CONVERT_TO_NON_NEG_DOUBLE = "Cannot convert amount into Double type in "; + private static final String FAILED_CONVERT_TO_LOCALDATE = "Cannot convert date into LocalDate type in "; + private static final String STORAGE_DIR = "./data"; + private static final String GOAL_STORAGE_FILE_NAME = STORAGE_DIR + "/goal-store.csv"; + private static final String CATEGORY_STORAGE_FILE_NAME = STORAGE_DIR + "/category-store.csv"; + private static final String INCOME_STORAGE_FILE_NAME = STORAGE_DIR + "/income-store.csv"; + private static final String EXPENSE_STORAGE_FILE_NAME = STORAGE_DIR + "/expense-store.csv"; + private static final String EXPORT_STORAGE_FILE_NAME = "./Transactions.csv"; + private static final String[] GOAL_HEADER = {"Description", "Amount"}; + private static final String[] CATEGORY_HEADER = {"Name"}; + private static final String[] INCOME_HEADER = {"Description", "Amount", "Date", "Goal", + "Recurrence", "Has Next Recurrence"}; + private static final String[] EXPENSE_HEADER = {"Description", "Amount", "Date", "Category", + "Recurrence", "Has Next Recurrence"}; + private static final int DESCRIPTION = 0; + private static final int AMOUNT = 1; + private static final int DATE = 2; + private static final int GOAL = 3; + private static final int CATEGORY = 3; + private static final int RECURRENCE = 4; + private static final int HAS_NEXT_RECURRENCE = 5; + + private static final int CATEGORY_ROW_LENGTH = 1; + private static final int GOAL_ROW_LENGTH = 2; + private static final int TRANSACTIONS_ROW_LENGTH = 6; + private static final int NOT_FOUND = -1; + + private static String goalStorageFileName; + private static String categoryStorageFileName; + private static String incomeStorageFileName; + private static String expenseStorageFileName; + + public Storage() { + goalStorageFileName = GOAL_STORAGE_FILE_NAME; + categoryStorageFileName = CATEGORY_STORAGE_FILE_NAME; + incomeStorageFileName = INCOME_STORAGE_FILE_NAME; + expenseStorageFileName = EXPENSE_STORAGE_FILE_NAME; + exportStorageFileName = EXPORT_STORAGE_FILE_NAME; + } + + public Storage(String goalFileName, String categoryFileName, String incomeFileName, String expenseFileName, + String exportFileName) { + goalStorageFileName = goalFileName; + categoryStorageFileName = categoryFileName; + incomeStorageFileName = incomeFileName; + expenseStorageFileName = expenseFileName; + exportStorageFileName = exportFileName; + } + public boolean checkDirExist() { + return checkDirExist(STORAGE_DIR); + } + + public boolean checkDirExist(String folderDirectory) { + File directory = new File(folderDirectory); + if (!directory.exists()) { + directory.mkdir(); + return false; + } + return true; + } + + /** + * Check if the columns in each row is it blank or empty. + * + * @param row Array of String from a row in the CSV File. + * @return true if there is no empty or blank column, false if there is empty or blank column. + */ + public boolean validRow(String[] row) { + for (String column : row) { + if (column.isBlank() || column.isEmpty()) { + return false; + } + } + return true; + } + + /** + * Check whether dateStr can be parsed into a LocalDate type and returns if possible. + * + * @param dateStr String to be parsed into a LocalDate type. + * @param fileName Current File that is using this function. + * @return date after parsing successful. + * @throws DukeException if unable to parse into a LocalDate type. + */ + public LocalDate validDate(String dateStr, String fileName) throws DukeException { + try { + return LocalDate.parse(dateStr, FORMATTER); + } catch (DateTimeParseException e) { + throw new DukeException(FAILED_CONVERT_TO_LOCALDATE + fileName); + } + } + + /** + * Check if the string can be converted into boolean. + * + * @param booleanStr String to be converted into boolean. + * @return true if can be converted, else return false. + */ + public boolean validBoolean(String booleanStr) { + return booleanStr.equalsIgnoreCase("true") || booleanStr.equalsIgnoreCase("false"); + } + + /** + * Get the goal based on the name provided. + * + * @param name goal name. + * @return Goal object that has that name. + */ + public Goal convertToGoal(String name) { + int index = StateManager.getStateManager().getGoalIndex(name); + Goal goal = StateManager.getStateManager().getGoal(index); + if (goal == null) { + goal = StateManager.getStateManager().getUncategorisedGoal(); + } + return goal; + } + + /** + * Get the category based on the name provided. + * + * @param name category name. + * @return Category object that has that name. + */ + public Category convertToCategory(String name) { + int index = StateManager.getStateManager().getCategoryIndex(name); + Category category = StateManager.getStateManager().getCategory(index); + if (category == null) { + category = StateManager.getStateManager().getUncategorisedCategory(); + } + return category; + } + + /** + * Check if the Transaction data is valid or Invalid. + * + * @param description Description of the Transaction. + * @param recurrence Recurrence of the Transaction. + * @param hasRecurrence Has Recurrence for the Transaction. + * @return False if all is valid, else return true. + */ + private boolean isTransactionInvalid(String description, String recurrence, String hasRecurrence) { + if (description.isBlank()) { + return true; + } + + if (TransactionRecurrence.getRecurrence(recurrence) == null) { + return true; + } + + if (!(validBoolean(hasRecurrence))) { + return true; + } + + return false; + } + + /** + * Convert all the data required into a Transaction Object. + * + * @param row Current transaction row being processed + * @return Transaction object created. + */ + public Transaction prepareTransaction(String[] row) { + String description = row[DESCRIPTION]; + String recurrence = row[RECURRENCE]; + String hasRecurrence = row[HAS_NEXT_RECURRENCE].strip(); + boolean parsedHasRecurrence = Boolean.parseBoolean(hasRecurrence); + if (isTransactionInvalid(description, recurrence, hasRecurrence)) { + return null; + } + + String amount = row[AMOUNT]; + Double parsedAmount = Parser.parseNonNegativeDouble(amount); + if (parsedAmount == null) { + return null; + } + + String date = row[DATE]; + LocalDate parsedDate; + try { + parsedDate = validDate(date, expenseStorageFileName); + } catch (DukeException e) { + System.out.println(e.getMessage()); + return null; + } + + Transaction transaction = new Transaction(description.strip(), parsedAmount, parsedDate); + transaction.setHasGeneratedNextRecurrence(parsedHasRecurrence); + if (recurrence != null) { + TransactionRecurrence transactionRecurrence = TransactionRecurrence.getRecurrence(recurrence); + transaction.setRecurrence(transactionRecurrence); + } + + return transaction; + } + + /** + * Loads all Goals objects from the CSV File. + * + * @throws DukeException if GOAL_STORAGE_FILENAME cannot be opened. + */ + public void loadGoal() throws DukeException { + CsvReader goalCsvFile = new CsvReader(goalStorageFileName); + String[] row; + Double amount; + StateManager stateManager = StateManager.getStateManager(); + while ((row = goalCsvFile.readLine()) != null) { + if (validRow(row) && row.length >= GOAL_ROW_LENGTH) { + String description = row[DESCRIPTION].strip(); + int goalIndex = stateManager.getGoalIndex(description); + if (description.equalsIgnoreCase(StateManager.UNCATEGORISED_CLASS) || (goalIndex != NOT_FOUND)) { + continue; + } + amount = Parser.parseNonNegativeDouble(row[AMOUNT]); + if (amount == null) { + System.out.println(FAILED_CONVERT_TO_NON_NEG_DOUBLE + goalStorageFileName); + continue; + } + Goal goal = new Goal(description, amount); + stateManager.addGoal(goal); + } + } + goalCsvFile.close(); + } + + /** + * Loads all the Category objects from the CSV File. + * + * @throws DukeException if CATEGORY_STORAGE_FILENAME cannot be opened. + */ + public void loadCategory() throws DukeException { + CsvReader categoryCsvFile = new CsvReader(categoryStorageFileName); + String[] row; + StateManager stateManager = StateManager.getStateManager(); + while ((row = categoryCsvFile.readLine()) != null) { + if (validRow(row) && row.length >= CATEGORY_ROW_LENGTH) { + String description = row[DESCRIPTION].strip(); + int categoryIndex = stateManager.getCategoryIndex(description); + if (description.equalsIgnoreCase(StateManager.UNCATEGORISED_CLASS) || categoryIndex != NOT_FOUND) { + continue; + } + Category category = new Category(description); + stateManager.addCategory(category); + } + } + categoryCsvFile.close(); + } + + /** + * Loads all the Income objects from the CSV File. + * + * @throws DukeException if INCOME_STORAGE_FILENAME cannot be opened. + */ + public void loadIncome() throws DukeException { + CsvReader incomeCsvFile = new CsvReader(incomeStorageFileName); + String[] row; + StateManager stateManager = StateManager.getStateManager(); + while ((row = incomeCsvFile.readLine()) != null) { + if (validRow(row) && row.length >= TRANSACTIONS_ROW_LENGTH) { + Transaction transaction = prepareTransaction(row); + if (transaction == null) { + continue; + } + + Goal goal = convertToGoal(row[GOAL]); + Income income = new Income(transaction, goal); + stateManager.addIncome(income); + } + } + stateManager.sortIncomes(); + incomeCsvFile.close(); + } + + /** + * Loads all Expense Objects from the CSV File. + * + * @throws DukeException if EXPENSE_STORAGE_FILENAME cannot be opened. + */ + public void loadExpense() throws DukeException { + CsvReader expenseCsvFile = new CsvReader(expenseStorageFileName); + String[] row; + StateManager stateManager = StateManager.getStateManager(); + while ((row = expenseCsvFile.readLine()) != null) { + if (validRow(row) && row.length >= TRANSACTIONS_ROW_LENGTH) { + Transaction transaction = prepareTransaction(row); + if (transaction == null) { + continue; + } + + Category category = convertToCategory(row[CATEGORY]); + Expense expense = new Expense(transaction, category); + stateManager.addExpense(expense); + } + } + stateManager.sortExpenses(); + expenseCsvFile.close(); + } + + public void load() throws DukeException { + if (checkDirExist()) { + loadGoal(); + loadCategory(); + loadIncome(); + loadExpense(); + } + } + + /** + * Save the current state of Goal objects into the CSV File. + * + * @throws DukeException if GOAL_STORAGE_FILENAME cannot be opened. + */ + public void saveGoal() throws DukeException { + CsvWriter goalStorageFile = new CsvWriter(goalStorageFileName); + StateManager stateManager = StateManager.getStateManager(); + ArrayList goalList = stateManager.getAllGoals(); + goalStorageFile.write(GOAL_HEADER); + for (Goal goal : goalList) { + String description = goal.getDescription(); + String amount = Double.toString(goal.getAmount()); + String[] row = {description, amount}; + goalStorageFile.write(row); + } + goalStorageFile.close(); + } + + /** + * Save the current state of Category objects into the CSV File. + * + * @throws DukeException if CATEGORY_STORAGE_FILENAME cannot be opened. + */ + public void saveCategory() throws DukeException { + CsvWriter categoryStorageFile = new CsvWriter(categoryStorageFileName); + StateManager stateManager = StateManager.getStateManager(); + ArrayList categoryList = stateManager.getAllCategories(); + categoryStorageFile.write(CATEGORY_HEADER); + for (Category category : categoryList) { + String name = category.getName(); + String[] row = {name}; + categoryStorageFile.write(row); + } + categoryStorageFile.close(); + } + + /** + * Saves the current state of Income objects into the CSV File. + * + * @throws DukeException if INCOME_STORAGE_FILENAME cannot be opened. + */ + public void saveIncome() throws DukeException { + CsvWriter incomeStorageFile = new CsvWriter(incomeStorageFileName); + StateManager stateManager = StateManager.getStateManager(); + ArrayList incomesList = stateManager.getAllIncomes(); + incomeStorageFile.write(INCOME_HEADER); + for (Income income : incomesList) { + Transaction transaction = income.getTransaction(); + String description = transaction.getDescription(); + String amount = Double.toString(transaction.getAmount()); + String date = transaction.getDate().format(FORMATTER); + String goal = income.getGoal().getDescription(); + String recurrence = transaction.getRecurrence().toString(); + String hasNextRecurrence = Boolean.toString(transaction.getHasGeneratedNextRecurrence()); + String[] row = {description, amount, date, goal, recurrence, hasNextRecurrence}; + incomeStorageFile.write(row); + } + incomeStorageFile.close(); + } + + /** + * Saves the current state of Expense objects into the CSV File. + * + * @throws DukeException if EXPENSE_STORAGE_FILENAME cannot be opened. + */ + public void saveExpense() throws DukeException { + CsvWriter expenseStorageFile = new CsvWriter(expenseStorageFileName); + StateManager stateManager = StateManager.getStateManager(); + ArrayList expensesList = stateManager.getAllExpenses(); + expenseStorageFile.write(EXPENSE_HEADER); + for (Expense expense : expensesList) { + Transaction transaction = expense.getTransaction(); + String description = transaction.getDescription(); + String amount = Double.toString(transaction.getAmount()); + String date = transaction.getDate().format(FORMATTER); + String category = expense.getCategory().getName(); + String recurrence = transaction.getRecurrence().toString(); + String hasNextRecurrence = Boolean.toString(transaction.getHasGeneratedNextRecurrence()); + String[] row = {description, amount, date, category, recurrence, hasNextRecurrence}; + expenseStorageFile.write(row); + } + expenseStorageFile.close(); + } + + public void save() throws DukeException { + checkDirExist(); + saveGoal(); + saveCategory(); + saveIncome(); + saveExpense(); + } + +} diff --git a/src/main/java/seedu/duke/ui/Ui.java b/src/main/java/seedu/duke/ui/Ui.java new file mode 100644 index 0000000000..2040fcab5a --- /dev/null +++ b/src/main/java/seedu/duke/ui/Ui.java @@ -0,0 +1,432 @@ +package seedu.duke.ui; + +import seedu.duke.classes.Category; +import seedu.duke.classes.TypePrint; +import seedu.duke.classes.Goal; +import seedu.duke.classes.StateManager; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Ui { + public static final int COLUMN_WIDTH = 10; + public static final int LIST_COLUMN_WIDTH = 30; + public static final int TYPE_WIDTH = 20; + private static final String ELLIPSIS = "..."; + private static final String PROGRAM_NAME = "FinText"; + private static final char FILLER_CHAR = ' '; + private static final char LIST_SEPARATOR = '='; + private static final int ID_COLUMN_PADDING = 2; + + private static final int SPACE_BETWEEN_COLS = 3; + private static final int PROGRESS_WIDTH = 35; + + private static final String AMOUNT_FORMAT = "%.2f"; + private static final char LINE_DELIMITER = '\n'; + private static final Integer[] TYPE_COLUMN_WIDTHS_WITH_PROGRESS = {TYPE_WIDTH, TYPE_WIDTH, PROGRESS_WIDTH}; + private static final Integer[] TYPE_COLUMN_WIDTHS_WO_PROGRESS = {TYPE_WIDTH, TYPE_WIDTH}; + private static final String EMPTY_STRING = ""; + + private final Scanner scanner; + private final OutputStream outputStream; + + public Ui() { + outputStream = System.out; + scanner = new Scanner(System.in); + } + + public Ui(OutputStream outputStream) { + this.outputStream = outputStream; + scanner = new Scanner(System.in); + } + + public void printTableRow(ArrayList rowValues, Integer[] customWidths) { + printTableRow(rowValues, null, customWidths); + } + + public void printTableRow(ArrayList rowValues, String[] headers, Integer[] customWidths) { + assert rowValues != null; + ArrayList colWidths = printTableHeader(headers, customWidths); + if (colWidths == null) { + colWidths = getDefaultColWidths(rowValues.size()); + colWidths = mergeColWidths(colWidths, customWidths); + } + print(formatColumnValues(colWidths, rowValues)); + } + + public void printTableRows(ArrayList> rows) { + printTableRows(rows, null, null); + } + + public void printTableRows(ArrayList> rows, String[] headers) { + printTableRows(rows, headers, null); + } + + public void printTableRows(ArrayList> rows, String[] headers, Integer[] customWidths) { + assert rows != null; + ArrayList colWidths = printTableHeader(headers, customWidths); + if (rows.isEmpty()) { + return; + } + + if (colWidths == null) { + colWidths = getPrintWidths(genColWidths(rows.get(0).size(), 0), customWidths); + } + + for (ArrayList rowValues : rows) { + print(formatColumnValues(colWidths, rowValues)); + } + } + + public ArrayList printTableHeader(String[] headers, Integer[] customWidths) { + if (headers == null) { + return null; + } + + ArrayList colWidths = (ArrayList) Arrays.stream(headers) + .parallel() + .map(String::length) + .collect(Collectors.toList()); + colWidths = getPrintWidths(colWidths, customWidths); + List headerList = Arrays.asList(headers); + print(formatColumnValues(colWidths, new ArrayList<>(headerList))); + return colWidths; + } + + public String formatAmount(Double value) { + return String.format(AMOUNT_FORMAT, value); + } + + public void print(String value) { + try { + outputStream.write(value.getBytes(StandardCharsets.UTF_8)); + outputStream.write(LINE_DELIMITER); + outputStream.flush(); + } catch (IOException e) { + // Fail quietly for now + } + } + + public void printGreeting() { + print("Welcome to " + PROGRAM_NAME + ", your personal finance tracker."); + } + + public void printBye() { + print("Bye Bye!"); + } + + public String readUserInput() throws Exception { + String userInput = EMPTY_STRING; + try { + userInput = scanner.nextLine(); + } catch (Exception e) { + if (e instanceof java.util.NoSuchElementException) { + System.exit(0); + } else { + throw new Exception(e.getMessage()); + } + } + return userInput; + } + + public void close() { + scanner.close(); + } + + private ArrayList genColWidths(int length, int width) { + return (ArrayList) IntStream.range(0, length) + .mapToObj(i -> width) + .collect(Collectors.toList()); + } + + private ArrayList getDefaultColWidths(int length) { + return genColWidths(length, COLUMN_WIDTH); + } + + private ArrayList getPrintWidths(ArrayList colWidths, Integer[] customWidths) { + if (customWidths == null) { + colWidths = mergeColWidths(colWidths, getDefaultColWidths(colWidths.size())); + } else { + colWidths = mergeColWidths(colWidths, customWidths); + } + return colWidths; + } + + private ArrayList mergeColWidths(ArrayList colWidths, ArrayList customWidths) { + if (customWidths == null) { + return mergeColWidths(colWidths, (Integer[]) null); + } + Integer[] customWidthArray = new Integer[customWidths.size()]; + customWidths.toArray(customWidthArray); + return mergeColWidths(colWidths, customWidthArray); + } + + private ArrayList mergeColWidths(ArrayList colWidths, Integer[] customWidths) { + assert colWidths != null; + if (customWidths == null) { + return colWidths; + } + + assert colWidths.size() <= customWidths.length; + int colCount = colWidths.size(); + for (int i = 0; i < colCount; ++i) { + colWidths.add(i, Math.max(colWidths.get(i), customWidths[i])); + } + + return colWidths; + } + + /** + * Formats column values by truncating them if exceeds width + * @param colWidths width of columns + * @param colValues column values + * @return formatted column values + */ + private String formatColumnValues(ArrayList colWidths, ArrayList colValues) { + assert colWidths != null; + assert colValues != null; + assert colWidths.size() >= colValues.size(); + + ArrayList finalValues = new ArrayList<>(colValues.size()); + int lastIdx = colValues.size() - 1; + for (int i = 0; i < colValues.size(); ++i) { + int maxWidth = colWidths.get(i); + String truncatedValue = colValues.get(i); + if (truncatedValue.length() > maxWidth) { + truncatedValue = truncatedValue.substring(0, maxWidth - ELLIPSIS.length()) + ELLIPSIS; + } else if (i != lastIdx) { + char[] fillerChars = new char[maxWidth - truncatedValue.length()]; + Arrays.fill(fillerChars, FILLER_CHAR); + truncatedValue += new String(fillerChars); + } + finalValues.add(i, truncatedValue); + } + + char[] spacer = new char[SPACE_BETWEEN_COLS]; + Arrays.fill(spacer, FILLER_CHAR); + return String.join(new String(spacer), finalValues); + } + + /** + * Prints all transactions in the list + * @param list list of transaction to print + * @param headers array of header widths + * @param headerMessage header message to print + */ + public void listTransactions(ArrayList> list, String[] headers, String headerMessage) { + String end = " transactions."; + if (list.size() == 1) { + end = " transaction."; + } + print("Alright! Displaying " + list.size() + end); + Integer[] columnWidths = {Integer.toString(list.size()).length() + ID_COLUMN_PADDING, LIST_COLUMN_WIDTH, + COLUMN_WIDTH, COLUMN_WIDTH, TYPE_WIDTH, COLUMN_WIDTH}; + String wrapper = createWrapper(columnWidths, headerMessage); + print(wrapper); + printTableRows(list, headers, columnWidths); + print(wrapper); + + } + + /** + * Creates border around output + * @param columnWidths width of each column + * @param headerMessage header message + * @return returns formatted border + */ + private String createWrapper(Integer[] columnWidths, String headerMessage) { + int totalSpace = Arrays.stream(columnWidths) + .mapToInt(Integer::intValue) + .sum(); + totalSpace = totalSpace + (SPACE_BETWEEN_COLS * columnWidths.length) - headerMessage.length(); + int leftSide = totalSpace / 2; + int rightSide = totalSpace - leftSide; + String leftPad = new String(new char[leftSide]).replace('\0', LIST_SEPARATOR); + String rightPad = new String(new char[rightSide]).replace('\0', LIST_SEPARATOR); + StringJoiner wrapper = new StringJoiner(" "); + wrapper.add(leftPad); + wrapper.add(headerMessage); + wrapper.add(rightPad); + return (wrapper.toString()); + } + + /** + * Prints list of active goals + * @param goalsMap list of goals + */ + public void printGoalsStatus(HashMap goalsMap) { + ArrayList goalsToPrint = new ArrayList<>(); + TypePrint uncategorised = null; + Goal uncategorisedGoal = StateManager.getStateManager().getUncategorisedGoal(); + if (goalsMap.containsKey(uncategorisedGoal)) { + String description = uncategorisedGoal.getDescription(); + double currentAmount = goalsMap.get(uncategorisedGoal); + uncategorised = new TypePrint(description, currentAmount); + goalsMap.remove(uncategorisedGoal); + } + for (Map.Entry entry : goalsMap.entrySet()) { + String description = entry.getKey().getDescription(); + double currentAmount = entry.getValue(); + double targetAmount = entry.getKey().getAmount(); + TypePrint goalEntry = new TypePrint(description, currentAmount, targetAmount); + goalsToPrint.add(goalEntry); + } + Comparator typeComparator = Comparator.comparing(TypePrint::getDescription); + goalsToPrint.sort(typeComparator); + if (uncategorised != null) { + goalsToPrint.add(uncategorised); + } + String headerMessage = "Goals Status"; + String wrapper = createWrapper(TYPE_COLUMN_WIDTHS_WITH_PROGRESS, headerMessage); + print(wrapper); + printStatus(goalsToPrint, true); + printUnusedGoals(goalsMap); + print(wrapper); + } + + /** + * Prints goal/category and their amounts + * @param arrayToPrint array of the type (goal/category) to print + */ + private void printStatus(ArrayList arrayToPrint, boolean showProgress) { + if (arrayToPrint.isEmpty()) { + String message = "No existing transactions"; + print(message); + return; + } + if (showProgress) { + String[] headers = {"Name", "Amount", "Progress"}; + printTableHeader(headers, TYPE_COLUMN_WIDTHS_WITH_PROGRESS); + } else { + String[] headers = {"Name", "Amount"}; + printTableHeader(headers, TYPE_COLUMN_WIDTHS_WO_PROGRESS); + } + for (TypePrint entry : arrayToPrint) { + ArrayList printEntry = new ArrayList<>(); + printEntry.add(entry.getDescription()); + printEntry.add(entry.getAmount()); + if (entry.targetAmountExists()) { + printEntry.add(progressBar(entry.getPercentage())); + printTableRow(printEntry, TYPE_COLUMN_WIDTHS_WITH_PROGRESS); + } else { + printTableRow(printEntry, TYPE_COLUMN_WIDTHS_WO_PROGRESS); + } + } + } + + /** + * Formats a progress bar + * @param percentage percentage to convert into a bar + */ + public String progressBar(Double percentage) { + int maxBars = 20; + int steps = 5; + double barCalculation = percentage / steps; + int barsToPrint = (int) Math.floor(barCalculation); + if (barsToPrint > maxBars) { + barsToPrint = maxBars; + } + String openingSeparator = "["; + String closingSeparator = "]"; + String progressBar = new String(new char[barsToPrint]).replace('\0', '='); + String filler = new String(new char[maxBars - barsToPrint]).replace('\0', ' '); + String progress = openingSeparator + progressBar + filler + + closingSeparator + " " + formatAmount(percentage) + "%"; + return progress; + } + + /** + * Prints list of unused goals + * @param goals list of goals + */ + private void printUnusedGoals(HashMap goals) { + HashSet keySet = new HashSet<>(goals.keySet()); + ArrayList> unusedGoals = new ArrayList<>(); + ArrayList goalList = StateManager.getStateManager().getAllGoals(); + for (Goal g : goalList) { + if (!keySet.contains(g)) { + ArrayList unusedGoal = new ArrayList<>(); + unusedGoal.add(g.getDescription()); + unusedGoal.add(formatAmount(g.getAmount())); + unusedGoals.add(unusedGoal); + } + } + if (unusedGoals.isEmpty()) { + return; + } + String unusedHeader = LINE_DELIMITER + "Unused Goals:"; + print(unusedHeader); + String[] header = {"Goal", "Target Amount"}; + printTableRows(unusedGoals, header, TYPE_COLUMN_WIDTHS_WO_PROGRESS); + } + + /** + * Prints list of all categories + * @param categoryMap hashmap of each category and the amount linked to each category + */ + public void printCategoryStatus(HashMap categoryMap) { + ArrayList categoriesToPrint = new ArrayList<>(); + Category uncategorisedCategory = StateManager.getStateManager().getUncategorisedCategory(); + TypePrint uncategorised = null; + if (categoryMap.containsKey(uncategorisedCategory)) { + String description = uncategorisedCategory.getName(); + double currentAmount = categoryMap.get(uncategorisedCategory); + uncategorised = new TypePrint(description, currentAmount); + categoryMap.remove(uncategorisedCategory); + } + + for (Map.Entry entry : categoryMap.entrySet()) { + String description = entry.getKey().getName(); + double currentAmount = entry.getValue(); + TypePrint categoryEntry = new TypePrint(description, currentAmount); + categoriesToPrint.add(categoryEntry); + } + Comparator typeComparator = Comparator.comparing(TypePrint::getDescription); + categoriesToPrint.sort(typeComparator); + if (uncategorised != null) { + categoriesToPrint.add(uncategorised); + } + String headerMessage = "Categories Status"; + String wrapper = createWrapper(TYPE_COLUMN_WIDTHS_WO_PROGRESS, headerMessage); + print(wrapper); + printStatus(categoriesToPrint, false); + printUnusedCategories(categoryMap); + print(wrapper); + } + + /** + * Prints list of unused categories + * @param categories list of categories + */ + private void printUnusedCategories(HashMap categories) { + HashSet keySet = new HashSet<>(categories.keySet()); + List unusedCategories = new ArrayList<>(); + ArrayList categoryList = StateManager.getStateManager().getAllCategories(); + for (Category c : categoryList) { + if (!keySet.contains(c)) { + unusedCategories.add(c.getName()); + } + } + if (unusedCategories.isEmpty()) { + return; + } + unusedCategories.sort(String::compareToIgnoreCase); + String header = LINE_DELIMITER + "Unused Categories:"; + print(header); + for (String s : unusedCategories) { + print(s); + } + } +} diff --git a/src/test/java/seedu/duke/classes/TransactionRecurrenceTest.java b/src/test/java/seedu/duke/classes/TransactionRecurrenceTest.java new file mode 100644 index 0000000000..172496ec39 --- /dev/null +++ b/src/test/java/seedu/duke/classes/TransactionRecurrenceTest.java @@ -0,0 +1,91 @@ +package seedu.duke.classes; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TransactionRecurrenceTest { + final Goal goal = new Goal("Test goal", 10.00); + final Category category = new Category("Test Category"); + + Income generatePastIncome(int expectedRecurrenceToGenerate) { + LocalDate timeNow = LocalDate.now(); + LocalDate transactionDate = timeNow.minusDays(expectedRecurrenceToGenerate * 7L); + Transaction transaction = new Transaction("Test Past", 10.00, transactionDate); + transaction.setRecurrence(TransactionRecurrence.WEEKLY); + return new Income(transaction, goal); + } + + Income generateFutureIncome() { + LocalDate timeNow = LocalDate.now(); + LocalDate transactionDate = timeNow.minusDays(6); + Transaction transaction = new Transaction("Test Future", 10.00, transactionDate); + transaction.setRecurrence(TransactionRecurrence.WEEKLY); + return new Income(transaction, goal); + } + + void generateRecurringIncomeEntries(int firstAmt, int secondAmt) { + ArrayList existingIncomeEntries = new ArrayList<>(); + existingIncomeEntries.add(generatePastIncome(firstAmt)); + existingIncomeEntries.add(generateFutureIncome()); + existingIncomeEntries.add(generatePastIncome(secondAmt)); + ArrayList newIncomes = TransactionRecurrence.generateRecurrentIncomes(existingIncomeEntries); + assertEquals(newIncomes.size(), firstAmt + secondAmt); + + ArrayList emptyIncomes = TransactionRecurrence.generateRecurrentIncomes(existingIncomeEntries); + emptyIncomes.addAll(TransactionRecurrence.generateRecurrentIncomes(newIncomes)); + assertEquals(emptyIncomes.size(), 0); + } + + @Test + void generateSingleRecurrentIncomeEntry() { + generateRecurringIncomeEntries(1, 1); + } + + @Test + void generateMultipleRecurrentIncomeEntries() { + generateRecurringIncomeEntries(5, 2); + } + + Expense generatePastExpense(int expectedRecurrenceToGenerate) { + LocalDate timeNow = LocalDate.now(); + LocalDate transactionDate = timeNow.minusDays(expectedRecurrenceToGenerate * 7L); + Transaction transaction = new Transaction("Test Past", 10.00, transactionDate); + transaction.setRecurrence(TransactionRecurrence.WEEKLY); + return new Expense(transaction, category); + } + + Expense generateFutureExpense() { + LocalDate timeNow = LocalDate.now(); + LocalDate transactionDate = timeNow.minusDays(6); + Transaction transaction = new Transaction("Test Future", 10.00, transactionDate); + transaction.setRecurrence(TransactionRecurrence.WEEKLY); + return new Expense(transaction, category); + } + + void generateRecurringExpenseEntries(int firstAmt, int secondAmt) { + ArrayList existingExpenseEntries = new ArrayList<>(); + existingExpenseEntries.add(generatePastExpense(firstAmt)); + existingExpenseEntries.add(generateFutureExpense()); + existingExpenseEntries.add(generatePastExpense(secondAmt)); + ArrayList newExpenses = TransactionRecurrence.generateRecurrentExpenses(existingExpenseEntries); + assertEquals(newExpenses.size(), firstAmt + secondAmt); + + ArrayList emptyExpenses = TransactionRecurrence.generateRecurrentExpenses(existingExpenseEntries); + emptyExpenses.addAll(TransactionRecurrence.generateRecurrentExpenses(newExpenses)); + assertEquals(emptyExpenses.size(), 0); + } + + @Test + void generateSingleRecurrentExpenseEntry() { + generateRecurringExpenseEntries(1, 1); + } + + @Test + void generateMultipleRecurrentExpenseEntries() { + generateRecurringExpenseEntries(5, 2); + } +} diff --git a/src/test/java/seedu/duke/command/AddExpenseCommandTest.java b/src/test/java/seedu/duke/command/AddExpenseCommandTest.java new file mode 100644 index 0000000000..7111ccd80d --- /dev/null +++ b/src/test/java/seedu/duke/command/AddExpenseCommandTest.java @@ -0,0 +1,178 @@ +/** + * The AddExpenseCommandTest class contains JUnit tests for the AddExpenseCommand class, + * which is responsible for adding expense transactions. + * It tests various scenarios such as valid inputs, missing or invalid descriptions, amounts, categories, + * and ensures proper handling of exceptions. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; + +import java.time.LocalDate; + +class AddExpenseCommandTest { + private static final DukeException MISSING_DESC_EXCEPTION = new DukeException("Description cannot be empty..."); + private static final DukeException MISSING_AMT_EXCEPTION = new DukeException("Amount cannot be empty..."); + private static final DukeException BAD_AMOUNT_EXCEPTION = new DukeException("Invalid amount value specified..."); + private static final DukeException MISSING_CAT_EXCEPTION = new DukeException("Category cannot be empty..."); + private static final DukeException BAD_RECURRENCE = new DukeException("Invalid recurrence period specified..."); + + /** + * Clears the StateManager after each test to ensure a clean slate for the next test. + */ + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Tests valid inputs for adding expense transactions. + */ + @Test + void validInputs() { + LocalDate date = LocalDate.now(); + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "out dinner /amount 10.50 /category food", + "Nice! The following expense has been tracked:\n" + + "Description Date Amount Category " + + " Recurrence\n" + + "dinner " + date + " 10.50 food " + + " none\n" + ), + new CommandTestCase( + "out pokemon card pack /amount 10.50 /category games", + "Nice! The following expense has been tracked:\n" + + "Description Date Amount Category " + + " Recurrence\n" + + "pokemon card pack " + date + " 10.50 games " + + " none\n" + ), new CommandTestCase( + "out dinner /amount 500", + "Nice! The following expense has been tracked:\n" + + "Description Date Amount Category " + + "Recurrence\n" + + "dinner " + date + " 500.00 Uncategorised " + + " none\n" + ), + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with missing or invalid descriptions. + */ + @Test + void missingDescription() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "out", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "out ", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "out /amount -1", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "out /amount 500", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "out /amount 500 /goal car", + MISSING_DESC_EXCEPTION + ), + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with missing or invalid amounts. + */ + @Test + void missingAmount() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "out dinner", + MISSING_AMT_EXCEPTION + ), + new CommandTestCase( + "out dinner /amount", + MISSING_AMT_EXCEPTION + ), + new CommandTestCase( + "out dinner /amount ", + MISSING_AMT_EXCEPTION + ), + }; + CommandTestCase.runTestCases(testCases); + } + + @Test + void badAmount() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "out dinner /amount -1", + BAD_AMOUNT_EXCEPTION + ), + new CommandTestCase( + "out dinner /amount -1 /category games", + BAD_AMOUNT_EXCEPTION + ) + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with missing or invalid categories. + */ + @Test + void missingClassification() { + LocalDate date = LocalDate.now(); + CommandTestCase[] testCases = new CommandTestCase[]{ + + new CommandTestCase( + "out dinner /category /amount 500", + MISSING_CAT_EXCEPTION + ), + new CommandTestCase( + "out dinner /amount 500 /category", + MISSING_CAT_EXCEPTION + ), + new CommandTestCase( + "out dinner /amount 500 /category ", + MISSING_CAT_EXCEPTION + ) + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with invalid recurrence. + */ + @Test + void badRecurrence() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "out pocket money /amount 50 /category dinner /recurrence", + BAD_RECURRENCE + ), + new CommandTestCase( + "out pocket money /amount 50 /recurrence /category dinner", + BAD_RECURRENCE + ), + new CommandTestCase( + "out pocket money /amount 50 /category dinner /recurrence random", + BAD_RECURRENCE + ) + }; + CommandTestCase.runTestCases(testCases); + } +} diff --git a/src/test/java/seedu/duke/command/AddIncomeCommandTest.java b/src/test/java/seedu/duke/command/AddIncomeCommandTest.java new file mode 100644 index 0000000000..916c158648 --- /dev/null +++ b/src/test/java/seedu/duke/command/AddIncomeCommandTest.java @@ -0,0 +1,276 @@ +/** + * The AddIncomeCommandTest class contains JUnit tests for the AddIncomeCommand class, + * which is responsible for adding income transactions. + * It tests various scenarios such as valid inputs, missing or invalid descriptions, amounts, dates, goals, + * recurrences, and ensures proper handling of exceptions. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.classes.Income; +import seedu.duke.classes.StateManager; +import seedu.duke.classes.TransactionRecurrence; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AddIncomeCommandTest { + private static final DukeException MISSING_DESC_EXCEPTION = new DukeException("Description cannot be empty..."); + private static final DukeException MISSING_AMT_EXCEPTION = new DukeException("Amount cannot be empty..."); + private static final DukeException BAD_AMOUNT_EXCEPTION = new DukeException("Invalid amount value specified..."); + private static final DukeException BAD_DATE_EXCEPTION = new DukeException("Invalid date specified..."); + private static final DukeException MISSING_GOAL_EXCEPTION = new DukeException("Goal cannot be empty..."); + private static final DukeException BAD_RECURRENCE = new DukeException("Invalid recurrence period specified..."); + private static final DukeException BAD_RECURRENCE_DATE_EXCEPTION = new DukeException( + "Cannot specify date for recurring transaction" + + " to be larger than 1 period in the past..." + ); + + /** + * Adds sample categories before each test. + */ + @BeforeEach + void addCategories() { + assertDoesNotThrow(() -> { + new CommandTestCase("goal /add car /amount 5000").evaluate(); + new CommandTestCase("goal /add PS5 /amount 300").evaluate(); + }); + } + + /** + * Clears the StateManager after each test to ensure a clean slate for the next test. + */ + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Tests valid inputs for adding income transactions. + */ + @Test + void validInputs() { + LocalDate date = LocalDate.now(); + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in part-time job /amount 500 /goal car", + "Nice! The following income has been tracked:\n" + + "Description Date Amount Goal " + + "Recurrence\n" + + "part-time job " + date + " 500.00 car " + + " none\n" + ), + new CommandTestCase( + "in red packet money /amount 50 /goal PS5", + "Nice! The following income has been tracked:\n" + + "Description Date Amount Goal " + + "Recurrence\n" + + "red packet money " + date + " 50.00 PS5 " + + "none\n" + ), + new CommandTestCase( + "in red packet money /amount 50 /goal PS5 /date 12102000", + "Nice! The following income has been tracked:\n" + + "Description Date Amount Goal " + + "Recurrence\n" + + "red packet money 2000-10-12 50.00 PS5 " + + "none\n" + ), + new CommandTestCase( + "in pocket money /amount 50 /goal PS5 /recurrence weekly", + "Nice! The following income has been tracked:\n" + + "Description Date Amount Goal " + + "Recurrence\n" + + "pocket money " + date + " 50.00 PS5 " + + "weekly\n", + () -> { + ArrayList incomes = StateManager.getStateManager().getAllIncomes(); + Income lastAddedIncome = incomes.get(2); + assertEquals(lastAddedIncome.getTransaction().getRecurrence(), TransactionRecurrence.WEEKLY); + } + ) + }; + CommandTestCase.runTestCases(testCases); + } + + @Test + void missingGoal() { + String goal = "missing"; + CommandTestCase tc = new CommandTestCase( + "in part-time job /amount 500 /goal " + goal, + new DukeException("Please add '" + goal + "' as a goal first.") + ); + tc.evaluate(); + } + + /** + * Tests cases with missing or invalid descriptions. + */ + @Test + void missingDescription() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "in ", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "in /amount -1", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "in /amount 500", + MISSING_DESC_EXCEPTION + ), + new CommandTestCase( + "in /amount 500 /goal car", + MISSING_DESC_EXCEPTION + ), + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with missing or invalid amounts. + */ + @Test + void missingAmount() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in part-time job", + MISSING_AMT_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount", + MISSING_AMT_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount ", + MISSING_AMT_EXCEPTION + ), + }; + CommandTestCase.runTestCases(testCases); + } + + @Test + void badAmount() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in part-time job /amount -1", + BAD_AMOUNT_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount -1 /goal car", + BAD_AMOUNT_EXCEPTION + ) + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with invalid dates. + */ + @Test + void badDate() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in part-time job /amount 10 /date", + BAD_DATE_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount 10 /date 32102000", + BAD_DATE_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount 10 /date 12-10-2000", + BAD_DATE_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount 10 /date ", + BAD_DATE_EXCEPTION + ), + new CommandTestCase( + "in part-time job /date /amount 10", + BAD_DATE_EXCEPTION + ), + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with missing or invalid goals. + */ + @Test + void missingClassification() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in part-time job /goal /amount 500", + MISSING_GOAL_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount 500 /goal", + MISSING_GOAL_EXCEPTION + ), + new CommandTestCase( + "in part-time job /amount 500 /goal ", + MISSING_GOAL_EXCEPTION + ) + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with invalid recurrences. + */ + @Test + void badRecurrence() { + CommandTestCase[] testCases = new CommandTestCase[]{ + new CommandTestCase( + "in pocket money /amount 50 /recurrence /goal PS5", + BAD_RECURRENCE + ), + new CommandTestCase( + "in pocket money /amount 50 /goal PS5 /recurrence random", + BAD_RECURRENCE + ) + }; + CommandTestCase.runTestCases(testCases); + } + + /** + * Tests cases with invalid recurrence dates. + */ + @Test + void badRecurrenceDate() { + LocalDate date = LocalDate.now(); + LocalDate goodDate = date.minusDays(6); + String goodDateStr = goodDate.format(Parser.DATE_INPUT_FORMATTER); + String badDate = date.minusWeeks(1).format(Parser.DATE_INPUT_FORMATTER); + CommandTestCase[] testCases = new CommandTestCase[] { + new CommandTestCase( + "in pocket money /amount 50 /goal PS5 /recurrence weekly /date " + goodDateStr, + "Nice! The following income has been tracked:\n" + + "Description Date Amount Goal " + + " Recurrence\n" + + "pocket money " + goodDate + " 50.00 PS5" + + " weekly\n" + ), + new CommandTestCase( + "in pocket money /amount 50 /goal PS5 /recurrence weekly /date " + badDate, + BAD_RECURRENCE_DATE_EXCEPTION + ), + }; + CommandTestCase.runTestCases(testCases); + } +} diff --git a/src/test/java/seedu/duke/command/CategoryCommandTest.java b/src/test/java/seedu/duke/command/CategoryCommandTest.java new file mode 100644 index 0000000000..bfa6c25fb6 --- /dev/null +++ b/src/test/java/seedu/duke/command/CategoryCommandTest.java @@ -0,0 +1,119 @@ +/** + * The CategoryCommandTest class contains JUnit tests for the CategoryCommand class, + * which is responsible for handling category-related commands. + * It tests various scenarios such as invalid categories, empty category names, adding and removing categories, + * and ensures proper handling of exceptions. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.Test; +import seedu.duke.classes.Category; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CategoryCommandTest { + + /** + * Tests whether an exception is thrown for an invalid category command. + * + * @throws DukeException if the test fails. + */ + @Test + void invalidCategory() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "category"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + CategoryCommand command = new CategoryCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests whether an exception is thrown for an empty category name during addition. + * + * @throws DukeException if the test fails. + */ + @Test + void emptyCategoryAdd() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "category /add "; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + CategoryCommand command = new CategoryCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests whether a valid category is successfully added. + * + * @throws DukeException if the test fails. + */ + @Test + void validCategory() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "category /add test"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + CategoryCommand command = new CategoryCommand(commandWord, args); + command.execute(ui); + assertEquals("Successfully added test!\n", outputStream.toString()); + } + + /** + * Tests whether an exception is thrown for an invalid category during removal. + * + * @throws DukeException if the test fails. + */ + @Test + void invalidRemoveCategory() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "category /remove categorytofail"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + CategoryCommand command = new CategoryCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests whether a valid category is successfully removed. + * + * @throws DukeException if the test fails. + */ + @Test + void validRemoveCategory() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + StateManager.getStateManager().addCategory(new Category("test")); + String userInput = "category /remove test"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + CategoryCommand command = new CategoryCommand(commandWord, args); + command.execute(ui); + assertEquals("Successfully removed test!\n", outputStream.toString()); + } + +} diff --git a/src/test/java/seedu/duke/command/CommandTestCase.java b/src/test/java/seedu/duke/command/CommandTestCase.java new file mode 100644 index 0000000000..c4a285e30c --- /dev/null +++ b/src/test/java/seedu/duke/command/CommandTestCase.java @@ -0,0 +1,67 @@ +/** + * The CommandTestCase class represents a test case for evaluating the behavior of a command. + * It allows specifying the input, expected output, expected exception, and executable for a command. + * The test case can be evaluated using the evaluate() method, and multiple test cases can be run using the + * runTestCases() method. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.function.Executable; + +public class CommandTestCase { + private final String commandInput; + private final String commandOutput; + private final Exception exception; + private final Executable executable; + + public CommandTestCase(String commandInput) { + this(commandInput, null, null, null); + } + + public CommandTestCase(String commandInput, Exception exception) { + this(commandInput, null, exception, null); + } + + public CommandTestCase(String commandInput, String commandOutput) { + this(commandInput, commandOutput, null, null); + } + + public CommandTestCase(String commandInput, String commandOutput, Executable executable) { + this(commandInput, commandOutput, null, executable); + } + + /** + * Constructs a CommandTestCase with the specified command input, expected output, expected exception, + * and executable. + * + * @param commandInput The input string representing the command to be tested. + * @param commandOutput The expected output string after executing the command. + * @param exception The expected exception that should be thrown during command execution. + * @param executable The executable representing the command logic to be executed. + */ + public CommandTestCase(String commandInput, String commandOutput, Exception exception, Executable executable) { + this.commandInput = commandInput; + this.commandOutput = commandOutput; + this.exception = exception; + this.executable = executable; + } + + /** + * Evaluates the current test case by running the command and asserting its behavior. + */ + public void evaluate() { + CommandTestUtils.runSingleCommand(commandInput, commandOutput, exception, executable); + } + + /** + * Runs multiple test cases by iterating through the provided array of CommandTestCase objects. + * + * @param testCases An array of CommandTestCase objects to be evaluated. + */ + public static void runTestCases(CommandTestCase[] testCases) { + for (CommandTestCase tc : testCases) { + tc.evaluate(); + } + } +} diff --git a/src/test/java/seedu/duke/command/CommandTestUtils.java b/src/test/java/seedu/duke/command/CommandTestUtils.java new file mode 100644 index 0000000000..acb7dc6dc6 --- /dev/null +++ b/src/test/java/seedu/duke/command/CommandTestUtils.java @@ -0,0 +1,53 @@ +/** + * The CommandTestUtils class provides utility methods for testing command execution. + * It includes a method to run a single command, specifying the command input, expected output, expected exception, + * and an optional executable for additional assertions. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.function.Executable; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +public class CommandTestUtils { + + /** + * Runs a single command test case by executing the provided command and asserting its behavior. + * + * @param command The input string representing the command to be tested. + * @param expectedOutput The expected output string after executing the command. + * @param expectedException The expected exception that should be thrown during command execution. + * @param executable The optional executable representing additional assertions on the command logic. + */ + public static void runSingleCommand(String command, String expectedOutput, Exception expectedException, + Executable executable) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + Parser parser = new Parser(); + if (expectedException != null) { + Exception thrownException = assertThrowsExactly(expectedException.getClass(), () -> { + parser.parse(command).execute(ui); + }, command); + assertEquals(expectedException.getMessage(), thrownException.getMessage(), command); + } else { + assertDoesNotThrow(() -> { + parser.parse(command).execute(ui); + }); + } + + if (expectedOutput != null) { + assertEquals(expectedOutput, outputStream.toString(), command); + } + + if (executable != null) { + assertDoesNotThrow(executable); + } + } +} diff --git a/src/test/java/seedu/duke/command/EditTransactionCommandTest.java b/src/test/java/seedu/duke/command/EditTransactionCommandTest.java new file mode 100644 index 0000000000..d80d29ca6c --- /dev/null +++ b/src/test/java/seedu/duke/command/EditTransactionCommandTest.java @@ -0,0 +1,142 @@ +/** + * The EditTransactionCommandTest class contains JUnit tests for the EditTransactionCommand class, + * which is responsible for editing transactions. + * It tests various scenarios such as missing arguments, invalid indices, too many arguments, and successful edits. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class EditTransactionCommandTest { + + private static final Parser parser = new Parser(); + private static final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private static final Ui ui = new Ui(outputStream); + + /** + * Populates the StateManager with sample transactions before each test. + */ + @BeforeEach + void populateStateManager() { + try { + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 18092023").execute(ui); + parser.parse("in allowance /amount 500 /goal car").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5").execute(ui); + parser.parse("out buy dinner /amount 15 /category food").execute(ui); + parser.parse("out popmart /amount 12 /category toy").execute(ui); + parser.parse("out grab /amount 20 /category transport").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Tests whether an exception is thrown when the index is missing. + * + * @throws DukeException if the test fails. + */ + @Test + void execute_missingIdx_exceptionThrown() throws DukeException { + Command command = parser.parse("edit /type in /description part-time job"); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Tests whether an exception is thrown when the type argument is missing. + * + * @throws DukeException if the test fails. + */ + @Test + void execute_missingTypeArgument_exceptionThrown() throws DukeException { + Command command = parser.parse("edit 1 "); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Tests whether an exception is thrown when the type value is missing. + * + * @throws DukeException if the test fails. + */ + @Test + void execute_missingTypeValue_exceptionThrown() throws DukeException { + Command command = parser.parse("edit 1 /type /description part-time job"); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + @Test + void execute_negativeIdx_exceptionThrown() throws DukeException { + Command command = parser.parse("edit -1 /type in /description part-time job"); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Tests whether an exception is thrown when the index is out of range. + * + * @throws DukeException if the test fails. + */ + @Test + void execute_outOfRangeIdx_exceptionThrown() throws DukeException { + Command command = parser.parse("edit 1000 /type in /description part-time job"); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + + /** + * Tests whether an exception is thrown when attempting to edit the date. + * + * @throws DukeException if the test fails. + */ + @Test + void execute_attemptToEditDate_exceptionThrown() throws DukeException { + Command command = parser.parse("edit 1 /type in /date 18102023"); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Tests whether a valid income transaction is successfully edited. + * + * @throws DukeException if the test fails. + */ + + @Test + void execute_validIncomeIdx_edited() throws DukeException { + Command command = parser.parse("edit 1 /type in /description salary"); + command.execute(ui); + String transactionDescription = StateManager.getStateManager().getIncome(0) // 0-based indexing + .getTransaction().getDescription(); + assertNotEquals("allowance", transactionDescription); + } + + /** + * Tests whether a valid expense transaction is successfully edited. + * + * @throws DukeException if the test fails. + */ + @Test + void execute_validExpenseIdx_edited() throws DukeException { + Command command = parser.parse("edit 2 /type out /amount 10"); + command.execute(ui); + Double transactionAmount = StateManager.getStateManager().getExpense(1) // 0-based indexing + .getTransaction().getAmount(); + assertNotEquals(12.0, transactionAmount); + } +} diff --git a/src/test/java/seedu/duke/command/ExportCommandTest.java b/src/test/java/seedu/duke/command/ExportCommandTest.java new file mode 100644 index 0000000000..2554a32f8c --- /dev/null +++ b/src/test/java/seedu/duke/command/ExportCommandTest.java @@ -0,0 +1,470 @@ +package seedu.duke.command; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.storage.Storage; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class ExportCommandTest { + + private static final String TEST_DIR = "./TestFiles"; + private static final String GOAL_STORAGE_FILENAME = TEST_DIR + "/goal-store.csv"; + private static final String CATEGORY_STORAGE_FILENAME = TEST_DIR + "/category-store.csv"; + private static final String INCOME_STORAGE_FILENAME = TEST_DIR + "/income-store.csv"; + private static final String EXPENSE_STORAGE_FILENAME = TEST_DIR + "/expense-store.csv"; + private static final String EXPORT_STORAGE_FILENAME = TEST_DIR + "/Transactions.csv"; + private static Parser parser = new Parser(); + private ByteArrayOutputStream outputStream; + private Storage storage; + + /** + * Initialise the storage object before each test. + */ + @BeforeEach + void initialise() { + File directory = new File(TEST_DIR); + if (!directory.exists()) { + directory.mkdir(); + } + storage = new Storage(GOAL_STORAGE_FILENAME, CATEGORY_STORAGE_FILENAME, INCOME_STORAGE_FILENAME, + EXPENSE_STORAGE_FILENAME, EXPORT_STORAGE_FILENAME); + } + + /** + * Remove the Transaction file after each test + */ + @AfterEach + void removeFile() { + File file = new File(EXPORT_STORAGE_FILENAME); + file.delete(); + } + + @Nested + class ExportCommandOutput { + /** + * Reset the state to original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(EXPORT_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Populate the state manager with values before the test. + */ + @BeforeEach + void populateStateManager() { + try { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 29102023").execute(ui); + parser.parse("in allowance /amount 500 /goal car /date 29102023").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5 /date 29102023").execute(ui); + parser.parse("out buy dinner /amount 15 /category food /date 29102023 /recurrence monthly") + .execute(ui); + parser.parse("out popmart /amount 12 /category toy /date 29102023").execute(ui); + parser.parse("out grab /amount 20 /category transport /date 29102023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Test if it prints the export sucessful message after finish exporting without error. + * @throws DukeException if command cannot be executed. + */ + @Test + public void exportSuccessful() throws DukeException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + assertEquals("Transaction Data extracted\n", outputStream.toString()); + } + + /** + * Test if export command still work if there is an existing export file. + * @throws DukeException if command fails to execute. + */ + @Test + public void exportFileWhenExist() throws DukeException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + assertEquals("Transaction Data extracted\n", outputStream.toString()); + } + } + + @Nested + class TypeIn { + + /** + * Populate the state manager with values before the test. + */ + @BeforeEach + void populateStateManager() { + try { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 29102023").execute(ui); + parser.parse("in allowance /amount 500 /goal car /date 29102023").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5 /date 29102023").execute(ui); + parser.parse("out buy dinner /amount 15 /category food /date 29102023 /recurrence monthly") + .execute(ui); + parser.parse("out popmart /amount 12 /category toy /date 29102023").execute(ui); + parser.parse("out grab /amount 20 /category transport /date 29102023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Reset the state to original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(EXPORT_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if the export command successfully export all income transactions. + * This test is for Windows OS. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.WINDOWS}) + public void exportFileInTransactionsWindows() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Windows/valid/Transactions-in.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if the export command successfully export all income transactions. + * This test is for MacOS. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.MAC}) + public void exportFileInTransactionsMac() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/MacOS/valid/Transactions-in.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if the export command successfully export all income transactions. + * This test is for Linux. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.LINUX}) + public void exportFileInTransactionsLinux() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Linux/valid/Transactions-in.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + } + + @Nested + class TypeAll { + + /** + * Populate the state manager with values before the test. + */ + @BeforeEach + void populateStateManager() { + try { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 29102023").execute(ui); + parser.parse("in allowance /amount 500 /goal car /date 29102023").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5 /date 29102023").execute(ui); + parser.parse("out buy dinner /amount 15 /category food /date 29102023 /recurrence monthly") + .execute(ui); + parser.parse("out popmart /amount 12 /category toy /date 29102023").execute(ui); + parser.parse("out grab /amount 20 /category transport /date 29102023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Reset the state to original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(EXPORT_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if the export command successfully export all transactions. + * This test is for Windows OS. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.WINDOWS}) + public void exportFileAllTransactionsWindows() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Windows/valid/Transactions-all.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if the export command successfully export all transactions. + * This test is for MacOS. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.MAC}) + public void exportFileAllTransactionsMac() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/MacOS/valid/Transactions-all.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if the export command successfully export all transactions. + * This test is for Linux. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.LINUX}) + public void exportFileAllTransactionsLinux() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Linux/valid/Transactions-all.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + } + + @Nested + class TypeOut { + + /** + * Populate the state manager with values before the test. + */ + @BeforeEach + void populateStateManager() { + try { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 29102023").execute(ui); + parser.parse("in allowance /amount 500 /goal car /date 29102023").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5 /date 29102023").execute(ui); + parser.parse("out buy dinner /amount 15 /category food /date 29102023 /recurrence monthly") + .execute(ui); + parser.parse("out popmart /amount 12 /category toy /date 29102023").execute(ui); + parser.parse("out grab /amount 20 /category transport /date 29102023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Reset the state to original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(EXPORT_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if the export command successfully export expense transactions. + * This test is for Windows OS. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.WINDOWS}) + public void exportFileOutTransactionsWindows() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type out"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Windows/valid/Transactions-out.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if the export command successfully export expense transactions. + * This test is for MacOS. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.MAC}) + public void exportFileOutTransactionsMac() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type out"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/MacOS/valid/Transactions-out.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if the export command successfully export expense transactions. + * This test is for Linux. + * @throws DukeException if command fails to execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.LINUX}) + public void exportFileOutTransactionsLinux() throws DukeException, IOException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type out"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + File output = new File(EXPORT_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Linux/valid/Transactions-out.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + } + + @Nested + class TypeError { + /** + * Populate the state manager with values before the test. + */ + @BeforeEach + void populateStateManager() { + try { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 29102023").execute(ui); + parser.parse("in allowance /amount 500 /goal car /date 29102023").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5 /date 29102023").execute(ui); + parser.parse("out buy dinner /amount 15 /category food /date 29102023").execute(ui); + parser.parse("out popmart /amount 12 /category toy /date 29102023").execute(ui); + parser.parse("out grab /amount 20 /category transport /date 29102023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Reset the state to original after each test. + */ + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Test if the error message is thrown if the type enter is a wrong format. + * @throws DukeException if the type is not correctly specified. + */ + @Test + public void exportWrongType() throws DukeException { + outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String userInput = "export /type adsad"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ExportCommand command = new ExportCommand(commandWord, args); + command.execute(ui); + assertEquals("Wrong type entered. Please enter /type in, /type out or blank\n" + , outputStream.toString()); + } + } +} diff --git a/src/test/java/seedu/duke/command/GoalCommandTest.java b/src/test/java/seedu/duke/command/GoalCommandTest.java new file mode 100644 index 0000000000..f2c3da9fe6 --- /dev/null +++ b/src/test/java/seedu/duke/command/GoalCommandTest.java @@ -0,0 +1,166 @@ +/** + * The GoalCommandTest class contains JUnit tests for the GoalCommand class, + * which is responsible for managing goals. + * It tests various scenarios such as adding, removing, and handling exceptions related to goals. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import seedu.duke.classes.Goal; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +class GoalCommandTest { + + /** + * Clears the StateManager after each test to ensure a clean slate for the next test. + */ + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Tests handling of an invalid goal command. + * + * @throws DukeException if the test fails. + */ + @Test + void invalidGoal() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "goal"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests handling of an empty goal addition. + * + * @throws DukeException if the test fails. + */ + @Test + void emptyGoalAdd() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "goal /add "; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests handling of missing amount during goal addition. + * + * @throws DukeException if the test fails. + */ + @Test + void missingAmount() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "goal /add abc /amount "; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests handling of an invalid amount during goal addition. + * + * @throws DukeException if the test fails. + */ + @Test + void invalidAmount() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "goal /add ps5 /amount $500"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests successful addition of a valid goal. + * + * @throws DukeException if the test fails. + */ + @Test + void validGoal() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "goal /add test /amount 500"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + command.execute(ui); + assertEquals("Successfully added test!\n", outputStream.toString()); + } + + /** + * Tests handling of an invalid goal removal command. + * + * @throws DukeException if the test fails. + */ + @Test + void invalidRemoveGoal() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "goal /remove goaltofail"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests successful removal of a valid goal. + * + * @throws DukeException if the test fails. + */ + @Test + void validRemoveGoal() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + StateManager.getStateManager().addGoal(new Goal("test", 500)); + String userInput = "goal /remove test"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + GoalCommand command = new GoalCommand(commandWord, args); + command.execute(ui); + assertEquals("Successfully removed test!\n", outputStream.toString()); + } + +} diff --git a/src/test/java/seedu/duke/command/HelpCommandTest.java b/src/test/java/seedu/duke/command/HelpCommandTest.java new file mode 100644 index 0000000000..29f83bfefc --- /dev/null +++ b/src/test/java/seedu/duke/command/HelpCommandTest.java @@ -0,0 +1,357 @@ +/** + * The HelpCommandTest class contains JUnit tests for the HelpCommand class, + * which is responsible for displaying information about various commands. + * It tests different scenarios, including displaying general help, command-specific help, + * and handling invalid commands. + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.Test; + +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HelpCommandTest { + + /** + * Test if the full list is printed correctly. + * @throws DukeException if the command does not execute. + */ + @Test + void helpCommand_printFullList() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nCommand Description\n" + + "help Shows a list of all the commands available to the user\n" + + "in Adds an income towards goal\n" + + "out Adds an expense for a category\n" + + "delete Delete a specific transaction based on the index in the list\n" + + "list Shows a list of all added transactions based on type\n" + + "category Create or delete a spending category\n" + + "goal Add or remove goals\n" + + "export Exports the transactions stored into a CSV File. " + + "By Default, it will export ALL transactions\n" + + "edit Edits an existing transaction\n" + + "summary Shows the summarised total of transactions\n" + + "bye Exits the program\n\n", outputStream.toString()); + } + + /** + * Test if it will print the full list with empty space. + * @throws DukeException if the command does not execute. + */ + @Test + void helpCommand_withEmptyCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help "; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nCommand Description\n" + + "help Shows a list of all the commands available to the user\n" + + "in Adds an income towards goal\n" + + "out Adds an expense for a category\n" + + "delete Delete a specific transaction based on the index in the list\n" + + "list Shows a list of all added transactions based on type\n" + + "category Create or delete a spending category\n" + + "goal Add or remove goals\n" + + "export Exports the transactions stored into a CSV File. " + + "By Default, it will export ALL transactions\n" + + "edit Edits an existing transaction\n" + + "summary Shows the summarised total of transactions\n" + + "bye Exits the program\n\n", outputStream.toString()); + } + + /** + * Test if it will print out error message if a invalid command is entered. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withInvalidCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help asdasds"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nNO SUCH COMMAND\n\n", outputStream.toString()); + } + + /** + * Test if the help for in command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidInCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: in DESCRIPTION /amount AMOUNT [/goal GOAL]" + + " [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]\n" + + "Option Description\n" + + "/amount Amount to be added\n" + + "/goal The goal to classify it under\n" + + "/date Date of the transaction\n" + + "/recurrence Indicates whether the income added is recurring\n\n", outputStream.toString()); + } + + /** + * Test if the command enter is case-insensitive will work as intended. + * @throws DukeException if the command does not execute. + */ + @Test + void helpCommand_commandCaseSensitive() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help In"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: in DESCRIPTION /amount AMOUNT [/goal GOAL]" + + " [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]\n" + + "Option Description\n" + + "/amount Amount to be added\n" + + "/goal The goal to classify it under\n" + + "/date Date of the transaction\n" + + "/recurrence Indicates whether the income added is recurring\n\n", outputStream.toString()); + } + + /** + * Test if the command enter is in uppercase will work as intended. + * @throws DukeException if the command does not execute. + */ + @Test + void helpCommand_commandAllUpperCase() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help IN"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: in DESCRIPTION /amount AMOUNT [/goal GOAL]" + + " [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]\n" + + "Option Description\n" + + "/amount Amount to be added\n" + + "/goal The goal to classify it under\n" + + "/date Date of the transaction\n" + + "/recurrence Indicates whether the income added is recurring\n\n", outputStream.toString()); + } + + /** + * Test if the help for out command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidOutCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help out"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: out DESCRIPTION /amount AMOUNT [/category CATEGORY]" + + " [/date DATE in DDMMYYYY] [/recurrence RECURRENCE]\n" + + "Option Description\n" + + "/amount Amount to be deducted\n" + + "/category The spending category to classify it under\n" + + "/date Date of the transaction\n" + + "/recurrence Indicates whether the expense added is recurring\n\n", outputStream.toString()); + } + + /** + * Test if the help for delete command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidDeleteCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help delete"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: delete INDEX /type (in | out)\n" + + "Option Description\n" + + "/type To set whether it is a in or out transaction\n\n", outputStream.toString()); + } + + /** + * Test if the help for list command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidListCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help list"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: list (goal | category)\n" + + "Usage: list /type (in | out) [/goal GOAL] [/category CATEGORY] [/week] [/month]\n" + + "Option Description\n" + + "/type To set whether to display \"in\" or \"out\" transactions\n" + + "/goal The goal which it is classified under\n" + + "/category The spending category which it is classified under\n" + + "/week To filter the transactions to those in the current week\n" + + "/month To filter the transactions to those in the current month\n\n" + , outputStream.toString()); + } + + /** + * Test if the help for help command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidHelpCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help help"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: help\n\n", outputStream.toString()); + } + + /** + * Test if the help for bye command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidByeCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help bye"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: bye\n\n", outputStream.toString()); + } + + /** + * Test if the help for goal command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidGoalCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help goal"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: goal /add NAME /amount AMOUNT\n" + + "Usage: goal /remove NAME\n" + + "Option Description\n" + + "/add Name of goal to be added\n" + + "/amount The amount set for the goal\n" + + "/remove Name of goal to be removed\n\n", outputStream.toString()); + } + + /** + * Test if the help for category command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidCategoryCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help category"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: category /add NAME\n" + + "Usage: category /remove NAME\n" + + "Option Description\n" + + "/add Name of spending category to be created\n" + + "/remove Name of spending category to be deleted\n\n", outputStream.toString()); + } + + /** + * Test if the help for edit command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidEditCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help edit"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: edit INDEX /type (in | out) (/description DESCRIPTION" + + " | /amount AMOUNT | /goal GOAL | /category CATEGORY)\n" + + "Option Description\n" + + "/type To specify either in or out transaction to be edited\n"+ + "/description New description to be specified\n" + + "/amount New amount to be specified\n" + + "/goal New goal to be specified\n" + + "/category New category to be specified\n\n", outputStream.toString()); + } + + /** + * Test if the help for summary command work as intended. + * @throws DukeException if the command cannot be executed. + */ + @Test + void helpCommand_withValidSummaryCommand() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "help summary"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + HelpCommand command = new HelpCommand(commandWord, args); + command.execute(ui); + assertEquals("\nUsage: summary /type (in | out) [/day] [/week] [/month]\n" + + "Option Description\n" + + "/type To specific either in or out transaction to be listed\n"+ + "/day To filter transactions to those of current day\n" + + "/week To filter the transactions to those in the current week\n" + + "/month To filter the transactions to " + + "those in the current month\n\n", outputStream.toString()); + } +} diff --git a/src/test/java/seedu/duke/command/ListCommandTest.java b/src/test/java/seedu/duke/command/ListCommandTest.java new file mode 100644 index 0000000000..8d758db14e --- /dev/null +++ b/src/test/java/seedu/duke/command/ListCommandTest.java @@ -0,0 +1,540 @@ +/** + * The ListCommandTest class contains JUnit tests for the ListCommand class, + * which is responsible for displaying lists of transactions based on specified criteria. + * It tests various scenarios, including invalid input, filtering by type, goal, and category, + * and listing transactions for specific time periods (week and month). + */ + +package seedu.duke.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ListCommandTest { + private static Parser parser = new Parser(); + private static ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private static Ui ui = new Ui(outputStream); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("ddMMyyyy"); + + /** + * Clears the state manager after each test to ensure a clean state for the next test. + */ + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Tests the scenario where an invalid list command is provided, and a DukeException is expected. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void invalidList() throws DukeException { + String userInput = "list"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ListCommand command = new ListCommand(commandWord, args); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Tests the scenario where an invalid list type is provided, and a DukeException is expected. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void emptyListDescription() throws DukeException { + String userInput = "list "; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Tests the scenario where an invalid goal is provided for the list command, + * and a DukeException is expected. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void invalidListDescription_argumentExists() throws DukeException { + String userInput = "list goal /type in"; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + @Test + void invalidArgument() throws DukeException{ + String userInput = "list /goal ABC"; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + @Test + void invalidListType() throws DukeException { + String userInput = "list /type ABC"; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + @Test + void blankGoal() throws DukeException { + String userInput = "list /type in /goal "; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + @Test + void invalidGoal() throws DukeException { + String userInput = "list /type in /goal ABC"; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + @Test + void blankCategory() throws DukeException { + String userInput = "list /type out /category "; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + @Test + void invalidCategory() throws DukeException { + String userInput = "list /type out /category DEF"; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Tests the scenario where both an invalid goal and category are provided for the list command, + * and a DukeException is expected. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void invalidCategoryGoal() throws DukeException { + String userInput = "list /type in /goal ABC /category DEF"; + Command command = parser.parse(userInput); + assertThrows(DukeException.class, () -> command.execute(ui)); + } + + /** + * Adds sample income entries for testing list commands with goals. + */ + private static void addInEntries() { + try { + parser.parse("goal /add car /amount 5000").execute(ui); + parser.parse("goal /add PS5 /amount 300").execute(ui); + parser.parse("in part-time job /amount 500 /goal car").execute(ui); + parser.parse("in red packet money /amount 50 /goal PS5 /date 18092023").execute(ui); + outputStream.reset(); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + + } + + /** + * Adds sample expense entries for testing list commands with categories. + */ + private static void addOutEntries() { + try { + parser.parse("out dinner /amount 10.50 /category food").execute(ui); + parser.parse("out pokemon card pack /amount 10.50 /category games /date 18092023").execute(ui); + outputStream.reset(); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + + } + + /** + * Adds sample income entries with dates for testing list commands with date filtering. + */ + private static void addInEntriesWithDates() { + try { + parser.parse("goal /add car /amount 5000").execute(ui); + parser.parse("in part-time job /amount 500 /goal car /date " + + getFormattedCurrentDate()).execute(ui); + parser.parse("in allowance job /amount 300 /goal car /date " + + getFormattedPrevWeekDate()).execute(ui); + parser.parse("in red packet money /amount 150 /goal car /date " + + getFormattedPrevMonthDate()).execute(ui); + outputStream.reset(); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Adds sample expense entries with dates for testing list commands with date filtering. + */ + private static void addOutEntriesWithDates() { + try { + parser.parse("out lunch /amount 7.50 /category food /date " + + getFormattedCurrentDate()).execute(ui); + parser.parse("out dinner /amount 10.50 /category food /date " + + getFormattedPrevWeekDate()).execute(ui); + parser.parse("out pokemon card pack /amount 10.50 /category games /date " + + getFormattedPrevMonthDate()).execute(ui); + outputStream.reset(); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Retrieves the formatted current date as a string. + * + * @return The formatted current date. + */ + private static String getFormattedCurrentDate() { + LocalDate currentDate = LocalDate.now(); + return currentDate.format(DATE_FORMATTER); + } + + /** + * Retrieves the formatted date for the previous week as a string. + * + * @return The formatted date for the previous week. + */ + private static String getFormattedPrevWeekDate() { + LocalDate currentDate = LocalDate.now(); + LocalDate prevWeek = currentDate.minusDays(7); + return prevWeek.format(DATE_FORMATTER); + } + + /** + * Retrieves the formatted date for the previous month as a string. + * + * @return The formatted date for the previous month. + */ + private static String getFormattedPrevMonthDate() { + LocalDate currentDate = LocalDate.now(); + LocalDate prevMonth = currentDate.minusMonths(1); + return prevMonth.format(DATE_FORMATTER); + } + + /** + * Retrieves the current date as a LocalDate object. + * + * @return The current date. + */ + private static LocalDate getCurrentDate() { + LocalDate currentDate = LocalDate.now(); + return currentDate; + } + + /** + * Retrieves the date for the previous week as a LocalDate object. + * + * @return The date for the previous week. + */ + private static LocalDate getPrevWeekDate() { + LocalDate currentDate = LocalDate.now(); + LocalDate prevWeek = currentDate.minusDays(7); + return prevWeek; + } + + /** + * Checks if two LocalDate objects fall in the same month. + * + * @param date1 The first LocalDate object. + * @param date2 The second LocalDate object. + * @return True if the two dates are in the same month, false otherwise. + */ + public static boolean isInSameMonth(LocalDate date1, LocalDate date2) { + return date1.getYear() == date2.getYear() && date1.getMonthValue() == date2.getMonthValue(); + } + + /** + * Tests the scenario where valid income transactions are listed, and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void validInList() throws DukeException { + addInEntries(); + LocalDate currentDate = LocalDate.now(); + Command command = parser.parse("list /type in"); + command.execute(ui); + assertEquals("Alright! Displaying 2 transactions.\n" + + "=========================================== IN TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Goal " + + "Recurrence\n" + + "1 part-time job "+currentDate+" 500.00 car " + + " " + + "none\n" + + "2 red packet money 2023-09-18 50.00 PS5 " + + "none\n" + + "=========================================== IN TRANSACTIONS =============================" + + "==============\n" + , outputStream.toString()); + + } + + /** + * Tests the scenario where filtered valid income transactions are listed, and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void validFilteredInList() throws DukeException { + addInEntries(); + LocalDate currentDate = LocalDate.now(); + Command command = parser.parse("list /type in /goal car"); + command.execute(ui); + assertEquals("Alright! Displaying 1 transaction.\n" + + "=========================================== IN TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Goal " + + "Recurrence\n" + + "1 part-time job "+currentDate+" 500.00 car "+ + " " + + "none\n" + + "=========================================== IN TRANSACTIONS ============================" + + "===============\n" + , outputStream.toString()); + + } + + /** + * Tests the scenario where valid expense transactions are listed, and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void validOutList() throws DukeException { + addOutEntries(); + LocalDate currentDate = LocalDate.now(); + Command command = parser.parse("list /type out"); + command.execute(ui); + assertEquals("Alright! Displaying 2 transactions.\n" + + "========================================== OUT TRANSACTIONS ===========================" + + "================\n" + + "ID Description Date Amount Category " + + "Recurrence\n" + + "1 dinner "+currentDate+" 10.50 food " + + " " + + "none\n" + + "2 pokemon card pack 2023-09-18 10.50 games " + + "none\n" + + "========================================== OUT TRANSACTIONS ============================" + + "===============\n" + , outputStream.toString()); + + } + + /** + * Tests the scenario where filtered valid expense transactions are listed, and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void validFilteredOutList() throws DukeException { + addOutEntries(); + Command command = parser.parse("list /type out /category games"); + command.execute(ui); + assertEquals("Alright! Displaying 1 transaction.\n" + + "========================================== OUT TRANSACTIONS ==========================" + + "=================\n" + + "ID Description Date Amount Category " + + "Recurrence\n" + + "1 pokemon card pack 2023-09-18 10.50 games " + + "none\n" + + "========================================== OUT TRANSACTIONS ============================" + + "===============\n" + , outputStream.toString()); + + } + + /** + * Tests the scenario where income transactions for the current week are listed, and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void execute_listIncomeByWeek_printCurrentWeekTransactions() throws DukeException { + addInEntriesWithDates(); + Command command = parser.parse("list /type in /week"); + command.execute(ui); + assertEquals("Alright! Displaying 1 transaction.\n" + + "=========================================== IN TRANSACTIONS ==========================" + + "=================\n" + + "ID Description Date Amount Goal " + + "Recurrence\n" + + "1 part-time job "+getCurrentDate()+" 500.00 car " + + " " + + "none\n" + + "=========================================== IN TRANSACTIONS ===========================" + + "================\n" + , outputStream.toString()); + } + + @Test + void execute_listExpenseByWeek_printCurrentWeekTransactions() throws DukeException { + addOutEntriesWithDates(); + Command command = parser.parse("list /type out /week"); + command.execute(ui); + assertEquals("Alright! Displaying 1 transaction.\n" + + "========================================== OUT TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Category " + + "Recurrence\n" + + "1 lunch "+getCurrentDate()+" 7.50 food " + + " " + + "none\n" + + "========================================== OUT TRANSACTIONS ============================" + + "===============\n" + , outputStream.toString()); + } + + @Test + void execute_listIncomeByMonth_printCurrentMonthTransactions() throws DukeException { + addInEntriesWithDates(); + Command command = parser.parse("list /type in /month"); + command.execute(ui); + if (isInSameMonth(getCurrentDate(), getPrevWeekDate())) { // when current and previous week is in same month + assertEquals("Alright! Displaying 2 transactions.\n" + + "=========================================== IN TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Goal " + + "Recurrence\n" + + "1 part-time job "+getCurrentDate()+" 500.00 car " + + " none\n" + + "2 allowance job "+getPrevWeekDate()+" 300.00 car " + + " none\n" + + "=========================================== IN TRANSACTIONS ===========================" + + "================\n" + , outputStream.toString()); + } else { // when current and previous week date is in different month + assertEquals("Alright! Displaying 1 transaction.\n" + + "=========================================== IN TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Goal " + + "Recurrence\n" + + "1 part-time job "+getCurrentDate()+" 500.00 car " + + " none\n" + + "=========================================== IN TRANSACTIONS ===========================" + + "================\n" + , outputStream.toString()); + } + } + + /** + * Tests the scenario where expense transactions for the current month are listed, and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void execute_listExpenseByMonth_printCurrentMonthTransactions() throws DukeException { + addOutEntriesWithDates(); + Command command = parser.parse("list /type out /month"); + command.execute(ui); + if (isInSameMonth(getCurrentDate(), getPrevWeekDate())) { // when current and previous week is in same month + assertEquals("Alright! Displaying 2 transactions.\n" + + "========================================== OUT TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Category " + + "Recurrence\n" + + "1 lunch "+getCurrentDate()+" 7.50 food " + + " " + + "none\n" + + "2 dinner "+getPrevWeekDate()+" 10.50 food " + + " none\n" + + "========================================== OUT TRANSACTIONS ============================" + + "===============\n" + , outputStream.toString()); + } else { // when current and previous week date is in different month + assertEquals("Alright! Displaying 1 transaction.\n" + + "========================================== OUT TRANSACTIONS ===========================" + + "================\n" + + "ID Description Date Amount Category " + + "Recurrence\n" + + "1 lunch "+getCurrentDate()+" 7.50 food " + + " none\n" + + "========================================== OUT TRANSACTIONS ===========================" + + "================\n" + , outputStream.toString()); + } + } + + /** + * Tests the scenario where income transactions for the current week and month are listed, + * and the output is verified. + * + * @throws DukeException If an error occurs while executing the command. + */ + @Test + void execute_listIncomeByWeekAndMonth_printCurrentWeekTransactions() throws DukeException { + addInEntriesWithDates(); + Command command = parser.parse("list /type in /week /month"); + command.execute(ui); + assertEquals("Alright! Displaying 1 transaction.\n" + + "=========================================== IN TRANSACTIONS ============================" + + "===============\n" + + "ID Description Date Amount Goal " + + "Recurrence\n" + + "1 part-time job "+getCurrentDate()+" 500.00 car " + + " none\n" + + "=========================================== IN TRANSACTIONS ===========================" + + "================\n" + , outputStream.toString()); + } + + @Test + void execute_listExpenseByWeekAndMonth_printCurrentWeekTransactions() throws DukeException { + addOutEntriesWithDates(); + Command command = parser.parse("list /type out /week /month"); + command.execute(ui); + assertEquals("Alright! Displaying 1 transaction.\n" + + "========================================== OUT TRANSACTIONS ===========================" + + "================\n" + + "ID Description Date Amount Category " + + "Recurrence\n" + + "1 lunch "+getCurrentDate()+" 7.50 food " + + " none\n" + + "========================================== OUT TRANSACTIONS ===========================" + + "================\n" + , outputStream.toString()); + } + + @Test + void execute_listGoalStatus() throws DukeException { + addInEntries(); + Command command = parser.parse("list goal"); + command.execute(ui); + assertEquals("==================================== Goals Status ======================" + + "==============\n" + + "Name Amount Progress\n" + + "PS5 50.00/300.00 [=== ] 16.67%\n" + + "car 500.00/5000.00 [== ] 10.00%\n" + + "==================================== Goals Status ====================================\n" + , outputStream.toString()); + } + + @Test + void execute_listCategoryStatus() throws DukeException { + addOutEntries(); + Command command = parser.parse("list category"); + command.execute(ui); + assertEquals("============== Categories Status ===============\n" + + "Name Amount\n" + + "food 10.50\n" + + "games 10.50\n" + + "============== Categories Status ===============\n", outputStream.toString()); + } + +} diff --git a/src/test/java/seedu/duke/command/RemoveTransactionCommandTest.java b/src/test/java/seedu/duke/command/RemoveTransactionCommandTest.java new file mode 100644 index 0000000000..c5d2eb8743 --- /dev/null +++ b/src/test/java/seedu/duke/command/RemoveTransactionCommandTest.java @@ -0,0 +1,120 @@ +package seedu.duke.command; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RemoveTransactionCommandTest { + + private static Parser parser = new Parser(); + private static ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private static Ui ui = new Ui(outputStream); + + @BeforeEach + void populateStateManager() { + try { + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + + parser.parse("in part-time job /amount 1000 /goal car").execute(ui); + parser.parse("in allowance /amount 500 /goal car").execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5").execute(ui); + + parser.parse("out buy dinner /amount 15 /category food").execute(ui); + parser.parse("out popmart /amount 12 /category toy").execute(ui); + parser.parse("out grab /amount 20 /category transport").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + + } + + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + @Test + void execute_missingIdx_exceptionThrown() throws DukeException { + Command command = parser.parse("delete /type in"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + @Test + void execute_missingTypeArgument_exceptionThrown() throws DukeException { + Command command = parser.parse("delete 1"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + @Test + void execute_missingTypeValue_exceptionThrown() throws DukeException { + Command command = parser.parse("delete 1 /type"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Test to ensure that Exception will be thrown when the index + * is invalid. + */ + @Test + void execute_negativeIdx_exceptionThrown() throws DukeException { + Command command = parser.parse("delete -1 /type in"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Test to ensure that Exception will be thrown when the index + * is out or range. + */ + @Test + void execute_outOfRangeIdx_exceptionThrown() throws DukeException { + Command command = parser.parse("delete 1000 /type in"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + /** + * Test to ensure that income transaction is removed from the + * StateManager when the index is valid. + */ + @Test + void execute_validIncomeIdx_removedFromStateManager() throws DukeException { + Command command = parser.parse("delete 1 /type in"); + command.execute(ui); + String transactionDescription = StateManager.getStateManager().getIncome(0) // 0-based indexing + .getTransaction().getDescription(); + assertNotEquals("part-time-job", transactionDescription); + } + + /** + * Test to ensure that expense transaction is removed from the + * StateManager when the index is valid. + */ + @Test + void execute_validExpenseIdx_removedFromStateManager() throws DukeException { + Command command = parser.parse("delete 2 /type out"); + command.execute(ui); + String transactionDescription = StateManager.getStateManager().getExpense(1) // 0-based indexing + .getTransaction().getDescription(); + assertNotEquals("popmart", transactionDescription); + } + +} diff --git a/src/test/java/seedu/duke/command/SummaryCommandTest.java b/src/test/java/seedu/duke/command/SummaryCommandTest.java new file mode 100644 index 0000000000..861d6b383c --- /dev/null +++ b/src/test/java/seedu/duke/command/SummaryCommandTest.java @@ -0,0 +1,261 @@ +package seedu.duke.command; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.AfterEach; +import seedu.duke.classes.StateManager; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.time.LocalDate; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SummaryCommandTest { + + + @AfterEach + void clearStateManager() { + StateManager.clearStateManager(); + } + + /** + * Populates the StateManager with test income transactions. + */ + private static void addInEntriesWithDates() { + Parser parser = new Parser(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + try { + parser.parse("goal /add car /amount 5000").execute(ui); + parser.parse("in part-time job /amount 500 /goal car /date 30102023").execute(ui); + parser.parse("in carousell /amount 10 /goal car /date 31102023").execute(ui); + parser.parse("in allowance /amount 300 /goal car /date 23102023").execute(ui); + parser.parse("in red packet money /amount 150 /goal car /date 23092023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Populates the StateManager with test expense transactions. + */ + private static void addOutEntriesWithDates() { + Parser parser = new Parser(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + try { + parser.parse("out lunch /amount 7.50 /category food /date 30102023").execute(ui); + parser.parse("out grocery /amount 20.80 /category food /date 31102023").execute(ui); + parser.parse("out dinner /amount 10.50 /category food /date 23102023").execute(ui); + parser.parse("out pokemon card pack /amount 10.50 /category games /date 18092023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + + } + + /** + * Test to ensure that the summary command will throw + * Exception when called without /type. + */ + @Test + void execute_summaryWithoutType_throwsDukeException() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + Command command = parser.parse("summary"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + @Test + void execute_summaryEmptyType_throwsDukeException() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + Command command = parser.parse("summary /type"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + @Test + void execute_summaryInvalidType_throwsDukeException() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + Command command = parser.parse("summary /type invalid"); + assertThrows(DukeException.class, () -> { + command.execute(ui); + }); + } + + @Test + void execute_incomeSummary_printTotalIncome() throws DukeException { + addInEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + Command command = parser.parse("summary /type in"); + command.execute(ui); + assertEquals("Good job! Total income so far: $960.00\n", outputStream.toString()); + } + + @Test + void execute_incomeSummaryByDay_printTotalIncomeByDay() throws DukeException { + addInEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "in"); + put("day", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Good job! Total income so far for Today: $10.00\n", outputStream.toString()); + } + + @Test + void execute_incomeSummaryByWeek_printTotalIncomeByWeek() throws DukeException { + addInEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "in"); + put("week", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Good job! Total income so far for This Week: $510.00\n", outputStream.toString()); + } + + @Test + void execute_incomeSummaryByMonth_printTotalIncomeByMonth() throws DukeException { + addInEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "in"); + put("month", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Good job! Total income so far for This Month: $810.00\n", outputStream.toString()); + } + + /** + * Test to ensure that if multiple filters are indicated, + * filter by day will take priority. + */ + @Test + void execute_incomeSummaryByDayWeekMonth_printTotalIncomeByDay() throws DukeException { + addInEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "in"); + put("day", ""); + put("week", ""); + put("month", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Good job! Total income so far for Today: $10.00\n", outputStream.toString()); + } + + @Test + void execute_expenseSummary_printTotalExpense() throws DukeException { + addOutEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + Command command = parser.parse("summary /type out"); + command.execute(ui); + assertEquals("Wise spending! Total expense so far: $49.30\n", outputStream.toString()); + } + + @Test + void execute_expenseSummaryByDay_printTotalExpenseByDay() throws DukeException { + addOutEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "out"); + put("day", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Wise spending! Total expense so far for Today: $20.80\n", outputStream.toString()); + } + + @Test + void execute_expenseSummaryByWeek_printTotalExpenseByWeek() throws DukeException { + addOutEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "out"); + put("week", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Wise spending! Total expense so far for This Week: $28.30\n", outputStream.toString()); + } + + @Test + void execute_expenseSummaryByMonth_printTotalExpenseByMonth() throws DukeException { + addOutEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "out"); + put("month", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Wise spending! Total expense so far for This Month: $38.80\n", outputStream.toString()); + } + + /** + * Test to ensure that if multiple filters are indicated, + * filter by day will take priority. + */ + @Test + void execute_expenseSummaryByDayWeekMonth_printTotalExpenseByDay() throws DukeException { + addOutEntriesWithDates(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + LocalDate currentDate = LocalDate.of(2023, 10, 31); + String description = ""; + HashMap argMaps = new HashMap<>() {{ + put("type", "out"); + put("day", ""); + put("week", ""); + put("month", ""); + }}; + Command command = new SummaryCommand(description, argMaps, currentDate); + command.execute(ui); + assertEquals("Wise spending! Total expense so far for Today: $20.80\n", outputStream.toString()); + } + +} diff --git a/src/test/java/seedu/duke/parser/ParserTest.java b/src/test/java/seedu/duke/parser/ParserTest.java new file mode 100644 index 0000000000..b41b25c25d --- /dev/null +++ b/src/test/java/seedu/duke/parser/ParserTest.java @@ -0,0 +1,162 @@ +package seedu.duke.parser; + +import org.junit.jupiter.api.Test; +import seedu.duke.command.AddIncomeCommand; +import seedu.duke.command.Command; +import seedu.duke.exception.DukeException; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ParserTest { + + private static final String EMPTY_STRING = ""; + + @Test + void parse_validIncome_incomeCommand() throws DukeException { + Parser parser = new Parser(); + parser.parse("goal /add car /amount 5000"); + Command command = parser.parse("in job /amount 100 /goal car"); + assertEquals(AddIncomeCommand.class, command.getClass()); + } + + @Test + void parse_invalidCommand_exceptionThrown() { + Parser parser = new Parser(); + assertThrows(DukeException.class, () -> { + Command command = parser.parse("invalid command"); + }); + } + + @Test + void getCommandWord_validCommand_commandInLowerCase() { + Parser parser = new Parser(); + assertEquals("delete", parser.getCommandWord("Delete 1 /type in")); + } + + @Test + void getDescription_validDescription_trimmedDescription() { + Parser parser = new Parser(); + assertEquals("part time job", + parser.getDescription("in part time job /amount 100 /goal car")); + } + + @Test + void getDescription_emptyDescription_emptyString() { + Parser parser = new Parser(); + assertEquals("", + parser.getDescription("in")); + } + + @Test + void getDescription_emptyDescriptionWithArguments_emptyString() { + Parser parser = new Parser(); + assertEquals(EMPTY_STRING, + parser.getDescription("in /amount 100 /goal car")); + } + + /** + * Test to verify that the getArguments method + * will parse the argument and values correctly. + */ + @Test + void getArguments_validArguments_hashmapOfArguments() throws DukeException { + Parser parser = new Parser(); + HashMap args = parser.getArguments("in part time job /amount 100 /goal car"); + assertEquals("100", args.get("amount")); + assertEquals("car", args.get("goal")); + } + + @Test + void getArguments_noArgument_emptyHashMap() throws DukeException { + Parser parser = new Parser(); + HashMap args = parser.getArguments("in part time job"); + assertEquals(0, args.size()); + } + + @Test + void getArguments_argumentWithoutValue_emptyString() throws DukeException { + Parser parser = new Parser(); + HashMap args = parser.getArguments("in part time job /amount /goal"); + assertEquals(EMPTY_STRING, args.get("amount")); + assertEquals(EMPTY_STRING, args.get("goal")); + } + + @Test + void getArguments_firstArgumentWithEmptyString_emptyString() throws DukeException { + Parser parser = new Parser(); + HashMap args = parser.getArguments("in part time job /amount /goal"); + assertEquals(EMPTY_STRING, args.get("amount")); + assertEquals(EMPTY_STRING, args.get("goal")); + } + + @Test + void getArguments_secondArgumentWithEmptyString_emptyString() throws DukeException { + Parser parser = new Parser(); + HashMap args = parser.getArguments("in part time job /amount /goal "); + assertEquals(EMPTY_STRING, args.get("amount")); + assertEquals(EMPTY_STRING, args.get("goal")); + } + + @Test + void parseDoublePositive() { + assertEquals(Parser.parseNonNegativeDouble("18"), 18); + assertEquals(Parser.parseNonNegativeDouble("18."), 18); + assertEquals(Parser.parseNonNegativeDouble("0.5"), 0.5); + assertEquals(Parser.parseNonNegativeDouble(".5"), 0.5); + assertEquals(Parser.parseNonNegativeDouble("18.5"), 18.5); + assertEquals(Parser.parseNonNegativeDouble("9999999.99"), 9_999_999.99); + } + + @Test + void parseDoublePositiveZero() { + assertEquals(Parser.parseNonNegativeDouble("0"), 0); + assertEquals(Parser.parseNonNegativeDouble("0.00"), 0); + } + + @Test + void parseDoubleNegativeZero() { + assertNull(Parser.parseNonNegativeDouble("-0")); + } + + @Test + void parseNegativeDouble() { + assertNull(Parser.parseNonNegativeDouble("-18.5")); + assertNull(Parser.parseNonNegativeDouble("-18.")); + assertNull(Parser.parseNonNegativeDouble("-.5")); + assertNull(Parser.parseNonNegativeDouble("0.000")); + assertNull(Parser.parseNonNegativeDouble("10000000")); + } + + /** + * Test to verify that the getArguments method + * throws an Exception when duplicate arguments are provided. + */ + @Test + void getArguments_duplicateArgsWithValues_throwsDukeException() { + Parser parser = new Parser(); + assertThrows(DukeException.class, () -> { + parser.getArguments("in part time job /amount 100 /goal car /goal house"); + }); + } + + + @Test + void getArguments_duplicateArgsWithOneEmptyValue_throwsDukeException() { + Parser parser = new Parser(); + assertThrows(DukeException.class, () -> { + parser.getArguments("in part time job /amount 100 /goal car /goal"); + }); + } + + @Test + void getArguments_duplicateArgsWithBothEmptyValue_throwsDukeException() { + Parser parser = new Parser(); + assertThrows(DukeException.class, () -> { + parser.getArguments("in part time job /amount 100 /goal /goal"); + }); + } +} diff --git a/src/test/java/seedu/duke/storage/StorageTest.java b/src/test/java/seedu/duke/storage/StorageTest.java new file mode 100644 index 0000000000..2d5364b3a5 --- /dev/null +++ b/src/test/java/seedu/duke/storage/StorageTest.java @@ -0,0 +1,556 @@ +package seedu.duke.storage; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import seedu.duke.classes.StateManager; +import seedu.duke.command.ListCommand; +import seedu.duke.exception.DukeException; +import seedu.duke.parser.Parser; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; + +import org.apache.commons.io.FileUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class StorageTest { + private static final String DATE_PATTERN = "dd/MM/yyyy"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); + private static final String TEST_DIR = "./TestFiles"; + private static final String GOAL_STORAGE_FILENAME = TEST_DIR + "/goal-store.csv"; + private static final String CATEGORY_STORAGE_FILENAME = TEST_DIR + "/category-store.csv"; + private static final String INCOME_STORAGE_FILENAME = TEST_DIR + "/income-store.csv"; + private static final String EXPENSE_STORAGE_FILENAME = TEST_DIR + "/expense-store.csv"; + private static final String EXPORT_STORAGE_FILENAME = TEST_DIR + "/Transactions.csv"; + + private Storage storage; + + /** + * Before each test, initialise the storage object; + */ + @BeforeEach + void initialise() { + File directory = new File(TEST_DIR); + if (!directory.exists()) { + directory.mkdir(); + } + storage = new Storage(GOAL_STORAGE_FILENAME, CATEGORY_STORAGE_FILENAME, INCOME_STORAGE_FILENAME, + EXPENSE_STORAGE_FILENAME, EXPORT_STORAGE_FILENAME); + } + + /** + * Test for validRow function if there is empty value, it will return false. + */ + @Test + void validRowWithEmptyValues() { + String[] row = {"TEST1", ""}; + assertEquals(false, storage.validRow(row)); + row[0] = ""; + row[1] = "TEST"; + assertEquals(false, storage.validRow(row)); + } + + /** + * Test for validRow function if there is blank value, it will return false. + */ + @Test + void validRowWithBlankValues() { + String[] row = {"TEST1", " "}; + assertEquals(false, storage.validRow(row)); + row[0] = " "; + row[1] = "Test1"; + assertEquals(false, storage.validRow(row)); + } + + /** + * Test for validRow function if value is valid, it will return true. + */ + @Test + void validRowWithCorrectValues() { + String[] row = {"TEST1", "TEST2"}; + assertEquals(true, storage.validRow(row)); + } + + /** + * Test for validDate function if value is in wrong format, it will return an error. + */ + @Test + void validDateWithWrongFormat() { + String dateStr = "25-10-2023"; + String testFileName = "filename"; + assertThrows(DukeException.class, () -> { + storage.validDate(dateStr, testFileName); + }); + } + + /** + * Test for validDate function if value is not a date format, it will return an error. + */ + @Test + void validDateWithNotDateString() { + String dateStr = "TEST"; + String testFileName = "filename"; + assertThrows(DukeException.class, () -> { + storage.validDate(dateStr, testFileName); + }); + } + + /** + * Test for validDate function if value is in correct format, it will return the date. + */ + @Test + void validDateWithCorrectDateString() throws DukeException { + String dateStr = "25/10/2023"; + String testFileName = "filename"; + LocalDate date = LocalDate.parse("25/10/2023", FORMATTER); + assertEquals(date, storage.validDate(dateStr, testFileName)); + } + + /** + * Test for validBoolean function if value is a correct boolean string, it will return true. + */ + @Test + void validBooleanWithCorrectBoolString() { + String input = "True"; + assertEquals(true, storage.validBoolean(input)); + input = "TRUE"; + assertEquals(true, storage.validBoolean(input)); + input = "true"; + assertEquals(true, storage.validBoolean(input)); + input = "False"; + assertEquals(true, storage.validBoolean(input)); + input = "FALSE"; + assertEquals(true, storage.validBoolean(input)); + input = "false"; + assertEquals(true, storage.validBoolean(input)); + } + + /** + * Test for validBoolean function if value is a wrong boolean string, it will return false. + */ + @Test + void validBooleanWithWrongBoolString() { + String input = "test"; + assertEquals(false, storage.validBoolean(input)); + } + + /** + * Test if loading of storage file will throw an error if files cannot be found. + */ + @Test + void loadWithNoStorageFile() { + assertThrows(DukeException.class, () -> { + storage.loadIncome(); + }); + assertThrows(DukeException.class, () -> { + storage.loadExpense(); + }); + assertThrows(DukeException.class, () -> { + storage.loadGoal(); + }); + assertThrows(DukeException.class, () -> { + storage.loadCategory(); + }); + } + + @Nested + class WithValidStorage { + + /** + * Before each test, copy file to TestFiles Directory. + * @throws IOException if the files cannot be found. + */ + @BeforeEach + void copyFiles() throws IOException { + File src = new File("./TestCSV/Windows/valid/category-store.csv"); + File dst = new File(CATEGORY_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/valid/goal-store.csv"); + dst = new File(GOAL_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/valid/expense-store.csv"); + dst = new File(EXPENSE_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/valid/income-store.csv"); + dst = new File(INCOME_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + } + + /** + * Restore the state back to the original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(CATEGORY_STORAGE_FILENAME); + file.delete(); + file = new File(GOAL_STORAGE_FILENAME); + file.delete(); + file = new File(EXPENSE_STORAGE_FILENAME); + file.delete(); + file = new File(INCOME_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if the application can load back the information given valid storage files. + * @throws DukeException if the command cannot be executed. + */ + @Test + void loadWithValidStorageFile() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + storage.load(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "list /type in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ListCommand command = new ListCommand(commandWord, args); + command.execute(ui); + userInput = "list /type out"; + args = parser.getArguments(userInput); + commandWord = parser.getDescription(userInput); + command = new ListCommand(commandWord, args); + command.execute(ui); + assertEquals("Alright! Displaying 3 transactions.\n" + + "=========================================== IN TRANSACTIONS =========" + + "==================================\n" + + "ID Description Date Amount Goal" + + " Recurrence\n" + + "1 part-time job 2023-10-29 1000.00 car " + + " none\n" + + "2 allowance 2023-10-29 500.00 car " + + " monthly\n" + + "3 sell stuff 2023-10-29 50.00 ps5 " + + " none\n" + + "=========================================== IN TRANSACTIONS =========" + + "==================================\n" + + "Alright! Displaying 3 transactions.\n" + + "========================================== OUT TRANSACTIONS ==========" + + "=================================\n" + + "ID Description Date Amount Category " + + " Recurrence\n" + + "1 buy dinner 2023-10-29 15.00 food " + + " monthly\n" + + "2 popmart 2023-10-29 12.00 toy " + + " none\n" + + "3 grab 2023-10-29 20.00 transport " + + " none\n" + + "========================================== OUT TRANSACTIONS ==============" + + "=============================\n" + , outputStream.toString()); + + + } + } + + /** + * Test for loading storage files with empty columns + * Tests is split depending on the OS. + */ + @Nested + class WithEmptyColumns { + + /** + * Before each test, copy file to TestFiles Directory. + * @throws IOException if the files cannot be found. + */ + @BeforeEach + void copyFiles() throws IOException { + File src = new File("./TestCSV/Windows/empty/category-store.csv"); + File dst = new File(CATEGORY_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/empty/goal-store.csv"); + dst = new File(GOAL_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/empty/expense-store.csv"); + dst = new File(EXPENSE_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/empty/income-store.csv"); + dst = new File(INCOME_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + } + + /** + * Restore the state back to the original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(CATEGORY_STORAGE_FILENAME); + file.delete(); + file = new File(GOAL_STORAGE_FILENAME); + file.delete(); + file = new File(EXPENSE_STORAGE_FILENAME); + file.delete(); + file = new File(INCOME_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if the application can load back the information given storage files with empty column. + * @throws DukeException if the command cannot be executed. + */ + @Test + void loadWithEmptyColumns() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + storage.load(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "list /type in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ListCommand command = new ListCommand(commandWord, args); + command.execute(ui); + userInput = "list /type out"; + args = parser.getArguments(userInput); + commandWord = parser.getDescription(userInput); + command = new ListCommand(commandWord, args); + command.execute(ui); + assertEquals("Alright! Displaying 3 transactions.\n" + + "=========================================== IN TRANSACTIONS =========" + + "==================================\n" + + "ID Description Date Amount Goal" + + " Recurrence\n" + + "1 part-time job 2023-10-29 1000.00 car " + + " none\n" + + "2 allowance 2023-10-29 500.00 car " + + " monthly\n" + + "3 sell stuff 2023-10-29 50.00 ps5 " + + " none\n" + + "=========================================== IN TRANSACTIONS =========" + + "==================================\n" + + "Alright! Displaying 3 transactions.\n" + + "========================================== OUT TRANSACTIONS ==========" + + "=================================\n" + + "ID Description Date Amount Category " + + " Recurrence\n" + + "1 buy dinner 2023-10-29 15.00 food " + + " daily\n" + + "2 popmart 2023-10-29 12.00 toy " + + " none\n" + + "3 grab 2023-10-29 20.00 transport " + + " none\n" + + "========================================== OUT TRANSACTIONS ==============" + + "=============================\n" + , outputStream.toString()); + } + } + + @Nested + class WithErrorColumns { + + /** + * Before each test, copy file to TestFiles Directory. + * @throws IOException if the files cannot be found. + */ + @BeforeEach + void copyFiles() throws IOException { + File src = new File("./TestCSV/Windows/error/category-store.csv"); + File dst = new File(CATEGORY_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/error/goal-store.csv"); + dst = new File(GOAL_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/error/expense-store.csv"); + dst = new File(EXPENSE_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + src = new File("./TestCSV/Windows/error/income-store.csv"); + dst = new File(INCOME_STORAGE_FILENAME); + Files.copy(src.toPath(), dst.toPath()); + } + + /** + * Restore the state back to the original after each test. + */ + @AfterEach + void clearStateManager() { + File file = new File(CATEGORY_STORAGE_FILENAME); + file.delete(); + file = new File(GOAL_STORAGE_FILENAME); + file.delete(); + file = new File(EXPENSE_STORAGE_FILENAME); + file.delete(); + file = new File(INCOME_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if the application can load back the information given storage files with error columns. + * @throws DukeException if the command cannot be executed. + */ + @Test + void loadWithErrorColumns() throws DukeException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + storage.load(); + Parser parser = new Parser(); + Ui ui = new Ui(outputStream); + String userInput = "list /type in"; + HashMap args = parser.getArguments(userInput); + String commandWord = parser.getDescription(userInput); + ListCommand command = new ListCommand(commandWord, args); + command.execute(ui); + userInput = "list /type out"; + args = parser.getArguments(userInput); + commandWord = parser.getDescription(userInput); + command = new ListCommand(commandWord, args); + command.execute(ui); + assertEquals("Alright! Displaying 3 transactions.\n" + + "=========================================== IN TRANSACTIONS =========" + + "==================================\n" + + "ID Description Date Amount Goal" + + " Recurrence\n" + + "1 part-time job 2023-10-29 1000.00 car " + + " none\n" + + "2 allowance 2023-10-29 500.00 car " + + " monthly\n" + + "3 sell stuff 2023-10-29 50.00 ps5 " + + " none\n" + + "=========================================== IN TRANSACTIONS =========" + + "==================================\n" + + "Alright! Displaying 3 transactions.\n" + + "========================================== OUT TRANSACTIONS ==========" + + "=================================\n" + + "ID Description Date Amount Category " + + " Recurrence\n" + + "1 buy dinner 2023-10-29 15.00 food " + + " daily\n" + + "2 popmart 2023-10-29 12.00 toy " + + " none\n" + + "3 grab 2023-10-29 20.00 transport " + + " none\n" + + "========================================== OUT TRANSACTIONS ==============" + + "=============================\n" + , outputStream.toString()); + } + } + + @Nested + class SaveToFile { + + /** + * Before each test, populate the state manager. + */ + @BeforeEach + void populateStateManager() { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + Parser parser = new Parser(); + parser.parse("goal /add car /amount 1000").execute(ui); + parser.parse("goal /add ps5 /amount 1000").execute(ui); + parser.parse("in part-time job /amount 1000 /goal car /date 29102023").execute(ui); + parser.parse("in allowance /amount 500 /goal car /date 29102023 /recurrence monthly") + .execute(ui); + parser.parse("in sell stuff /amount 50 /goal ps5 /date 29102023").execute(ui); + parser.parse("out buy dinner /amount 15 /category food /date 29102023 /recurrence monthly") + .execute(ui); + parser.parse("out popmart /amount 12 /category toy /date 29102023").execute(ui); + parser.parse("out grab /amount 20 /category transport /date 29102023").execute(ui); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + + /** + * Restore the state back to the original. + */ + @AfterEach + void clearStateManager() { + File file = new File(CATEGORY_STORAGE_FILENAME); + file.delete(); + file = new File(GOAL_STORAGE_FILENAME); + file.delete(); + file = new File(EXPENSE_STORAGE_FILENAME); + file.delete(); + file = new File(INCOME_STORAGE_FILENAME); + file.delete(); + StateManager.clearStateManager(); + } + + /** + * Test if data saved is saved correctly. + * This test is for Windows OS. + * @throws DukeException if command cannot execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.WINDOWS}) + void saveDataWorkingWindows() throws DukeException, IOException { + storage.save(); + File output = new File(CATEGORY_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Windows/valid/category-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(GOAL_STORAGE_FILENAME); + testFile = new File("./TestCSV/Windows/valid/goal-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(INCOME_STORAGE_FILENAME); + testFile = new File("./TestCSV/Windows/valid/income-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(EXPENSE_STORAGE_FILENAME); + testFile = new File("./TestCSV/Windows/valid/expense-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if data saved is saved correctly. + * This test is for MacOS. + * @throws DukeException if command cannot execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.MAC}) + void saveDataWorkingMac() throws DukeException, IOException { + storage.save(); + File output = new File(CATEGORY_STORAGE_FILENAME); + File testFile = new File("./TestCSV/MacOS/valid/category-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(GOAL_STORAGE_FILENAME); + testFile = new File("./TestCSV/MacOS/valid/goal-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(INCOME_STORAGE_FILENAME); + testFile = new File("./TestCSV/MacOS/valid/income-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(EXPENSE_STORAGE_FILENAME); + testFile = new File("./TestCSV/MacOS/valid/expense-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + + /** + * Test if data saved is saved correctly. + * This test is for Linux. + * @throws DukeException if command cannot execute. + * @throws IOException if file cannot be found. + */ + @Test + @EnabledOnOs({OS.LINUX}) + void saveDataWorkingLinux() throws DukeException, IOException { + storage.save(); + File output = new File(CATEGORY_STORAGE_FILENAME); + File testFile = new File("./TestCSV/Linux/valid/category-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(GOAL_STORAGE_FILENAME); + testFile = new File("./TestCSV/Linux/valid/goal-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(INCOME_STORAGE_FILENAME); + testFile = new File("./TestCSV/Linux/valid/income-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + output = new File(EXPENSE_STORAGE_FILENAME); + testFile = new File("./TestCSV/Linux/valid/expense-store.csv"); + assertEquals(true, FileUtils.contentEquals(output, testFile)); + } + } +} diff --git a/src/test/java/seedu/duke/ui/UiTest.java b/src/test/java/seedu/duke/ui/UiTest.java new file mode 100644 index 0000000000..644346fe70 --- /dev/null +++ b/src/test/java/seedu/duke/ui/UiTest.java @@ -0,0 +1,98 @@ +package seedu.duke.ui; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class UiTest { + + @Test + public void printTestPrintTableRows() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String[] headers = new String[]{"Header 1", "Header 2"}; + ArrayList> rows = new ArrayList<>(); + ArrayList row = new ArrayList<>(); + row.add("Hi"); + row.add("Test print"); + rows.add(row); + ui.printTableRows(rows, headers); + assertEquals( + "Header 1 Header 2\n" + + "Hi Test print\n", + outputStream.toString() + ); + } + + @Test + public void printTableWithCustomWidths() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + String[] headers = new String[]{"Header 1", "Header 2"}; + Integer[] widths = new Integer[]{20, 20}; + ArrayList> rows = new ArrayList<>(); + ArrayList row = new ArrayList<>(); + row.add("Hi"); + row.add("Test print"); + rows.add(row); + ui.printTableRows(rows, headers, widths); + assertEquals( + "Header 1 Header 2\n" + + "Hi Test print\n", + outputStream.toString() + ); + } + + @Test + public void printTableNoHeader() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + ArrayList> rows = new ArrayList<>(); + ArrayList row = new ArrayList<>(); + row.add("Hi"); + row.add("Test print"); + rows.add(row); + ui.printTableRows(rows); + assertEquals( + "Hi Test print\n", + outputStream.toString() + ); + } + + @Test + public void printTableNoHeaderWithCustomWidths() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + Integer[] widths = new Integer[]{20, 20}; + ArrayList> rows = new ArrayList<>(); + ArrayList row = new ArrayList<>(); + row.add("Hi"); + row.add("Test print"); + rows.add(row); + ui.printTableRows(rows, null, widths); + assertEquals( + "Hi Test print\n", + outputStream.toString() + ); + } + + @Test + public void printTableNoHeaderWithCustomWidthsSmallerThanDefaultWidths() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Ui ui = new Ui(outputStream); + Integer[] widths = new Integer[]{5, 5}; + ArrayList> rows = new ArrayList<>(); + ArrayList row = new ArrayList<>(); + row.add("Hi"); + row.add("Test print"); + rows.add(row); + ui.printTableRows(rows, null, widths); + assertEquals( + "Hi Te...\n", + outputStream.toString() + ); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..d758dc171f 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,4 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +Welcome to FinText, your personal finance tracker. +> User: Sorry I do not understand your command +> User: +Bye Bye! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..2558e81084 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,2 @@ -James Gosling \ No newline at end of file +invalid command +bye \ No newline at end of file