diff --git a/.gitignore b/.gitignore index 99712178bf..5fc2c0c1b3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,16 @@ # Gradle build files /.gradle/ /build/ -src/main/resources/docs/ +/src/main/resources/docs/ # MacOS custom attributes files created by Finder .DS_Store *.iml -bin/ +/bin/ + +# Duke data files +/data/ +/addressbook.log.0 +/Duke++.log.0 +/Duke++.log.0.lck +/Duke++.log.0.1 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..8965ffa0c2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: java +jdk: oraclejdk11 + +before install: + - chmod +x gradlew + diff --git a/Activity_Diagram_inputHistory.png b/Activity_Diagram_inputHistory.png new file mode 100644 index 0000000000..8e312c9794 Binary files /dev/null and b/Activity_Diagram_inputHistory.png differ diff --git a/Duke++.log.0.1 b/Duke++.log.0.1 new file mode 100644 index 0000000000..69691db6fb --- /dev/null +++ b/Duke++.log.0.1 @@ -0,0 +1,597 @@ +Nov 08, 2019 11:24:30 PM duke.storage.payment.PaymentListStorageManager +INFO: PaymentList.txt file created +Nov 08, 2019 11:24:30 PM duke.Main init +INFO: Initialized the storage +Nov 08, 2019 11:24:30 PM duke.storage.StorageManager loadPaymentList +INFO: start loading paymentList +Nov 08, 2019 11:24:30 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Entered the JsonStorageManager +Nov 08, 2019 11:24:31 PM duke.commons.JsonUtil deserializeObjectFromJsonFile +INFO: Entered the deserializeObjectFromJsonFile method +Nov 08, 2019 11:24:31 PM duke.commons.JsonUtil fromJsonString +INFO: objectMapper starts working. +Nov 08, 2019 11:24:31 PM duke.commons.JsonUtil readJsonFile +INFO: Have read the deserialize object +Nov 08, 2019 11:24:31 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Json file is read +Nov 08, 2019 11:24:31 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: returning the paymentList from storage +Nov 08, 2019 11:24:32 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $6 +Nov 08, 2019 11:24:32 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $6 +Nov 08, 2019 11:24:32 PM duke.storage.StorageManager loadPaymentList +INFO: start loading paymentList +Nov 08, 2019 11:24:32 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Entered the JsonStorageManager +Nov 08, 2019 11:24:32 PM duke.commons.JsonUtil deserializeObjectFromJsonFile +INFO: Entered the deserializeObjectFromJsonFile method +Nov 08, 2019 11:24:32 PM duke.commons.JsonUtil fromJsonString +INFO: objectMapper starts working. +Nov 08, 2019 11:24:32 PM duke.commons.JsonUtil readJsonFile +INFO: Have read the deserialize object +Nov 08, 2019 11:24:32 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Json file is read +Nov 08, 2019 11:24:32 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: returning the paymentList from storage +Nov 08, 2019 11:24:32 PM duke.model.PlanBot sendCompletedMessage +INFO: Completed Plan Bot +Nov 08, 2019 11:24:32 PM duke.Main init +INFO: Initialized the model +Nov 08, 2019 11:24:32 PM duke.Main init +INFO: Initialized the logic +Nov 08, 2019 11:24:32 PM duke.Main init +INFO: Initialized the app +Nov 08, 2019 11:24:33 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:33 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:33 PM duke.ui.ExpensePane +INFO: expenseList has length 4 +Nov 08, 2019 11:24:33 PM duke.ui.ExpensePane +INFO: expenseList has length 4 +Nov 08, 2019 11:24:33 PM duke.ui.ExpensePane +INFO: Items are set. +Nov 08, 2019 11:24:33 PM duke.ui.ExpensePane +INFO: cell factory is set. +Nov 08, 2019 11:24:33 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:33 PM duke.ui.ExpensePane +INFO: Pie chart is set. +Nov 08, 2019 11:24:33 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:33 PM duke.ui.MainWindow fillInnerPart +INFO: The filled externalList length 4 +Nov 08, 2019 11:24:33 PM duke.ui.MainWindow fillInnerPart +INFO: trendingPane is constructed. +Nov 08, 2019 11:24:33 PM duke.ui.PlanPane +INFO: DialogList set +Nov 08, 2019 11:24:33 PM duke.ui.MainWindow fillInnerPart +INFO: planPane is constructed.2 +Nov 08, 2019 11:24:33 PM duke.model.DukePP getIncomeExternalList +INFO: Model sends external income list length 3 +Nov 08, 2019 11:24:33 PM duke.ui.BudgetPane +INFO: incomeList has length 3 +Nov 08, 2019 11:24:33 PM duke.ui.BudgetPane +INFO: Items are set. +Nov 08, 2019 11:24:33 PM duke.ui.BudgetPane +INFO: cell factory is set. +Nov 08, 2019 11:24:34 PM duke.ui.MainWindow fillInnerPart +INFO: Budget plane is constructed. +Nov 08, 2019 11:24:36 PM duke.ui.UiManager start +INFO: MainWindow are showed and filled in. +Nov 08, 2019 11:24:52 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:52 PM duke.ui.ExpensePane +INFO: expenseList has length 4 +Nov 08, 2019 11:24:52 PM duke.ui.ExpensePane +INFO: expenseList has length 4 +Nov 08, 2019 11:24:52 PM duke.ui.ExpensePane +INFO: Items are set. +Nov 08, 2019 11:24:52 PM duke.ui.ExpensePane +INFO: cell factory is set. +Nov 08, 2019 11:24:52 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:52 PM duke.ui.ExpensePane +INFO: Pie chart is set. +Nov 08, 2019 11:24:52 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 4 +Nov 08, 2019 11:24:52 PM duke.ui.MainWindow fillInnerPart +INFO: The filled externalList length 4 +Nov 08, 2019 11:24:52 PM duke.ui.MainWindow fillInnerPart +INFO: trendingPane is constructed. +Nov 08, 2019 11:24:52 PM duke.ui.PlanPane +INFO: DialogList set +Nov 08, 2019 11:24:52 PM duke.ui.MainWindow fillInnerPart +INFO: planPane is constructed.2 +Nov 08, 2019 11:24:52 PM duke.model.DukePP getIncomeExternalList +INFO: Model sends external income list length 3 +Nov 08, 2019 11:24:52 PM duke.ui.BudgetPane +INFO: incomeList has length 3 +Nov 08, 2019 11:24:52 PM duke.ui.BudgetPane +INFO: Items are set. +Nov 08, 2019 11:24:52 PM duke.ui.BudgetPane +INFO: cell factory is set. +Nov 08, 2019 11:24:52 PM duke.ui.MainWindow fillInnerPart +INFO: Budget plane is constructed. +Nov 09, 2019 5:54:01 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 09, 2019 5:54:02 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 09, 2019 5:54:02 PM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 09, 2019 5:54:02 PM duke.model.IncomeList add +INFO: externalList lengths 3 +Nov 09, 2019 5:54:02 PM duke.model.IncomeList add +INFO: externalList lengths 4 +Nov 09, 2019 5:54:02 PM duke.model.IncomeList add +INFO: externalList lengths 5 +Nov 09, 2019 5:54:02 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 10, 2019 6:21:25 PM duke.storage.payment.PaymentListStorageManager +INFO: PaymentList.txt file created +Nov 10, 2019 6:21:26 PM duke.Main init +INFO: Initialized the storage +Nov 10, 2019 6:21:26 PM duke.storage.StorageManager loadPaymentList +INFO: start loading paymentList +Nov 10, 2019 6:21:26 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Entered the JsonStorageManager +Nov 10, 2019 6:21:28 PM duke.commons.JsonUtil deserializeObjectFromJsonFile +INFO: Entered the deserializeObjectFromJsonFile method +Nov 10, 2019 6:21:28 PM duke.commons.JsonUtil fromJsonString +INFO: objectMapper starts working. +Nov 10, 2019 6:21:28 PM duke.commons.JsonUtil readJsonFile +INFO: Have read the deserialize object +Nov 10, 2019 6:21:28 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Json file is read +Nov 10, 2019 6:21:28 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: returning the paymentList from storage +Nov 10, 2019 6:21:28 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $6 +Nov 10, 2019 6:21:28 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $6 +Nov 10, 2019 6:21:28 PM duke.storage.StorageManager loadPaymentList +INFO: start loading paymentList +Nov 10, 2019 6:21:28 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Entered the JsonStorageManager +Nov 10, 2019 6:21:29 PM duke.commons.JsonUtil deserializeObjectFromJsonFile +INFO: Entered the deserializeObjectFromJsonFile method +Nov 10, 2019 6:21:29 PM duke.commons.JsonUtil fromJsonString +INFO: objectMapper starts working. +Nov 10, 2019 6:21:29 PM duke.commons.JsonUtil readJsonFile +INFO: Have read the deserialize object +Nov 10, 2019 6:21:29 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: Json file is read +Nov 10, 2019 6:21:29 PM duke.storage.payment.PaymentListStorageManager readPaymentList +INFO: returning the paymentList from storage +Nov 10, 2019 6:21:29 PM duke.model.PlanQuestionBank +INFO: QuestionBank generated successfully! +Nov 10, 2019 6:21:29 PM duke.model.PlanBot sendCompletedMessage +INFO: Completed Plan Bot +Nov 10, 2019 6:21:29 PM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 10, 2019 6:21:29 PM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 10, 2019 6:21:29 PM duke.Main init +INFO: Initialized the model +Nov 10, 2019 6:21:29 PM duke.Main init +INFO: Initialized the logic +Nov 10, 2019 6:21:29 PM duke.Main init +INFO: Initialized the app +Nov 10, 2019 6:21:30 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 6 +Nov 10, 2019 6:21:30 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 6 +Nov 10, 2019 6:21:30 PM duke.ui.ExpensePane +INFO: expenseList has length 6 +Nov 10, 2019 6:21:30 PM duke.ui.ExpensePane +INFO: expenseList has length 6 +Nov 10, 2019 6:21:30 PM duke.ui.ExpensePane +INFO: Items are set. +Nov 10, 2019 6:21:30 PM duke.ui.ExpensePane +INFO: cell factory is set. +Nov 10, 2019 6:21:30 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 6 +Nov 10, 2019 6:21:31 PM duke.ui.ExpensePane +INFO: Pie chart is set. +Nov 10, 2019 6:21:31 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 6 +Nov 10, 2019 6:21:31 PM duke.ui.MainWindow fillInnerPart +INFO: The filled externalList length 6 +Nov 10, 2019 6:21:31 PM duke.ui.MainWindow fillInnerPart +INFO: trendingPane is constructed. +Nov 10, 2019 6:21:31 PM duke.ui.PlanPane +INFO: DialogList set +Nov 10, 2019 6:21:31 PM duke.ui.MainWindow fillInnerPart +INFO: planPane is constructed.2 +Nov 10, 2019 6:21:31 PM duke.model.DukePP getIncomeExternalList +INFO: Model sends external income list length 1 +Nov 10, 2019 6:21:31 PM duke.ui.BudgetPane +INFO: incomeList has length 1 +Nov 10, 2019 6:21:31 PM duke.ui.BudgetPane +INFO: Items are set. +Nov 10, 2019 6:21:31 PM duke.ui.BudgetPane +INFO: cell factory is set. +Nov 10, 2019 6:21:32 PM duke.ui.MainWindow fillInnerPart +INFO: Budget plane is constructed. +Nov 10, 2019 6:21:32 PM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter completeParameterComplements +INFO: ComplementList for parameter names is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter produceParameterComplements +INFO: ComplementList producing parameter names is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter getPurpose +INFO: The original input itself is already complete. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter getPurpose +INFO: The original input itself is already complete. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 10:44:32 AM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 10:44:32 AM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $1 +Nov 11, 2019 10:44:32 AM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $1 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 2 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 3 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 4 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 5 +Nov 11, 2019 10:44:32 AM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 3 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 4 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 5 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 10:44:32 AM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 11, 2019 10:44:32 AM duke.model.PlanQuestionBank +INFO: QuestionBank generated successfully! +Nov 11, 2019 10:44:32 AM duke.model.PlanBot processInput +INFO: + + +Queue size: 2 +Nov 11, 2019 10:44:32 AM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter completeParameterComplements +INFO: ComplementList for parameter names is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter produceParameterComplements +INFO: ComplementList producing parameter names is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter getPurpose +INFO: The original input itself is already complete. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter getPurpose +INFO: The original input itself is already complete. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 11:05:49 AM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 11:05:49 AM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $1 +Nov 11, 2019 11:05:49 AM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $1 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 2 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 3 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 4 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 5 +Nov 11, 2019 11:05:49 AM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 3 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 4 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 5 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 11:05:49 AM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 11, 2019 11:05:49 AM duke.model.PlanQuestionBank +INFO: QuestionBank generated successfully! +Nov 11, 2019 11:05:49 AM duke.model.PlanBot processInput +INFO: + + +Queue size: 2 +Nov 11, 2019 11:05:49 AM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter completeParameterComplements +INFO: ComplementList for parameter names is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter produceParameterComplements +INFO: ComplementList producing parameter names is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter iterateIndex +INFO: Index has been iterated. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter getPurpose +INFO: The original input itself is already complete. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter getPurpose +INFO: The original input itself is already complete. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter receiveText +INFO: Received text for auto-completer. +Nov 11, 2019 8:11:51 PM duke.logic.util.AutoCompleter completeCommandNameComplements +INFO: ComplementList for command names is constructed. +Nov 11, 2019 8:11:51 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $1 +Nov 11, 2019 8:11:51 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $1 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 2 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 3 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 4 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 5 +Nov 11, 2019 8:11:51 PM duke.model.ExpenseList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 3 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 4 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 5 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 1 +Nov 11, 2019 8:11:51 PM duke.model.IncomeList add +INFO: externalList lengths 2 +Nov 11, 2019 8:11:51 PM duke.model.PlanQuestionBank +INFO: QuestionBank generated successfully! +Nov 11, 2019 8:11:51 PM duke.model.PlanBot processInput +INFO: + + +Queue size: 2 +Nov 11, 2019 8:11:51 PM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 11, 2019 10:01:49 PM duke.storage.IncomeListStorageManager +INFO: income.txt file created +Nov 11, 2019 10:01:49 PM duke.storage.BudgetViewStorage +INFO: budgetView.txt file created +Nov 11, 2019 10:01:49 PM duke.storage.payment.PaymentListStorageManager +INFO: PaymentList.txt has been located. +Nov 11, 2019 10:01:49 PM duke.Main init +INFO: Initialized the storage +Nov 11, 2019 10:01:50 PM duke.storage.StorageManager loadPaymentList +INFO: start loading paymentList +Nov 11, 2019 10:01:51 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $7 +Nov 11, 2019 10:01:51 PM duke.model.Budget updateBudgetObservableList +INFO: Size of budgetObserverList: $7 +Nov 11, 2019 10:01:51 PM duke.storage.StorageManager loadPaymentList +INFO: start loading paymentList +Nov 11, 2019 10:01:51 PM duke.model.PlanQuestionBank +INFO: QuestionBank generated successfully! +Nov 11, 2019 10:01:51 PM duke.model.PlanBot sendCompletedMessage +INFO: Completed Plan Bot +Nov 11, 2019 10:01:51 PM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 11, 2019 10:01:51 PM duke.model.PlanQuestionBank makeRecommendation +INFO: Recommendation made successfully! +Nov 11, 2019 10:01:51 PM duke.Main init +INFO: Initialized the model +Nov 11, 2019 10:01:51 PM duke.Main init +INFO: Initialized the logic +Nov 11, 2019 10:01:51 PM duke.Main init +INFO: Initialized the app +Nov 11, 2019 10:01:52 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 12 +Nov 11, 2019 10:01:52 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 12 +Nov 11, 2019 10:01:52 PM duke.ui.ExpensePane +INFO: expenseList has length 12 +Nov 11, 2019 10:01:52 PM duke.ui.ExpensePane +INFO: expenseList has length 12 +Nov 11, 2019 10:01:52 PM duke.ui.ExpensePane +INFO: Items are set. +Nov 11, 2019 10:01:52 PM duke.ui.ExpensePane +INFO: cell factory is set. +Nov 11, 2019 10:01:52 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 12 +Nov 11, 2019 10:01:53 PM duke.ui.ExpensePane +INFO: Pie chart is set. +Nov 11, 2019 10:01:53 PM duke.model.DukePP getExpenseExternalList +INFO: Model sends external expense list length 12 +Nov 11, 2019 10:01:53 PM duke.ui.MainWindow fillInnerPart +INFO: The filled externalList length 12 +Nov 11, 2019 10:01:53 PM duke.ui.MainWindow fillInnerPart +INFO: trendingPane is constructed. +Nov 11, 2019 10:01:53 PM duke.ui.PlanPane +INFO: DialogList set +Nov 11, 2019 10:01:53 PM duke.ui.MainWindow fillInnerPart +INFO: planPane is constructed.2 +Nov 11, 2019 10:01:53 PM duke.model.DukePP getIncomeExternalList +INFO: Model sends external income list length 0 +Nov 11, 2019 10:01:53 PM duke.ui.BudgetPane +INFO: incomeList has length 0 +Nov 11, 2019 10:01:53 PM duke.ui.BudgetPane +INFO: Items are set. +Nov 11, 2019 10:01:53 PM duke.ui.BudgetPane +INFO: cell factory is set. +Nov 11, 2019 10:01:53 PM duke.ui.MainWindow fillInnerPart +INFO: Budget plane is constructed. +Nov 11, 2019 10:01:54 PM duke.logic.util.AutoCompleter +INFO: Auto Completer is constructed. +Nov 11, 2019 10:01:55 PM duke.ui.UiManager start +INFO: MainWindow are showed and filled in. diff --git a/README.md b/README.md index 84755485a7..9dd9b4f14c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ +# Duke++ +Duke++ a better version of duke, a task scheduling tool and is built upon Java 11 platfrom. +For executable Jar files, please visit the [releases](https://github.com/AY1920S1-CS2113T-T12-2/main/releases) section + + +# Developer Guide +[hackmd](https://hackmd.io/@cCavNghrQpmWYXAaTaB_CQ/rk1rcMluS) + + # Setting up + +# User Guide + +[hackmd](https://hackmd.io/eDB8uyaARGWpFUN58Aw-eg?both) + **Prerequisites** * JDK 11 @@ -20,20 +34,3 @@ 1. Ensure that your src folder is checked. Keep clicking `Next`. 1. Click `Finish`. -# Tutorials - -Duke Increment | Tutorial ----------------|--------------- -`A-Gradle` | [Gradle Tutorial](tutorials/gradleTutorial.md) -`A-TextUiTesting` | [Text UI Testing Tutorial](tutorials/textUiTestingTutorial.md) -`Level-10` | JavaFX tutorials:
→ [Part 1: Introduction to JavaFX][fx1]
→ [Part 2: Creating a GUI for Duke][fx2]
→ [Part 3: Interacting with the user][fx3]
→ [Part 4: Introduction to FXML][fx4] - -[fx1]: -[fx2]: -[fx3]: -[fx4]: - -# Feedback, Bug Reports - -* If you have feedback or bug reports, please post in [se-edu/duke issue tracker](https://github.com/se-edu/duke/issues). -* We welcome pull requests too. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..6a51f38046 --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} +checkstyle { + toolVersion = '8.23' +} + +shadowJar { + archiveBaseName = "duke" + archiveVersion = "V1.4" + archiveClassifier = null + archiveAppendix = null + mainClassName = 'duke.Launcher' +} + +dependencies { + String javaFxVersion = '11' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' + + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.5.0' +} + +test { + useJUnitPlatform() +} + +group 'seedu.duke' +version '0.1.0' + +repositories { + mavenCentral() +} + +application { + // Change this to your main class. + mainClassName = "duke/Launcher" +} + +run { + standardInput = System.in + systemProperty "file.encoding", "utf-8" +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..2632364da8 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc new file mode 100644 index 0000000000..53330960ee --- /dev/null +++ b/docs/AboutUs.adoc @@ -0,0 +1,48 @@ += About Us +:site-section: AboutUs +:relfileprefix: team/ +:imagesDir: images +:stylesDir: stylesheets + +Duke was developed by the https://github.com/AY1920S1-CS2113T-T12-2[Duke] team. + +_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + +{empty} + +We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. + +== Project Team + +=== Lucas Foo +image::lucasfoo.png[width="150", align="left"] +{empty} [https://github.com/lucasfoo[github]] [<>] + +Role: Developer + +Responsibilities: Back-end developer + +''' + +=== Xavier Loh +image::xavier.png[width="150", align="left"] +{empty}[http://github.com/otonashixav[github]] [<>] + +Role: Team Lead + +Responsibilities: Front-end developer + +''' + +=== Tamelly Lim +image::termehlee.png[width="150", align="left"] +{empty}[http://github.com/termehlee[github]] [<>] + +Role: Developer + +Responsibilities: Back-end developer + +''' + +=== Liu Chao Jie +image::chaojieliu666.png[width="150", align="left"] +{empty}[https://github.com/ChaojieLiu666[github]] [<>] + +Role: Developer + +Responsibilities: Documentation + testing + +''' diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md new file mode 100644 index 0000000000..f721cb46f9 --- /dev/null +++ b/docs/DeveloperGuide.md @@ -0,0 +1,915 @@ +# Duke++ Developer Guide + + +[TOC] + +## 1. Setting up + +### 1.1 Prerequisites +1. **JDK** `11` or above +2. **IntelliJ** IDE + + :information_source: IntelliJ by default has Gradle and JavaFx plugins installed. Go to `File` > `Settings` > `Plugins` to re-enable them if you have disabled them as this project requires the plugins. + +### 1.2 Setting Up On Your Computer +The following lists the steps to be taken in order to import the project to your local computer. + +1. Fork this repo, and clone the fork to your computer + +2. Open IntelliJ (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project dialog first) + +3. Set up the correct JDK version for Gradle + i. Click `Configure` > `Project Defaults` > `Project Structure` + ii. Click `New…​` and find the directory of the JDK + +4. Click `Import Project` + +5. Locate the `build.gradle` file and select it. Click OK + +6. Click `Open as Project` + +7. Click `OK` to accept the default settings + +8. Open a console and run the command `gradlew processResources` (Mac/Linux: `./gradlew processResources`). It should finish with the `BUILD SUCCESSFUL` message. This will generate all resources required by the application and tests. + +### 1.3 Verifying The Set-Up +Run `duke.Launcher.main` and try a few commands. + +### 1.4 Getting Started With Coding +When you are ready to start coding, we recommend that you get some sense of the overall design by reading the other sections about the architecture of Duke++ in this Developer Guide. + +## 2. Design + +### 2.1 Architecture + + + +Main At app launch: Initializes the components in the correct sequence, and connects them up with each other. + +DukePP object holds all of our models in memory. + +### 2.2 Ui + +The classes in the package Ui, other than UiManager, are controller classes, and will have a corresponding JavaFX .fxml file in resources/view and a optionmal css file in resources/layout. + +UiManager acts as the bridge between and `mainWindow`. + +mainWindow is the primary GUI container that will contain the outer most GUI and will contain inner GUI elements. + +The inner GUI elements inherits from UiPart. +In mainWindow ,the method showPane() chooses which inner GUI elements to display and fillInnerPart populates the inner GUI elements of based on the user commands. + + + +The implementation of Ui is explained in [3.2](#3.2-Ui) + +### 2.3 Logic + +LogicManageris the bridge between the user inputs and the Model and Storage + + +CommandParams splits the user input into individual components of the Main Commandand its +main parameters and secondary parameters. + +The command package includes all of our Commands, which all inherits from the clas Command. + +In each of the sub-classes of Command, execute is overridden with its own specialized Logic. + + +### 2.4 Model + +#### 2.4.1 DukeItem +A generic object in Duke++, for Duke++ to track other things other than expenses. It has the following attribute: +* `tag` +#### 2.4.2 DukeList +A generic list of DukeItems, for Duke++ to track other things other than expenses. +#### 2.4.3 Expense +An Expense inherits from DukeItem and includes the following attributes: +* `BigDecmial amount` +* `String description` +* `LocalDateTime time` +* `boolean isTentative` + +It is constructed using Builder() methods and its attributes are modified and set using its setter methods. + +#### 2.4.4 ExpenseList +Contains a list of all `Expense` objects. + +#### 2.4.5 PlanBot + + +*Figure 2.4.5 PlanBot Class Diagram* + +`PlanBot` is designed as a singleton, i.e there should be only one instance of `PlanBot`. It contains the following: + +* List<PlanDialog> dialogList with an associating ListPlanDialog ObservabledialogList which is the dialog history between the bot and the user. +* PlanQuestion currentQuestion to keep track of the current question being asked. +* Queue<PlanQuestion> questionQueue questionQueue a list of questions that is going to be asked, implemented in a Queue. +* PlanQuestionBank planQuestionBank a collection of all questions that we can retrive questions from. Further explained in [PlanQuestion](#PlanQuestion) + +* Map<String,String> planAttributes which are the known attributes of the user. The key is the attribute of the user, while the value is the variable of the attribute. + +#### 2.4.6 PlanQuestionBank +PlanQuestionBank holds a Map of integers which the keys are the index of the questions, and the value is a PlanQuestion. + +#### 2.4.7 PlanQuestion +Each question has the following attributes: +* String question which is the question itself +* Map<String, String> answersAttributesValue a map of valid answers and attribute values each answer maps to +* Map<String, Set<Integer>neighbouringQuestions +a map of attributes values of the question to its set of integers contain the indices of the neighbouring questions. +* String attribute the attribute we want to determine of the user + +For example, a question of "Are you a NUS student?"" will have a answers of "yes" or "no", to determine the attribute of NUS_STUDENT which can have values "TRUE" or "FALSE", and for "TRUE" replies has neigbouring question of 2,3 and 4. It would look as follows +``` + question = "Are you a NUS student?" + answersAttributesValue = YES : TRUE, NO : FALSE + neighbouringQuestions = FALSE : {2,3,4}"; + attribute = "NUS_STUDENT"; +``` + +#### 2.4.8 PlanRecommendation +PlanRecommendation is an internal class which holds: +`String recommendation` the text that will be displayed to the user. +` Map budget` the recommended budget for the user. Key is the Category, and the value is the monetary value for the category. +`List recommendationExpenseList;` The expenses that is recommended to be added to ExpenseList. + +#### 2.4.8 Income + + +*Figure 2.4.8 Income Class Diagram* + +Each `Income` inherits from DukeItem and contains the following attributes: +- `BigDecimal amount`: The amount of money of the income +- `String description`: The source of the income +- `LocalDateTime time`: The time in which the income was added + +#### 2.4.9 IncomeList +IncomeList inherits from DukeList<Income> and contains the following: +- `List filteredSortedViewedList`: The list which stores all the income after filtering and sorting +- `ObservableList internalFinalList`: The ObservableList to track any changes in the income list +- `ObservableList externalFinalList`: The ObservableList which shows the list of income + +#### 2.4.10 BudgetView +BudgetView contains the following attribute: +- `Map budgetViewCategory`: The map that stores the wanted budget views + +#### 2.4.11 Payment + +A Payment includes the following attributes: + +* `BigDecmial amount` +* `LocalDate due` +* `String description` +* `String receiver` +* `String tag` +* `Priority priority` + +It is constructed and its atrributes are modified and set using Builder() methods. + +#### 2.4.12 PaymentList + + + +The `PaymentList` Contains a list of all `Payment` objects. + +It provides an external unmodiafiable list of sorted and filtered payments. + +The filter and the search function are supported by these `Predicate`s. + +### 2.5 Storage + + + +*Figure 2.5 Structure of the Storage Component* + +The `Storage` component, + +* can save the Expense List data and read it back. + +* can save the Plan Attributes data and read it back. + +* can save the Income List data and read it back. + +* can save the Budget data and read it back. + +* can save the Budget View data and read it back. + +* can save the Payment List data in json format and read it back. + + +## 3. Implementation + +### 3.1 Startup Sequence + +1. Upon starting the Application, it will first run Main.init(), which will construct all of our classes and bridge them based on the architecure as described in [Chapter 2](#2-Design) +2. Storage is first constructed. +3. Model is then constructed and populated by the loading Storage +4. Logic is then constructed with both Model and Storage. +5. Ui is finally constructed using Logic, and is started as described in section [3.2](#3.2-Ui). + + +#### 3.1.1 General implementation + + + +*Figure 3.1.1 Sequence Diagram of a general command being executed.* + +1. When the user enters a command in the UI, upon the on the "Enter" key, the UI passes the command into Logic to execute the command. +2. Within Logic, the command is first split into parts using `CommandParams` +3. Then, the commandResult is generated by executing the `Command` Class, which modifies `Storage` and Model, which is returned to the Ui +4. `Ui` then reflects the changes made in Model and displays it graphically. This is done by using `ObservableList` and `StringProperty` objects in `Model` objects, so that we only need to link the `Ui` to `Model` only once and do not have to manually refresh everything in the GUI on every command. + + +### 3.2 Ui +1. UiManager is first constructed with a Logic object. +2. UiManager then constructs a mainWindow object, passing it the Logic object. +3. mainWindow.show() is called, and displays the welcome screen initially with a textField which is the user input. +4. Then, mainwindow waits for input +5. Upon user input, the method handleKeyPress() in main window is called and will execute either command history or autoComplete() based on the user input +6. handleUserInput() is executed on the enter keypress and on it passes the user input String to Logic to execute the command. It should be noted that the first command should be a goto command so that a inner pane can be shown. +7. A commandResult is returned from Logic, and will populate the inner part of MainWindow based on the DisplayedPane in commandResult, by setting the visibilty of each pane since we are using a JavaFX`StackPane` for the inner elements of the UI. + +### 3.3 Logic +1. LogicManager is first constructed with a Model and Storage Object. +2. Upon a execute("userInput") command, the "userinput" is passed on to CommandParams, which will parse the "userinput" into a Command object and be split into its main parameters and secondary parameters. +3. The execute() of the Command object is run, which is overridden in each of the subclasses as described in [2.3](#2.3-Logic), with Models and Storage passed in so that they could be modified based on the command. + +### 3.4 Model +1. DukePP is constructed with already populated ExpenseList and PlanAttributes that was loaded from Storage. DukePP is the container for all of our models, which are in the Model package. +2. During construction, it also starts a new instance of PlanBot, and passes it the PlanAttributes +3. When a method is called from a higher class (usually Logic), it calls the relevant method of the model to perform operation on the model. + +#### 3.4.1 DukeItem +`DukeItem` is an abstract class to be implemented by its child classes. + +#### 3.4.2 DukeList +`DukeList` is an abstract class to be implemented by its child classes. + +#### 3.4.3 Expense +##### Construction - `Expense.Builder()` +Expense is constructed using an internal `Builder` object. For example: + +``` +Expense.Builder builder = new Expense.Builder(); +builder.setAmount("3.80"); +builder.setDescription("gong cha"); +builder.setTag("drinks"); +builder.setTime("14:00 09/11/2019"); +expenseList.add(builder.build()); +``` + + +`Expense.Builder` has the following methods: +* `setAmount(String amount)` +* `setDescription(String description)` +* `setRecurring(boolean isRecurring)` +* `setTentative(boolean isTentative)` +* `setTime(String Time)` +* `Build()` + +##### Methods +`Expense` has a getter method for each of its attributes previously mentioned. + +#### 3.4.4 ExpenseList implementation +##### Construction - `new ExpenseList(List internalList)` +1. On construction, `ExpenseList` sets its `internalList`, which is a list of all expenses, to the list that was loaded from storage. +2. It sets the default `viewScope` to `ALL` and default `SortCriteria` to `time`. +3. It then updates the `externalList`, which is the list displayed to the user based on the `viewScope` and `sortCriteria` + + +##### Add -`ExpenseList.Add(Expense expense)` +1. An Expense object is constructed as described in the previous section. +2. This expense is then added to the `internalList`. +3. The `externalList`, which is the list the user sees, is updated. + +##### Delete - `ExpenseList.delete(int index)` +The item that corressponds to the index in `externalList` is deleted from both the `externalList` and `internalList`. +##### Sort - `ExpenseList.setSortCriteria(String sortCriteria)` + +The inner enumerator `SortCriteria` represents the sort criteria used to sort the `externalList`. It contains: +* `AMOUNT` Expense with higher amount of money will be prior. +* `TIME` Expense with closer time will be prior. +* `DESCRIPTION` Alphabetical order of expense's description. + +1. The string `sortCriteria` is parsed by the inner enumerator `SortCriteria`. + +2. The field `sortCriteria:SortCriteria` is updated accordingly. + +3. The `externalList`, which is displayed to users, is sorted with the new `sortCriteria`. + + +##### View - `ExpenseList.setViewScope(String viewScope, int viewScopeNumber)` + +The inner class `ViewScope` represents the view scope applied to the `externalList`. It contains two fields: + +* `viewScopeName` including `DAY`, `WEEK`, `MONTH`, `YEAR` and `ALL`. +* `viewScopeNumber` indicates the time scope is how many `DAY`s, `WEEK`s, `MONTH`s or `YEAR`s ago. It is forced to be zero if `viewScopeName` is `ALL`. + +1. A `viewScope` object is created with the given two parameters. + +2. The field `viewScope:ViewScope` is updated accordingly. + +3. The `externalList`, which is displayed to users, is filtered with the new `viewScope`. + + +##### Tentative Expenses +If an `Expense` is tentative, we set its color to greyed out in the GUI, and the expense is not added to the total amount. + +Confirming a tentative expense checks if the specified expense is a tentative one, and if it is, sets the expense to a regular one. + +##### Recurring Expenses +If an `Expense` is recurring, we set its color to Green in the GUI. + +This expense will still appear in other months when `viewScope` is set to `month`, `year` or `all`. + + +#### 3.4.5 PlanBot implementation + + +getDialogObservableList() is the getter method of the dialog history of PlanBot. + +getKnownPlanAttributes() is the getter method of what we know about the user. + +ProcessPlanInput(String input) processes the input entered by the user to PlanBot. + +##### Construction - `Planbot.getInstance()` + +1. On construction, PlanBot constructs the objects mentioned in [2.4.3](#2.4.3-PlanBot). +2. A single instance of PlanQuestionBank is created, initializing its internal list of questions. The list of questions is implemented as a `Map` of the index of the question to a PlanQuestion. The implementation of PlanQuestion will be further explained in [3.4.5](#3.4.5-PlanQuestion). +3. It will then get questions from PlanQuestionBank, passing it the known attributes. PlanQuestionBank will return a set of questions based on the known attributes. The QuestionQueue in PlanBot is then populated with a set of Questions that PlanQuestionBank returned. +4. If the returned Queue is empty, it will show the recommendations based on the known attributes. +5. Else it will set the current Question to the question at the top of questionQueue and pop the question at the front of the questionQueue, and will display add the question to the ObservableDialogList. + + +##### Processing inputs - `PlanBot.processInput(String input)` + + +*Figure 3.4.3 Activity diagram of processing an input for `PlanBot`* + +1. When processInput(String input) is called, it first adds the input string , which is the user's input, into the ObservablePlanDialogList, in order for the user to see what he has replied to the bot. +2. It then checks if there is a currentQuestion, i.e what the user is typing is a reply to a question. If there is no question being asked, we can assume that the bot has already asked all the questions and it will respond to the user by adding a reply telling the user that he/she has completed answering all the questions. +3. If there is a valid currentQuestion, we pass the input into that question, output the reply into the ObservablePlanDialogList if it sucessfully executes, and the `knownAttributes` of the user is updated. +4. The next question is then asked to the user by querying `PlanQuestionBank`. +5. All errors (including inputs) are put into the ObservableDialogList as a reply from `PlanBot`. + +##### Recommendations`getPlanBudgetRecommendation()` +Returns the `PlanBudgetRecommendation` that has been generated by `PlanQuestionBank`. + +This method should only be executed after `PlanQuestionBank` sucessfully generates a budget recommendation after completing all the questions. + +##### Retriving user attributes ` getPlanAttributes()` +Returns `planAttributes`. + +#### 3.4.6 PlanQuestionBank +##### Construction - `PlanQuestionBank.getInstance()` +1. PlanQuestionBank upon construction initalizes a new HashMap<>(). +2. It then populates the `HashMap` with the key being index of the questions and the values being all the questions that we have pre-defined. + + +##### Retrieving unasked questions -`getQuestions()` + + +*Figure 3.4.6.1 FlowChart of Questions being asked to the user* + +1. Based on the known attributes, adds all the questions based on what we know about the user by using a Queue. +2. It does this by getting the neighbours as dictated by neighbouringAttribute and adds them to a Queue. +3. A Queue is chosen to implement this so that we can easily pop the question at the top. +4. Their neighbours then are also added to the queue due to the implementation of the whileloop. +5. While this is going on, the questions are added to a set of questions using a `hashMap`, with the attribute of the question as the key and the question itself as the value. This is so that we can easily prune the set in the next step. +6. Then, it prunes out the questions for attributes of the user that we already know about, and returns the set of questions. + +##### Making recommendations -`makeRecommendation(Map planAttributes)` + + + +*Figure 3.4.6.2 Sequence diagram of the last question being answered, PlanBot generating recommendations and exporting recommendation data* + +For each attribute we can add a: + +* Text suggestion to be shown to the user to a `StringBuilder`, +* Suggested expense to be added to the expenseList by adding an Expense to `List recommendationExpenseList` +* suggested budget for a category by adding to `Map` a category and monetary amount. + +The 3 objects are then passed into the container class `PlanRecommendation` which then is returned. + +#### 3.4.7 PlanQuestion + +##### Construction - `new PlanQestion(String question, String[] answers, String[] attributeValue,String attribute)` + +`String question` the question that we are asking the user. + +`String[] answers` is an Array of strings of the possible answers + +`String[] attributeValue` is an Array of Attributes the attribute could take + +`String attribute` the attribute of the user we want to determine from the question + +The i'th answer in `String[] answers` should correspond to the i'th value of `String[] attributeValue`, i.e if the first expected answer in answers is `YES`, the first value of attributeValue should be true. + +For monetary values, both`String[] answers` and `String[] attributeValue` should contain a single String `"Double"` + + +##### Add neighbouring questions - `addNeighbouring(Integer neighbouring)` +Adds the index of the next question as a neighbour for no matter the `attributeValue`. + + Alternatively, `addNeighbouring(String attributeValue, Integer neighbouring)` Adds the index of the next question for a specific `attributeValue`. + + + + + +##### Getting a reply from a PlanQuestion - `getReply(String input, Map attributes)` + +1. If the value of the attribute we are expecting is a monetary value, we parse the value to a big decimal and update `attributes`. +2. Else it will try to find the mapped attribute value to `input` from `answersAttributesValue`. +3. Then, the updated `attributes` and a Success message is returned using a container class `Reply` + +#### 3.4.7.1 Adding Questions to PlanQuestionBank +1. In the constructor of PlanQuestionBank, construct a new PlanQuestion object. +2. Then, put the question into `questionList`, where the key is the index of the question, and the value is the question you wish to add. The index you have chosen should not have been used by another question. +3. Then add the index to the desired (existing) question that you want your question to be asked after. + + +The following example shows how to add 2 questions(`question13` and `question14`) that is to be asked after `question11`(which is already in `Duke++` currently). Do note that: + +* `Question13` is asking for a yes or no answer +* `Question14` is asking for a monetary amount, +* `question14` will only be asked if `HAS_HOBBIES` is `TRUE`. +``` +private PlanQuestionBank() throws DukeException { + +... +PlanQuestion question11 = new PlanQuestion("Do you subscribe to a music subscription service? ", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "MUSIC_SUBSCRIPTION"); + question11.addNeighbouring(12); + question11.addNeighbouring(13); + questionList.put(11, question11); + +... + +PlanQuestion question13 = new PlanQuestion("Do you have hobbies, + {YES, NO}, + {TRUE, FALSE}, + "HAS_HOBBIES"); +question13.addNeighbouring("TRUE",14); +questionList.put(13, question11); + +PlanQuestion question14 = new PlanQuestion("How much do you spend on your hobbies monthly? " , + {"DOUBLE"}, + {"DOUBLE"}, + "HOBBIES"); +questionList.put(14, question14); + +} +``` +4. Optionally, in `makeRecommendations`, by checking the attrribute and its corresponding value in `planAttributes` the following can be done: +* Add Strings to the output by appending Strings to `StringBuilder recommendation`. +* Add to the budget by mapping a category to a monetary amount, or +* Add an expense by constructing an `Expense` object and adding it to `List recommendationExpenseList` + +Continuing from the previous example, the following shows how to make a `budgetRecommendation` when the user has a hobby and he spends a non-zero amount on it. + +``` +PlanRecommendation makeRecommendation( +MapplanAttributes) +throws DukeException { + Map budgetRecommendation = new HashMap<>(); + StringBuilder recommendation = new StringBuilder(); + List recommendationExpenseList = new ArrayList<>(); + try { + + ... + + if(planAttributes.get("HAS_HOBBIES") == "TRUE") { + BigDecimal hobbyAmount = Parser.parseMoney( + planAttributes.get("HOBBIES")) + if(hobbyAmount.compareTo(BigDecimal.ZERO == 1)) { + recommendation.append(" You should allocate $) + + hobbyAmount + + ( to your hobby!\n\n"); + budgetRecommendation.put("HOBBIES", hobbyAmount) + } + + ... + + } + + ... + }catch { + ... + } +``` + +#### 3.4.8 Income + +##### Overview +The Income feature allows the user to keep track of their sources of income. An `Income` class contain an `amount`, a `description` and a `time`.These incomes are stored in `IncomeList` class, which contains an `ObservableList`. + +New incomes can be added using `AddIncomeCommand` class, which inherits from `Command` class. When an `addIncome` command is given as input, it retrieves the two parameters - `amount` and `description`, of the income. It then adds a new `Income` to `IncomeList` and then stores it to `income.txt`. + +Incomes from `IncomeList` can be deleted using `DeleteIncomeCommand` class, which also inherits from `Command` class. When a `deleteIncome` command is given as input, it retrieves a parameter - `index`, of the income as shown. The `Income` is then deleted from `IncomeList` and stores the update to `income.txt`. + +*Example: User adds an income* + +1. While on `BudgetPane`, the user enters an input `addIncome 1000.50 /d Internship at Grab`. `CommandResult` is constructed, which then calls its own `execute()` method, creating a new `CommandParam` as a result to parse the string input. + +2. `CommandParam` then breaks down the string, recognises and calls `AddIncomeCommand` to initialise it with *1000.50* as the amount and *Internship at Grab* as the description. + +3. When the `execute()` method in `AddIncomeCommand` is called, a new `Income` is initialised with *1000.50* as the amount and *Internship at Grab* as the description through `Income.Builder`. This is added to `IncomeList` through the `addIncome()` method. + +4. This new income is saved to the `income.txt` file by overwritting the text file with the updated `IncomeList` through the `saveIncomeList()` method from `IncomeListStorageManager`. + +5. Finally, the `execute()` method in `AddIncomeCommand` returns a `CommandResult` which flashes a message in the GUI to show a completion of the addition of income. + +##### Construction - `Income.Builder()` +Income is constructed using an internal `Builder` object, just like Expense. For example: + + Income.Builder builder = new Income.Builder(); + builder.setAmount("400"); + builder.setDescription("Pocket Money"); + incomeList.add(builder.build()); +`Income.Builder` has the following methods: +- `setAmount(String amount)` +- `setAmount(BigDecimal amount)` +- `setDescription(String description)` +- `Build()` + +#### 3.4.9 IncomeList Implementation +##### Construction - `new IncomeList(List internalList)` +1. On construction, `IncomeList` sets its `internalList`, which is a list of all expenses, to the list that was loaded from storage file `income.txt`. +2. In cases where the storage file has been corrupted in which the data in `income.txt` cannot be read and parsed to `internalList`, a new `internalList` will be instantiated. + +##### Methods + +##### Add - `IncomeList.add(Income income)` +1. An Income object is constructed as described in [Section 3.4.7](#3.4.7). +2. This income is then added to the `internalList`. +3. The `externalList`, which is the list that the user sees, is updated + +##### Delete - `IncomeList.delete(int index)` +The item that corresponds to the index in `externalList` is deleted from both the `externalList` and `internalList`. + +#### 3.4.10 Budget View +##### Overview +The Budget View feature allows users to keep track of categories deemed as more important to monitor. A `BudgetView` class solely stores a map to handle the different view panes to their respectuve categories. + +Budget views can be added or replaced accordingly using the `ViewBudgetCommand`, depending if a view was already visible. When a `viewBudget` command is given, it takes in two parameters - `view` which refers to the position of the pane the user wishes the budget view to be, and `category`, the specified budget category to track. `budgetView.txt` is then updated and saved accordingly. + +There are 6 positions for the budget view to be placed, represented by a number between *1 and 6*. + +*Example: User adds a budget view* + +1. While on `BudgetPane`, the user enters an input `viewBudget 3 /tag Hall`. `CommandResult` is constructed, which then calls its own `execute()` method, creating a new `CommandParam` as a result to parse the string input. + +2. `CommandParam` then breaks down the string, recognizes and calls `ViewBudgetCommand` to initialise it with *3* as the view and *Hall* as the tag. + +3. When the `execute()` method in `ViewBudgetCommand` is called, `setBudgetView()` method is called from the class `BudgetView`. This updates the map `budgetViewCategory` in `BudgetView` with the two parameters in the new budget view . + +4. The budget view is then saved to the `budgetView.txt` file by overwriting the text file with the updated `budgetViewCatagory` through the `saveBudgetView()` method from `BudgetViewStorageManager`. + +5. Finally, the `execute()` method in `ViewBudgetCommand` returns a `CommandResult` which flashes a message in the GUI to show a completion of the addition of budget view. + +##### Construction - `new BudgetView(Map budgetViewCategory)` +1. On construction, `BudgetView` sets its `budgetViewCategory`, which is a map of budget views, to the map that was loaded from storage file "budgetView.txt" +2. It then updates the view of BudgetPane accordingly + +##### Methods +##### Set - BudgetView.setBudgetView(int view, String category) +The view pane is set to the budget of the specified category by adding the two parameters to `budgetViewCategory`. + + + +*Figure 3.4.9 BudgetView Activity Diagram* + +### 3.5 Storage +1. When launching the application, individual storages of all models are first initialized. + +2. The `StorageManager` is then constructed with these storages of models and it implements the `Storage` as the API. + +3. When constructing `Model`, the `Storage` provides data by its various `load...()` methods such as `loadExpenseList()` and `loadPlanAttributes()`. + +4. When data in `Model` changes due to user commands and the change is supposed to be saved in `Storage`, the `Storage` saves changed data by its various `save...()` methods such as `saveExpenseList()` and `savePlanAttributes()`. + +### 3.6 Reuse History Input +#### 3.6.1 Implementation +The Reuse History Input feature is facilitated by InputHistory class. It internally stores a list inputHistory containing all previous inputs entered by user since the launch of the application and an integer iteratingIndex to iterate through the list. + +It provides following public methods for MainWindow: + +* InputHistory#add(String userInput) — Saves the entered input into history. + +* InputHistory#isAbleToLast() — Indicates whether there are earlier inputs than current index. + +* InputHistory#isAbleToNext() — Indicates whether there are more recent inputs than current index. + +* InputHistory#getLastInput() — Returns one earlier input in history by subtracting index by one. + +* InputHistory#getNextInput() — Returns one more recent input in history by adding index by one. + +These methods are used by textField in MainWindow once a UP or DOWN KeyEvent is detected. + +The following activity diagram summarizes what happens when user press UP or DOWN keys: + + + +#### 3.6.2 Design Considerations +##### Aspect: Where to place inputHistory List +#### • Alternative 1 (current choice): Encapsulates it as individual class. +Pros: Maintains the Single Responsibility Principle. +Cons: Writes more lines of codes. +#### • Alternative 2 : Place it in the MainWindow class. +Pros: Writes fewer lines of codes. +Cons: Violates the Single Responsibility Principle as the MainWindow should not handle this feature. + +### 3.7 Auto-Complete User Input +#### 3.7.1 Implementation + +The auto-complete feature is facilitated by `AutoCompleter`. It receives content from `userInput` and modifies it into a completed command. Modification is done by replacing the fragment after the last space of original input with the complement token produced by `AutoCompleter`. + +A complementList is used to store all suitable complement tokens that can replace the original last token. An iteratingIndex is used to indicate which complement token will be adopted. + +The `AutoCompleter` acquires information of commands by having a `Supplier>` as its field. The following class diagram illustrates the classes involved. + + + +The `AutoCompleter` provides following public methods for MainWindow: + +* AutoCompleter#receiveText(String fromInput) — Receives the content from userInput TextField. + +* AutoCompleter#getFullComplement() — Returns the completed command. + +These methods are be used by `textField` in `MainWindow` once a Tab keyEvent is detected. + +After receiving the text, `AutoCompleter` first decides on its `purpose`. The `purpose` enumerator includes following elements: + +* `COMPLETE_COMMAND_NAME` +* `PRODUCE_PARAMETER` +* `COMPLETE_PARAMETER` +* `ITERATE` +* `NOT_DOABLE` + +If the `purpose` is one of `COMPLETE_COMMAND_NAME`, `PRODUCE_PARAMETER` and `COMPLETE_PARAMETER`, then the `complementList` is reset by all suitable complement tokens and `iteratingIndex` is reset to be zero. + +If the `purpose` is `ITERATE`, then the `iteratingIndex` increases by one, or returns to zero if the tail of `complementList` is reached. + +If the `purpose` is `NOT_DOABLE`, then the `complementList` will be cleared. i.e. There is no suitable complement token. + +Given below is an example usage scenario and how the auto-complement is achieved step by step. + +Step 1. The user launches the application. The `AutoCompleter` will be initialized with an empty `complementList` and an undefined `iteratingIndex`. + +Step 2. The user types "add" inside the command box and presses Tab key (Enter key is not pressed yet). Based on "add", the purpose is decided as `COMPLETE_COMMAND_NAME`. The `complementList` is then reset with all suitable options as below. The `iteratingIndex` is reset as zero, so "addExpense" is adopted to replace "add" in the original input. + + + +

+ +Step 3. The user presses Tab key again to iterate through other options. The `purpose` is decided as `ITERATE` and the `iteratingIndex` increases by one. Now "addPayment" is adopted. + + + +

+ +Step 4. The user presses Tab key again. Now "addBudget" is adopted. + + + +

+ +Step 5. The user presses Tab key again. As the `iteratingIndex` reached the tail of `complementList`, it then returned to the first element. The "addExpense" is adopted again. + + + +

+ +Step 6. The user writes the cost (e.g. S$10) of the expense after "addExpense" and leaves a space. Then user presses Tab again. Based on the valid command name "addExpense", the purpose is decided as `PRODUCE_PARAMETER`. The `complementList` is then reset with all suitable parameter names as below. The `iteratingIndex` is reset as zero, so "/recurring" is adopted to replace the fragment after the last space (appended to the end of input). + + + +

+ +The following activity diagram summarizes what happens when users press Tab key: + + + +#### 3.7.2 Design Consideration +##### Aspect: Coverage of auto-complete +#### Alternative 1: AutoCompleter can fill the command names, parameter names and parameter contents. +Pros: User can auto-complete more contents in the command. +Cons: Increases tremendous couplings between `AutoCompleter` and `Model` as `AutoCompleter` has to access all components in `Model` to get suitable parameter contents for each command. +#### Alternative 2 (current choice): AutoCompleter can only fill command names and parameter names. +Pros: Maintain lower couplings as `AutoCompleter` can get all information from all command classes. +Cons: User has to type contents of parameters by themselves. + +### 3.8 goto command + +The switchable region in the `MainWindow` is `StackPane`. The `StackPane` contains all our panes as its children. + +To show a certain pane, that pane should be set visible, while all other panes are set invisible by using `Node.setVisibility(boolean isVisible)`. + +### 3.9 exit command + + + + + +## 4. Appendix + +### Target User Profile +* NUS student +* has many different types of expenses +* has limited amount of money per month +* can type relatively fast +* does not like the conventional budget tracking app + + +### Value Proposition +Duke++ allows a user who can type quickly using his keyboard to manage his expenses much faster as compared to something like an app with touch interface. + + +### Requirements +#### User Stories +Priority | As a... | I can... | so that | +------------ | ------------- | ------------- | ------------- | +*** | Student | see a history of my spending, adding, editing and deleting entries of my past expenditure (CRUD). | I can track my spending | +** | Student | sort my expenditure according to the amount spent based on categories| I know which items are the most significant | +** | Student | filter my expenditure according to the date| I can see information relevant to me | +** | Student | tag my spendings to general categories such as food, entertainment, transport, shopping | I can manage and track how much I spend on different categories | +** | Student | set goals for overall spending and in each category of spending | so that I can keep within my budget | +** | Student | have a graphical breakdown of my spending with pie charts and graphs throughout the month | I can visualise my spending | +** | Student | filter my spending based on categories and know how much money I can spend in that category | I do not go over budget | +** | Student | track total amount of money loaned out to people | I know how much money I have to work with | +** | Student | keep note of the specific loans owed to me by different people | I can be aware of who owes me money| +** | Beginner Student | have a help menu to aid in navigation | I can learn how to use the program | +** | Student | view Duke++ in a nice User interface | I can interact with Duke more easily | +** | Student | clear the past financial records before a certain time | I can remove outdated information I don't want anymore and release storage| +** | Student | see the amounts of expense over recent months or weeks in one bar chart. | I can know the trend of how my expense varies over time to time. +** | Student | have a grpahical user interface that will display things I want | I can navigate the software easily. +\* | Student | define my own categories | I am not constrained by a fixed list of categories | +\* | Student | have recurring spending with the same name to be automatically tagged to a category | I do not need to do it repeatedly | +\* | Student | add tentative expenditures to my budget | I can choose the best option after comparing my options | +\* | Student |undo commands | I can undo typing mistakes quickly | +\* | Proficient student | have shorter commands | I don’t have to type out my commands fully | +\* | Student | add location information to certain places to see how much money I spend at each place | I know where my money is being spent | +\* | Treasurer | export data to another file format e.g CSV | I can show it to others outside of Duke++ | +\* | Student | hide a specific category from the graphical breakdown of my spending | I can omit certain expenditure when managing my finances if needed| + +#### Non-functional requirements: +* Java 11 +* Open Source APIs only +* CLI/Text based interaction +* Single User +* Human editable storage file +* Portable (No installer) + +#### Use Cases: +##### Use case: View trend for given category +1. User requests to list a given category +2. Duke++ shows a list of spending in a category +3. User requests to show trend in the category +4. Duke++ shows a bar chart of month to month spending in the category +Use case ends. + +##### Use case: Add expenditure +1. User requests to add a certain expenditure +2. Duke++ prompts user to fill in certain fields +3. User fills in requested fields +4. Duke++ confirms addition of expenditure by repeating expenditure added + +##### Extensions: +2. a) List is empty +Use case ends. + + +##### Use case: Export data as file in .csv format +1. User requests to export currently displayed information as a csv file +2. Duke++ prompts user to indicate the address to store the exported file +3. User types in the address +4. Duke exports the csv file to the given address +Use case ends. + + +### Manual Testing Instructions + + +#### PlanBot + +1. Navigate to `PlanPane` using `goto plan`. + +2. Simply reply to PlanBot's questions by typing in the user input and follow the instructions on screen! + +3. After answering all of PlanBot's questions, PlanBot will generate a list of recommendations. + +4. Type `export` when prompted to export the recommendations. + +#### Expense +1. Navigate to `ExpensePane` using `goto expense`. + +2. Add an expense. +``` +addExpense 2.60 /d Milk Tea /tag Drinks /isTentative +``` +3. Delete the expense using its index as reflected. +``` +deleteExpense 1 +``` +4. Confirm a tentative expense +``` +confirm 1 +``` +5. Sort the expense list +``` +sortExpense time +``` +6. Change the view scope of the expense list +``` +viewExpense month +``` + +#### Payment + +Tips: Please follow the order below for better user experience. + +1. Navigate to `PaymentPane` using `goto payment`. +``` +goto payment +``` + +2. Add a payment with properties given below. +``` +addPayment 65 /description Illustrator workshop sign up fee /due 02/12/2019 /priority medium /tag study /receiver Tuition Center +``` + +3. Change properties of an existing payment. Here we change the month from November to December both in description and due. After the change, the payment may be placed below as the sorting criteria is TIME. +``` +changePayment 1 /description Top Up Mobile Data for December /due 01/12/2019 +``` + +4. Delete a payment with given index. +``` +deletePayment 1 +``` + +5. Finish a payment with given index. The finished payment will be recorded by Expense Tracker. +``` +donePayment 1 +``` + +6. Use `goto` command to check whether the payment is recorded by Expense Tracker. +``` +goto expense +``` + +7. Change the view scope to only show payments coming in the current week. +``` +viewPayment week +``` + +8. Switch the view scope to all. +``` +viewPayment all +``` + +9. Sort payments with the sorting criteria `amount`. +``` +sortPayment amount +``` + +10. Search payments with the keyword "Raffles". +``` +searchPayment Raffles +``` + + + +#### Income List +1. Navigate to `BudgetPane` using `goto budget`. + +2. Add an income. +``` +addIncome 400 /d Pocket Money +``` +3. Delete an income based on its index (0-based). +``` +deleteIncome 1 +``` + +#### Budget +1. Navigate to `BudgetPane` using `goto budget`. + +2. Add budget for the month. +``` +addBudget 300 +``` +3. Add budget for a specific tag. +``` +addBudget 20 /tag Shopping +``` +4. Set a particular pane to the specified budget. +``` +viewBudget 1 /tag Food +``` + diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index fd44069597..0000000000 --- a/docs/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# User Guide - -## Features - -### Feature 1 -Description of feature. - -## Usage - -### `Keyword` - Describe action - -Describe action and its outcome. - -Example of usage: - -`keyword (optional arguments)` - -Expected outcome: - -`outcome` diff --git a/docs/UserGuide.md b/docs/UserGuide.md new file mode 100644 index 0000000000..c17eee0ef0 --- /dev/null +++ b/docs/UserGuide.md @@ -0,0 +1,1173 @@ +# User Guide for Duke++ + + + +## 1. Table of Contents + + + +[TOC] + + + +## 2. Introduction +Welcome to Duke++, a free, simple and fast budgeting program, designed specifically with NUS students in mind. Duke++ can keep track of your past expenses, and display them in a user friendly and clean graphical user interface. +Duke++ also can recommend to you a budget based on your preferences and lifestyle. + +This user guide will help you get started on using Duke++, and will give you tips and tricks to use Duke++ as efficiently as possible using some of its features. + +### 2.1 Glossary +`Expense` an amount of money spent. + +`Budget` a upper limit goal of how much you would like to spend. + +`Payment` a payment the user need to pay in future. + +`GUI` Graphical User Interface, the user interface that the user interacts with. + +`Chat bot` a software program that attempts acts like a human to hold a conversation with. + +`Recurring expenses` are expenses that repeats monthly. + +`Tentative expenses` are expenses that have not been confirmed to be spent. + +`Pane` A specific GUI for a given functionality. [Expense](#Expenses-management-in-`expense`) +has its own pane (*Figure 4.1*) for tracking expenses. + + + +## 3. Quick Start +1. Ensure you have Java 11 or above on your computer. +2. Download the latest release from our [releases](https://github.com/AY1920S1-CS2113T-T12-2/main/releases) page. +3. Copy the file to the folder you want to use as the root folder of Duke++. +4. Double click the jar file to run it. +5. You will be prompted by the chatbot to start interacting with it. Just follow along the instructions to start using! + + + +## 4. Features + +### 4.1 Expenses management in `expense` + + +*Figure 4.1 GUI of Expense pane* + +A list to keep track of expenses. + +Enter commands to sort, filter and change the scope of the expenses, further elaborated [here](#4.2-Expense-commands). + + +### 4.2 Budget recommendations using `plan` + + + +*Figure 4.2.1 GUI of PlanBot* + +Budget recommendations is done by a chat bot, known as PlanBot. + +PlanBot will ask you questions dynamically based on your responses, and then make recommendations to you based on your response. + +Simply reply to PlanBot's questions by typing in the user input and follow the instructions on screen! + +An example of `PlanBot` asking questions *dynamically* is: if the answer to *Do you live on campus?* is *yes*, `PlanBot` will not ask you about how you travel to school. + + +There are three main types of + +* For questions that asks for a choice `e.g. `, reply with one of the choice. e.g `yes` + +* For questions that asks for a monetary amount, enter the amount in dollars and cents. `e.g. 3.50` + +* For questions that prompts you with a range of numbers, enter a whole number in the range. `e.g. questions asks for <0-3>, valid answers are 1, 2 and 3` + + + +*Figure 4.2.2 PlanBot's recommendations* + + +After answering all of PlanBot's questions, PlanBot will generate a list of recommendations. + +An example of a recommendation is: if PlanBot detects that you are better off buying concessions pass for public transport then paying each fare, it will recommend you to do so. + +Type `export` when prompted to export the recommendations. + + +### 4.3 Payments Reminder in `payment` + + + +*Figure 4.3 GUI of Payment Reminder* + +A reminder of payments to pay in future is displayed via a filterable, sortable and searchable GUI list. It helps you manage and remind money you need to pay. + +### 4.4 Budget Management in `budget` + + +*Figure 4.4 GUI of Budget Management* + +Allows you to track budgets for specific categories and helps you closely monitor your expenses for these categories. + + + +## 5. Commands + +### 5.1 General commands and navigation + +Here is a complete list of all commands for your convenience. + +Please note that arguments with a `#` prefix in this guide are arguments that you should specify. + +#### 5.1.1 (Proposed - Coming in `v2.0`) `help` - Get Help with Commands + +Get help with using Duke++. + +#### 5.1.2 `goto` - Navigate between different GUIs of Duke++ + +##### Basic Usage + +``` +goto #pane +``` +`#pane` can take the following values below. +##### Panes + +| Pane | Description | +| --------- | ----------- | +| `expense` | Main screen with the expense list | +| `plan` | Recommendation chat bot| +| `payment` | Pending payments reminder | +| `budget` | Budget management | + +#### 5.1.3 Autofilling of commands +Press the `tab` button on your keyboard when entering a command to automatically complete the word of a command. E.g after typing in `add`, press `tab` and Duke++ will fill it up to `addExpense`. + +Pressing `tab` again will cycle through other options if there are multiple options. E.g. in the previous example, if we press `tab` again we will get `addIncome` or `addPayment`. + +When the command name is complete, pressing `tab` will produce or complete the paramter name. E.g. after typing in `addPayment` and a space , press `tab` and Duke++ will produce `/description`; or when parameter is incomplete as `/d`, press `tab` and Duke++ will fill it up to `/description`. + +Pressing `tab` again will cycle through other options if there are multiple options. E.g in the previous example, if we press `tab` again we will get `/due`. + + +This feature is useful in filling in commands and getting parameters' names. + +`Tips`: + +1. The autoCompleter can only make modification at the end of input. i.e. It doesn't support complement on a word not at the end. + +2. To complete a word, no space should be at the end of input; To produce a parameter name, a space must be at the end of input. + +#### 5.1.4 Command history + +Pressing the `UP` or `DOWN` cursor/arrow keys will bring back your previous inputs. + +This feature is useful for reusing or fixing previous commands. + +### 5.2 Expense Commands (To be done in `expense` pane) + +#### 5.2.1 `addExpense` - Add a New Expense + +Adds a new expense to the current list of expenses. The properties of the new expense can be specified. + +##### Basic Usage + +``` +addExpense #amount +``` + +`#amount` - The expense amount in dollars and cents. (E.g 2.50 for two dollars and fifty cents) + +##### Additional Parameters + +| Parameter | Description | +| --------- | ----------- | +| `/d #description` | The name or a short description of the expense. | +| `/time #time` | The time that the expense should take place. By default, the expense takes place at the time it is added. It should be in `hh:mm dd/mm/yyyy` format.| +| `/tag #tag` | The tags that should be assigned to the expense. | +| `/isRecurring` | Denotes that the expense is a recurring expense. | +| `/isTentative` | Denotes that the expense is a tentative expense. | +| `/tag #tag` | The tag that should be assigned to the expense. | + + +#### 5.2.2 `deleteExpense` - Deletes expense(s). + + +##### Basic Usage + +``` +deleteExpense #item(s) +``` + +`#items(s)` can take the following arguments: + +| Parameter | Description | +| --------- | ----------- | +|`#index` | The displayed number of the item we wish to delete from the list +| `#range` | the range of expenses we want to delete (e.g `deleteExpense 2-4`) deletes the indexes 2, 3 ,4 | +| `all` | deletes the entire list. Use with caution!| + + +#### 5.2.3 `confirm` - Confirms a tentative expense + + +##### Basic Usage + +``` +confirm #index +``` + +`#index` - The displayed number of the item we wish to confirm from the list + + + + + +#### 5.2.4. `sortExpense` - Sort Displayed Expenses + +Change the order that expenses are displayed. + +``` +sortExpense #properties +``` + +`#properties` - can take the values: + +* `time`: sorts the list by latest at the top +* `description`: sorts the list in alphabetical order +* `amount`: sorts the list by largest at the top + + + + +#### 5.2.5 `viewExpense` - Change the View Scope of Expenses + +Displays expenses within the given time scope. + +##### Basic Usage + +``` +viewExpense #timeScope +``` + +`#timeScope` - The time scope of displayed expenses. It can be one of `day`, `week`, `month`, `year` and `all`. + +##### Additional Parameters + + | Parameter | Description | + | --------- | ----------- | + |`/previous #previous`| Number of days/weeks/months/years ago. | + +Warning: Remember to switch back to `all` after `/previous` is applied, as the number of `/previous` may be forgotten and then some expenses added later may be filterd out by it. + +### 5.3 Payment Reminder Commands (To be done in `payment` pane) + +#### 5.3.1 `addPayment` - Add a New Payment to Pay + +Adds a new payment to the payments reminder. The properties of the new payment can be specified. + +##### Basic Usage + +``` +addPayment #amount /description #description /due #due +``` + +`#amount` - The payment amount in dollars and cents. (E.g 2.50 for two dollars and fifty cents) + +`#description` - The name or a short description of the payment. + +`#due` - The due date of the payments, following the format dd/mm/yyyy. (E.g. 31 Oct 2019 is represented as 31/10/2019) + +##### Additional Parameters + +| Parameter | Description | +| --------- | ----------- | +| `/priority #priority`| The priority of the payments. `#priority` can be one of `High`, `Medium` or `Low`. It is `Medium` by default if not specified.| +| `/tag #tag` | The tag assigned to the payment. It is totally customized by user. | +| `/receiver #receiver` | The receiver of the payment. | + +#### 5.3.2 `changePayment` - Change an Existing Payment + +Changes properties of a payment. + +##### Basic Usage + +``` +changePayment #index +``` +`#index` - The index of the target payment to be changed. + +Even though changePayment command does not require any compulsory parameters , but the command without parameters makes no sense as it does not change any property of the target payment. + +##### Additional Parameters + +| Parameter | Description | +| --------- | ----------- | +| `/description #description` | The new description of the payment. | +| `/due #due` | The new due date of the payment. It should follow the format dd/mm/yyyy.| +| `/tag #tag` | The new tag assigned to the payment. | +| `/priority #priority` | The new priority level of the payment. It should be one of `High`, `Medium` and `Low`. | +| `/receiver #receiver` | The new receiver of the payment. | +| `/amount #amount` | The new amount of money of the payment. | + + +#### 5.3.3 `deletePayment` - Delete an Existing Payment + +Deletes a payment. + +##### Basic Usage + +``` +deletePayment #index +``` +`#index` - The index of the target payment to be deleted. + +#### 5.3.4 `donePayment` - Finish an Existing Payment + +Completes a payment by removing it from payment list and then records it to the Expense Tracker. + +##### Basic Usage + +``` +donePayment #index +``` + +`#index` - The index of the target payment. + +#### 5.3.5 `viewPayment` - View Payments within a Specified Time Scope + +Filters payments with the given time scope. + +##### Basic Usage + +``` +viewPayment #timeScope +``` + +`#timeScope` - The timeScope used to filter payments. It can be one of `overdue`, `week`, `month` and `all`. + +The `overdue` represents payments not finished by due. The `week` and `month` represent current week and current month respectively. The `all` simply displays all payments. + +#### 5.3.6 `sortPayment` - Sort Payments. + +Sorts payments with the given sorting criteria. + +##### Basic Usage + +``` +sortPayment #sortingCriteria +``` + +`#sortingCriteria` - The sorting criteria used to sort the payments. It can be one of `amount`, `time` and `priority`. + +Payments with higher amount, closer due or higher priority are to be placed at the top of the list. + +#### 5.3.7 `searchPayment` - Search Payments. + +Searches payments matching with the given keyword. + + + +##### Basic Usage + +``` +searchPayment #keyword +``` + +`#keyword` - The keyword for searching, where the letter case is ignored. + +### 5.4 Income Commands (To be done in `budget` pane) +#### 5.4.1 **`addIncome` - Add a New Income** +Adds a new income to the current list of incomes. The source and amount of the new income has to be specified. + +*Basic Usage* +``` +addIncome #amount /d #description +``` +`#amount` - The income amount in dollars and cents. (e.g. `1000.50` for one thousand dollars and fifty cents) + +`#description`- The description or source of income + +Example: `addIncome 1000.50 /d Internship at Grab` + +>Keying in the command: +> +>![](https://i.imgur.com/LfrPFGi.png) +> +>After registering the command: +> +>![](https://i.imgur.com/zSr7Blr.png) + + + +#### 5.4.2 **`deleteIncome` - Delete Income(s)** + +Delete specific income(s) from the list. + +*Basic Usage* +``` +deleteIncome #index +``` +`#index` - The displayed number of the income we wish to delete from the list + +*Alternate Arguments* +|Parameter|Description| +|---------|-----------| +| `#range` | Range of incomes we wish to delete (e.g. `deleteIncome 2-4` deletes the incomes with indexes 2,3,4 as shown)| +|`all`| Deletes every income from the list. Use with caution!| + +Example: `deleteIncome 1` + +>Keying in the command: +>![](https://i.imgur.com/W1hFlTZ.png) +> +>After registering the command: +> +>![](https://i.imgur.com/hB70pjJ.png) + +### 5.5 Budget Commands (To be done in `budget` pane) +#### 5.5.1 **`addBudget` - Add a New Budget** +Adds a new budget. + +*Basic Usage* +``` +addBudget #index +``` +`#index` - The desired amount to be set as the budget + +*Note: Not including additional parameters changes the monthly budget only* + +*Alternate Arguments* +|Parameter |Description| +|--------------| -----------| +| `/tag #tag` | Name of desired category to add budget to | + +Example: `addBudget 100 /tag Hall` + +#### 5.5.2 **`viewBudget` - Track a Budget Category** +Adds a new budget category to monitor visually. + +*Basic Usage* +``` +viewBudget #view /tag #category +``` +`#view` - The view pane we wish to place the specified category which ranges from 1 to 6 + +`#category` - The category we wish to track + +Example: `viewBudget 3 /tag Hall` + +> Keying in the command: +> +>![](https://i.imgur.com/REttIvV.png) +> +> After registering the command: +> +>![](https://i.imgur.com/4LPAebd.png) + + + + + +## 6. Overview of Commands + + +### Navigation + +`#` prefix denotes an argument that should be replaced a valid value. + + +Command | Arguments | Optional +------- | --------- | -------- +`goto`| `expense`/` plan`/`payment`/`budget`| | + +#### 6.1 Expense +Command | Arguments | Optional +------- | --------- | -------- +`addExpense`| `#amount`| `/d /time /tag /isTentative /tag`| +`deleteExpense`|`#index`/`#range`/`all`| +`confirm`|`#index`| +`sortExpense`|`time`/`description`/`amount` | +`viewExpense`|`day`/`week`/`month`/`year` /`all`|`/previous` + +#### 6.2 PlanBot +Simply reply to PlanBot's questions by typing in the user input and follow the instructions on screen! + +After answering all of PlanBot's questions, PlanBot will generate a list of recommendations. + +Type `export` when prompted to export the recommendations. + + + +#### 6.3 Payment + +Command | Arguments | Optional +------- | --------- | -------- +`addPayment`| `#amount` & `/description` & `/due` | `/tag /receiver /priority`| +`changePayment`| `#index` | `/amount /description /due /tag /receiver /priority` | +`deletePayment`|`#index`| +`donePayment`|`#index`| +`sortPayment`|`time`/`priority`/`amount` | +`viewPayment`|`overdue`/`week`/`month` /`all`|| +`searchPayment` | `#keyword` || + +#### 6.4 Income + +| Command | Arguments | +| ----------- | --------- | +| `addIncome` | `#amount` | +| `deleteIncome`| `#index`| + +#### 6.6 Budget +|Command|Arguments|Optional| +| ------------ | ------| ----- | +| `addBudget`| `#amount`| `/tag`| +| `viewBudget` | `#view` `/tag`| + + + + diff --git a/docs/images/Activity Diagram for AutoCompleter.png b/docs/images/Activity Diagram for AutoCompleter.png new file mode 100644 index 0000000000..b588cd4d04 Binary files /dev/null and b/docs/images/Activity Diagram for AutoCompleter.png differ diff --git a/docs/images/Activity_Diagram_inputHistory.png b/docs/images/Activity_Diagram_inputHistory.png new file mode 100644 index 0000000000..8e312c9794 Binary files /dev/null and b/docs/images/Activity_Diagram_inputHistory.png differ diff --git a/docs/images/Architecture diagram (1).png b/docs/images/Architecture diagram (1).png new file mode 100644 index 0000000000..729adb10bd Binary files /dev/null and b/docs/images/Architecture diagram (1).png differ diff --git a/docs/images/AutoCompleteState1.png b/docs/images/AutoCompleteState1.png new file mode 100644 index 0000000000..9a23f9b37e Binary files /dev/null and b/docs/images/AutoCompleteState1.png differ diff --git a/docs/images/AutoCompleteState2.png b/docs/images/AutoCompleteState2.png new file mode 100644 index 0000000000..b36cde905e Binary files /dev/null and b/docs/images/AutoCompleteState2.png differ diff --git a/docs/images/AutoCompleteState3.png b/docs/images/AutoCompleteState3.png new file mode 100644 index 0000000000..a639b2d17e Binary files /dev/null and b/docs/images/AutoCompleteState3.png differ diff --git a/docs/images/AutoCompleteState4.png b/docs/images/AutoCompleteState4.png new file mode 100644 index 0000000000..e96f89a620 Binary files /dev/null and b/docs/images/AutoCompleteState4.png differ diff --git a/docs/images/AutoCompleteState5.png b/docs/images/AutoCompleteState5.png new file mode 100644 index 0000000000..ed3ef97db0 Binary files /dev/null and b/docs/images/AutoCompleteState5.png differ diff --git a/docs/images/AutoCompleter Class diagram.png b/docs/images/AutoCompleter Class diagram.png new file mode 100644 index 0000000000..ef89b83c55 Binary files /dev/null and b/docs/images/AutoCompleter Class diagram.png differ diff --git a/docs/images/General Sequence Diagram Example.png b/docs/images/General Sequence Diagram Example.png new file mode 100644 index 0000000000..4aa2a557b5 Binary files /dev/null and b/docs/images/General Sequence Diagram Example.png differ diff --git a/docs/images/Income List Class Diagram.png b/docs/images/Income List Class Diagram.png new file mode 100644 index 0000000000..461a0defee Binary files /dev/null and b/docs/images/Income List Class Diagram.png differ diff --git a/docs/images/Payment Reminder Class Diagram.png b/docs/images/Payment Reminder Class Diagram.png new file mode 100644 index 0000000000..e408433ca7 Binary files /dev/null and b/docs/images/Payment Reminder Class Diagram.png differ diff --git a/docs/images/PlanBot Activity Diagram.png b/docs/images/PlanBot Activity Diagram.png new file mode 100644 index 0000000000..3b5028d470 Binary files /dev/null and b/docs/images/PlanBot Activity Diagram.png differ diff --git a/docs/images/PlanBot Class Diagram.png b/docs/images/PlanBot Class Diagram.png new file mode 100644 index 0000000000..9c26fa4a88 Binary files /dev/null and b/docs/images/PlanBot Class Diagram.png differ diff --git a/docs/images/PlanBot Export Sequence.png b/docs/images/PlanBot Export Sequence.png new file mode 100644 index 0000000000..9fdde0a390 Binary files /dev/null and b/docs/images/PlanBot Export Sequence.png differ diff --git a/docs/images/PlanQuestion Flow.png b/docs/images/PlanQuestion Flow.png new file mode 100644 index 0000000000..9997f45b94 Binary files /dev/null and b/docs/images/PlanQuestion Flow.png differ diff --git a/docs/images/Screenshots/Payment_Reminder.png b/docs/images/Screenshots/Payment_Reminder.png new file mode 100644 index 0000000000..ec1bd8461b Binary files /dev/null and b/docs/images/Screenshots/Payment_Reminder.png differ diff --git a/docs/images/Screenshots/Screenshot of BudgetPane.png b/docs/images/Screenshots/Screenshot of BudgetPane.png new file mode 100644 index 0000000000..6a34976b05 Binary files /dev/null and b/docs/images/Screenshots/Screenshot of BudgetPane.png differ diff --git a/docs/images/Screenshots/expense.png b/docs/images/Screenshots/expense.png new file mode 100644 index 0000000000..07d3db48cf Binary files /dev/null and b/docs/images/Screenshots/expense.png differ diff --git a/docs/images/Screenshots/planbot.png b/docs/images/Screenshots/planbot.png new file mode 100644 index 0000000000..8bd25ef125 Binary files /dev/null and b/docs/images/Screenshots/planbot.png differ diff --git a/docs/images/Screenshots/planbotExport.png b/docs/images/Screenshots/planbotExport.png new file mode 100644 index 0000000000..251a136f6a Binary files /dev/null and b/docs/images/Screenshots/planbotExport.png differ diff --git a/docs/images/Storage Class Diagram.png b/docs/images/Storage Class Diagram.png new file mode 100644 index 0000000000..a6f58067ac Binary files /dev/null and b/docs/images/Storage Class Diagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png new file mode 100644 index 0000000000..33800547d5 Binary files /dev/null and b/docs/images/Ui.png differ diff --git a/docs/images/chaojieliu666.png b/docs/images/chaojieliu666.png new file mode 100644 index 0000000000..c31760619c Binary files /dev/null and b/docs/images/chaojieliu666.png differ diff --git a/docs/images/lucasfoo.png b/docs/images/lucasfoo.png new file mode 100644 index 0000000000..d35248eab0 Binary files /dev/null and b/docs/images/lucasfoo.png differ diff --git a/docs/images/otonashixav.png b/docs/images/otonashixav.png new file mode 100644 index 0000000000..cf608f6ab2 Binary files /dev/null and b/docs/images/otonashixav.png differ diff --git a/docs/images/termehlee.png b/docs/images/termehlee.png new file mode 100644 index 0000000000..9d2953051f Binary files /dev/null and b/docs/images/termehlee.png differ diff --git a/docs/images/viewBudget Activity Diagram.png b/docs/images/viewBudget Activity Diagram.png new file mode 100644 index 0000000000..d791003622 Binary files /dev/null and b/docs/images/viewBudget Activity Diagram.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..b493aca85c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Oct 02 16:50:15 SGT 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..6d57edc706 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..d1e92fe5db --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'duke' diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/duke/Launcher.java b/src/main/java/duke/Launcher.java new file mode 100644 index 0000000000..b8c0f9c373 --- /dev/null +++ b/src/main/java/duke/Launcher.java @@ -0,0 +1,9 @@ +package duke; + +import javafx.application.Application; + +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/duke/Main.java b/src/main/java/duke/Main.java new file mode 100644 index 0000000000..778f02f659 --- /dev/null +++ b/src/main/java/duke/Main.java @@ -0,0 +1,245 @@ +package duke; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.logic.Logic; +import duke.logic.LogicManager; +import duke.model.Income; +import duke.model.IncomeList; +import duke.model.Expense; +import duke.model.ExpenseList; +import duke.model.DukePP; +import duke.model.Model; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; +import duke.storage.BudgetStorage; +import duke.storage.BudgetViewStorage; +import duke.storage.ExpenseListStorage; +import duke.storage.ExpenseListStorageManager; +import duke.storage.IncomeListStorage; +import duke.storage.IncomeListStorageManager; +import duke.storage.PlanAttributesStorage; +import duke.storage.PlanAttributesStorageManager; +import duke.storage.Storage; +import duke.storage.StorageManager; +import duke.storage.payment.PaymentListStorage; +import duke.storage.payment.PaymentListStorageManager; +import duke.ui.Ui; +import duke.ui.UiManager; +import javafx.application.Application; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Map; +import java.util.logging.Logger; + +/** + * Bridge between duke and MainWindow. + */ +public class Main extends Application { + + private static final Logger logger = LogsCenter.getLogger(Main.class); + + private Ui ui; + private Logic logic; + private Model model; + private Storage storage; + + @Override + public void init() throws Exception { + super.init(); + + ExpenseListStorage expenseListStorage = new ExpenseListStorageManager(); + PlanAttributesStorage planAttributesStorage = new PlanAttributesStorageManager(); + IncomeListStorage incomeListStorage = new IncomeListStorageManager(); + BudgetStorage budgetStorage = new BudgetStorage(); + BudgetViewStorage budgetViewStorage = new BudgetViewStorage(); + PaymentListStorage paymentListStorage = new PaymentListStorageManager(); + + storage = new StorageManager(expenseListStorage, + planAttributesStorage, + incomeListStorage, + budgetStorage, + budgetViewStorage, + paymentListStorage); + + logger.info("Initialized the storage"); + + //Demo Code, loads demo data on first boot + if (storage.loadExpenseList().internalSize() == 0 || storage.loadExpenseList() == null) { + loadListDemoData(storage); + } + if (storage.loadPaymentList().isEmpty()) { + logger.warning("PaymentList is not loaded"); + } + if (storage.loadExpenseList() == null) { + logger.warning("expenseList is not loaded"); + } + if (storage.loadIncomeList() == null) { + logger.warning("incomeList is not loaded"); + } + if (storage.loadBudget() == null) { + logger.warning("budgetList is not loaded"); + } + + model = new DukePP(storage.loadExpenseList(), + storage.loadPlanAttributes(), + storage.loadIncomeList(), + storage.loadBudget(), + storage.loadBudgetView(), + storage.loadPaymentList()); + + logger.info("Initialized the model"); + + logic = new LogicManager(model, storage); + + logger.info("Initialized the logic"); + + ui = new UiManager(logic); + logger.info("Initialized the app"); + + } + + + /** + * Starts Duke with MainWindow. + * + * @param primaryStage The main GUI of Duke + */ + @Override + public void start(Stage primaryStage) { + primaryStage.setResizable(false); + ui.start(primaryStage); + } + + public static void main(String[] args) { + launch(args); + } + + private final Storage loadListDemoData(Storage storage) { + Expense.Builder expenseBuilder = new Expense.Builder(); + Income.Builder incomeBuilder = new Income.Builder(); + Payment.Builder paymentBuilder = new Payment.Builder(); + try { + // loading expense demo data + expenseBuilder.setAmount("3.50"); + expenseBuilder.setDescription("Chicken Rice"); + expenseBuilder.setTag("FOOD"); + expenseBuilder.setTime("18:00 09/11/2019"); + ExpenseList expenseList = storage.loadExpenseList(); + expenseList.add(expenseBuilder.build()); + + expenseBuilder.setAmount("5.50"); + expenseBuilder.setDescription("Pineapple Fried Rice"); + expenseBuilder.setTag("FOOD"); + expenseBuilder.setTime("18:00 08/11/2019"); + expenseList.add(expenseBuilder.build()); + + expenseBuilder.setAmount("4.99"); + expenseBuilder.setDescription("Mighty Zinger Burger"); + expenseBuilder.setTag("FOOD"); + expenseBuilder.setTime("12:00 08/11/2019"); + expenseList.add(expenseBuilder.build()); + + expenseBuilder.setAmount("3.80"); + expenseBuilder.setDescription("Gong Cha"); + expenseBuilder.setTag("DRINKS"); + expenseBuilder.setTime("14:00 09/11/2019"); + expenseList.add(expenseBuilder.build()); + + expenseBuilder.setAmount("78.50"); + expenseBuilder.setDescription("Uniqlo"); + expenseBuilder.setTag("CLOTHES"); + expenseBuilder.setTime("14:00 09/06/2019"); + expenseList.add(expenseBuilder.build()); + + expenseBuilder.setAmount("85"); + expenseBuilder.setDescription("Mario Kart 8"); + expenseBuilder.setTag("GAMES"); + expenseBuilder.setTime("14:00 09/06/2018"); + expenseList.add(expenseBuilder.build()); + storage.saveExpenseList(expenseList); + + // loading income demo data + incomeBuilder.setAmount("400"); + incomeBuilder.setDescription("Pocket Money"); + IncomeList incomeList = storage.loadIncomeList(); + incomeList.add(incomeBuilder.build()); + + incomeBuilder.setAmount("250.70"); + incomeBuilder.setDescription("Part-Time Job"); + incomeList.add(incomeBuilder.build()); + storage.saveIncomeList(incomeList); + + // loading plan bot demo data + Map planAttributes = storage.loadPlanAttributes(); + planAttributes.put("NUS_STUDENT", "TRUE"); + planAttributes.put("ONLINE_SHOPPING", "100"); + planAttributes.put("MUSIC_SUBSCRIPTION", "TRUE"); + planAttributes.put("PHONE_BILL", "30.00"); + planAttributes.put("NETFLIX", "TRUE"); + storage.savePlanAttributes(planAttributes); + + // loading payment demo data + + paymentBuilder.setDescription("Raffles Hall Orientation Fee"); + paymentBuilder.setAmount("60").setTag("school life").setDue("05/01/2020"); + paymentBuilder.setPriority("Low").setReceiver("Raffles Hall"); + PaymentList paymentList = storage.loadPaymentList().get(); + paymentList.add(paymentBuilder.build()); + + logger.info("*********loading sample payment"); + + paymentBuilder.setDescription("Matriculation Card Replacement Fee"); + paymentBuilder.setAmount("30").setTag("school life").setDue("08/12/2019"); + paymentBuilder.setPriority("High").setReceiver("OSA"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("Top Up Mobile Data for November"); + paymentBuilder.setAmount("10").setTag("phone bill").setDue("01/11/2019"); + paymentBuilder.setPriority("Low").setReceiver("Singtel"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("Raffles Hall Room Preservation Fee"); + paymentBuilder.setAmount("200").setTag("housing").setDue("05/12/2019"); + paymentBuilder.setPriority("High").setReceiver("Raffles Hall"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("Pay Back Money to John"); + paymentBuilder.setAmount("35").setTag("loan").setDue("05/11/2019"); + paymentBuilder.setPriority("Medium").setReceiver("John"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("Pay Back Money to Alice"); + paymentBuilder.setAmount("15").setTag("loan").setDue("17/11/2019"); + paymentBuilder.setPriority("Medium").setReceiver("Alice"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("Repay OCBC Student Loan for November"); + paymentBuilder.setAmount("600").setTag("loan").setDue("30/11/2019"); + paymentBuilder.setPriority("High").setReceiver("OCBC"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("In Room Storage Fee"); + paymentBuilder.setAmount("150").setTag("housing").setDue("19/11/2019"); + paymentBuilder.setPriority("Medium").setReceiver("Alice"); + paymentList.add(paymentBuilder.build()); + + paymentBuilder.setDescription("PhotoShop Camp Sign Up Fee"); + paymentBuilder.setAmount("60").setTag("study").setDue("03/12/2019"); + paymentBuilder.setPriority("Medium").setReceiver("NUSSU CommIT"); + paymentList.add(paymentBuilder.build()); + + try { + storage.savePaymentList(paymentList); + } catch (IOException e) { + logger.warning("Sample data did not save"); + } + + } catch (DukeException e) { + e.printStackTrace(); + } + return storage; + } + +} diff --git a/src/main/java/duke/commons/FileUtil.java b/src/main/java/duke/commons/FileUtil.java new file mode 100644 index 0000000000..abf6f9cf5f --- /dev/null +++ b/src/main/java/duke/commons/FileUtil.java @@ -0,0 +1,85 @@ +// Credit: Adopted from reference project addressbook-level3. + +package duke.commons; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Writes and reads files. + */ +public class FileUtil { + + private static final String CHARSET = "UTF-8"; + + public static boolean isFileExists(Path file) { + return Files.exists(file) && Files.isRegularFile(file); + } + + /** + * Returns true if {@code path} can be converted into a {@code Path} via Paths#get(String), + * otherwise returns false. + * @param path A string representing the file path. Cannot be null. + */ + public static boolean isValidPath(String path) { + try { + Paths.get(path); + } catch (InvalidPathException ipe) { + return false; + } + return true; + } + + /** + * Creates a file if it does not exist along with its missing parent directories. + * @throws IOException if the file or directory cannot be created. + */ + public static void createIfMissing(Path file) throws IOException { + if (!isFileExists(file)) { + createFile(file); + } + } + + /** + * Creates a file if it does not exist along with its missing parent directories. + */ + public static void createFile(Path file) throws IOException { + if (Files.exists(file)) { + return; + } + + createParentDirsOfFile(file); + + Files.createFile(file); + } + + /** + * Creates parent directories of file if it has a parent directory. + */ + public static void createParentDirsOfFile(Path file) throws IOException { + Path parentDir = file.getParent(); + + if (parentDir != null) { + Files.createDirectories(parentDir); + } + } + + /** + * Assumes file exists. + */ + public static String readFromFile(Path file) throws IOException { + return new String(Files.readAllBytes(file), CHARSET); + } + + /** + * Writes given string to a file. + * Will create the file if it does not exist yet. + */ + public static void writeToFile(Path file, String content) throws IOException { + Files.write(file, content.getBytes(CHARSET)); + } + +} \ No newline at end of file diff --git a/src/main/java/duke/commons/JsonUtil.java b/src/main/java/duke/commons/JsonUtil.java new file mode 100644 index 0000000000..e8f7193f3b --- /dev/null +++ b/src/main/java/duke/commons/JsonUtil.java @@ -0,0 +1,143 @@ +// Credit: Adopted from reference project addressbook-level3. + +package duke.commons; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import duke.exception.DukeException; + +/** + * Converts a Java object instance to JSON and vice versa. + */ +public class JsonUtil { + + private static final Logger logger = LogsCenter.getLogger(JsonUtil.class); + + private static ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .registerModule(new SimpleModule("SimpleModule") + .addSerializer(Level.class, new ToStringSerializer()) + .addDeserializer(Level.class, new LevelDeserializer(Level.class))); + + static void serializeObjectToJsonFile(Path jsonFile, T objectToSerialize) throws IOException { + FileUtil.writeToFile(jsonFile, toJsonString(objectToSerialize)); + } + + static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObjectToDeserialize) + throws IOException { + return fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize); + } + + /** + * Returns the Json object from the given file or {@code Optional.empty()} object if the file is not found. + * If any values are missing from the file, default values will be used, as long as the file is a valid json file. + * @param filePath cannot be null. + * @param classOfObjectToDeserialize Json file has to correspond to the structure in the class given here. + * @throws DukeException if the file format is not as expected. + */ + public static Optional readJsonFile( + Path filePath, Class classOfObjectToDeserialize) throws DukeException { + requireNonNull(filePath); + + if (!Files.exists(filePath)) { + logger.info("Json file " + filePath + " not found"); + return Optional.empty(); + } + + T jsonFile; + + try { + jsonFile = deserializeObjectFromJsonFile(filePath, classOfObjectToDeserialize); + } catch (IOException e) { + logger.warning("Error reading from jsonFile file " + filePath + ": " + e); + throw new DukeException(e.getMessage()); + } + + return Optional.of(jsonFile); + } + + /** + * Saves the Json object to the specified file. + * Overwrites existing file if it exists, creates a new file if it doesn't. + * @param jsonFile cannot be null + * @param filePath cannot be null + * @throws IOException if there was an error during writing to the file + */ + public static void saveJsonFile(T jsonFile, Path filePath) throws IOException { + requireNonNull(filePath); + requireNonNull(jsonFile); + + serializeObjectToJsonFile(filePath, jsonFile); + } + + + /** + * Converts a given string representation of a JSON data to instance of a class. + * @param The generic type to create an instance of + * @return The instance of T with the specified values in the JSON string + */ + public static T fromJsonString(String json, Class instanceClass) throws IOException { + return objectMapper.readValue(json, instanceClass); + } + + /** + * Converts a given instance of a class into its JSON data string representation. + * @param instance The T object to be converted into the JSON string + * @param The generic type to create an instance of + * @return JSON data representation of the given class instance, in string + */ + public static String toJsonString(T instance) throws JsonProcessingException { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(instance); + } + + /** + * Contains methods that retrieve logging level from serialized string. + */ + private static class LevelDeserializer extends FromStringDeserializer { + + protected LevelDeserializer(Class vc) { + super(vc); + } + + @Override + protected Level _deserialize(String value, DeserializationContext ctxt) { + return getLoggingLevel(value); + } + + /** + * Gets the logging level that matches loggingLevelString. + *

+ * Returns null if there are no matches + * + */ + private Level getLoggingLevel(String loggingLevelString) { + return Level.parse(loggingLevelString); + } + + @Override + public Class handledType() { + return Level.class; + } + } + +} \ No newline at end of file diff --git a/src/main/java/duke/commons/LogsCenter.java b/src/main/java/duke/commons/LogsCenter.java new file mode 100644 index 0000000000..079298ebc2 --- /dev/null +++ b/src/main/java/duke/commons/LogsCenter.java @@ -0,0 +1,103 @@ +// Credit: Adopted from reference project addressbook-level3. + +package duke.commons; + +import java.io.IOException; +import java.util.Arrays; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * Configures and manages loggers and handlers, including their logging level + * Named {@link Logger}s can be obtained from this class
+ * These loggers have been configured to output messages to the console and a {@code .log} file by default, + * at the {@code INFO} level. A new {@code .log} file with a new numbering will be created after the log + * file reaches 5MB big, up to a maximum of 5 files.
+ */ +public class LogsCenter { + private static final int MAX_FILE_COUNT = 5; + private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB + private static final String LOG_FILE = "Duke++.log"; + private static Level currentLogLevel = Level.INFO; + private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); + private static FileHandler fileHandler; + private static ConsoleHandler consoleHandler; + + /** + * Creates a logger with the given name. + */ + public static Logger getLogger(String name) { + Logger logger = Logger.getLogger(name); + logger.setUseParentHandlers(false); + + removeHandlers(logger); + addConsoleHandler(logger); + addFileHandler(logger); + + return Logger.getLogger(name); + } + + /** + * Creates a Logger for the given class name. + */ + public static Logger getLogger(Class clazz) { + if (clazz == null) { + return Logger.getLogger(""); + } + return getLogger(clazz.getSimpleName()); + } + + /** + * Adds the {@code consoleHandler} to the {@code logger}.
+ * Creates the {@code consoleHandler} if it is null. + */ + private static void addConsoleHandler(Logger logger) { + if (consoleHandler == null) { + consoleHandler = createConsoleHandler(); + } + logger.addHandler(consoleHandler); + } + + /** + * Remove all the handlers from {@code logger}. + */ + private static void removeHandlers(Logger logger) { + Arrays.stream(logger.getHandlers()) + .forEach(logger::removeHandler); + } + + /** + * Adds the {@code fileHandler} to the {@code logger}.
+ * Creates {@code fileHandler} if it is null. + */ + private static void addFileHandler(Logger logger) { + try { + if (fileHandler == null) { + fileHandler = createFileHandler(); + } + logger.addHandler(fileHandler); + } catch (IOException e) { + logger.warning("Error adding file handler for logger."); + } + } + + /** + * Creates a {@code FileHandler} for the log file. + * @throws IOException if there are problems opening the file. + */ + private static FileHandler createFileHandler() throws IOException { + FileHandler fileHandler = new FileHandler(LOG_FILE, MAX_FILE_SIZE_IN_BYTES, MAX_FILE_COUNT, true); + fileHandler.setFormatter(new SimpleFormatter()); + fileHandler.setLevel(currentLogLevel); + return fileHandler; + } + + private static ConsoleHandler createConsoleHandler() { + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(currentLogLevel); + return consoleHandler; + } +} \ No newline at end of file diff --git a/src/main/java/duke/exception/DukeException.java b/src/main/java/duke/exception/DukeException.java new file mode 100644 index 0000000000..ed2ec4964f --- /dev/null +++ b/src/main/java/duke/exception/DukeException.java @@ -0,0 +1,44 @@ +package duke.exception; + +/** + * The exception Duke throws upon encountering a problem that can be recovered from. + */ +public class DukeException extends Exception { + public static final String MESSAGE_LOAD_FILE_FAILED = "The file at %s could not be loaded. " + + "I will back it up and create a new file."; + public static final String MESSAGE_SAVE_FILE_FAILED = "The file at %s could not be saved to. " + + "Close other programs that may be accessing it."; + public static final String MESSAGE_NO_ITEM_AT_INDEX = "There is no %s numbered %d!"; + public static final String MESSAGE_PARSER_TIME_INVALID = "%s is not a valid time!"; + public static final String MESSAGE_EXPENSE_AMOUNT_INVALID = "%s is not a valid amount!"; + public static final String MESSAGE_EXPENSE_TIME_INVALID = "%s is not a valid time!"; + public static final String MESSAGE_COMMAND_PARAM_UNKNOWN = "%s is not a valid parameter for this command!"; + public static final String MESSAGE_COMMAND_PARAM_MISSING_VALUE = "/%s needs a value!"; + public static final String MESSAGE_COMMAND_PARAM_MISSING = "This command needs /%s to be given!"; + public static final String MESSAGE_COMMAND_PARAM_DUPLICATE = "/%s cannot be specified twice!"; + public static final String MESSAGE_COMMAND_NAME_UNKNOWN = "I don't know what command that is!"; + public static final String MESSAGE_COMMAND_PARAM_REDUNDANT = "Redundant parameter %s!"; + public static final String MESSAGE_BUDGET_AMOUNT_INVALID = "%s is not a valid amount!"; + public static final String MESSAGE_BUDGET_VIEW_INVALID = "%s is not a valid pane! Choose a pane between 1 to 6!"; + public static final String MESSAGE_SORT_CRITERIA_INVALID = "%s is not a valid sort criteria!"; + public static final String MESSAGE_EXPENSE_VIEW_NAME_INVALID = "%s is not a valid view scope name!"; + public static final String MESSAGE_EXPENSE_VIEW_NUMBER_INVALID = "%s is not a valid view scope number!"; + public static final String MESSAGE_PANE_NAME_INVALID = "%s is not an available pane! " + + "Choose between \"expense, payment, budget and plan\"!"; + public static final String MESSAGE_INCOME_AMOUNT_INVALID = "%s is not a valid amount!"; + public static final String MESSAGE_DELETE_FORMAT_INVALID = "%s is not a valid format! " + + "First index has to be smaller than the second index!"; + public static final String MESSAGE_PRIORITY_NAME_INVALID = "%s is not a priority name."; + public static final String MESSAGE_PAYMENT_STORAGE_MISSING_FIELD = "Payment's %s field is missing!"; + public static final String MESSAGE_PAYMENT_AMOUNT_INVALID = "%s is not a valid amount!"; + public static final String MESSAGE_PAYMENT_TIME_INVALID = "%s is not a valid date!"; + public static final String MESSAGE_PAYMENT_SAVE_FAILED = "Errors occur in payment storage"; + public static final String MESSAGE_NUMBER_FORMAT_INVALID = "%s is not a valid index!"; + public static final String MESSAGE_PAYMENT_SCOPE_INVALID = "%s is not a valid time scope"; + public static final String MESSAGE_PLANBOT_INVALID_REPLY = "Please enter a valid reply!"; + public static final String MESSAGE_TAG_TOO_LONG = "The maximum length of tag is 30 chars."; + + public DukeException(String message) { + super(message); + } +} diff --git a/src/main/java/duke/exception/DukeRuntimeException.java b/src/main/java/duke/exception/DukeRuntimeException.java new file mode 100644 index 0000000000..5f4bb370a7 --- /dev/null +++ b/src/main/java/duke/exception/DukeRuntimeException.java @@ -0,0 +1,23 @@ +package duke.exception; + +/** + * The exception Duke throws upon encountering an unexpected error not caused by the user nor + * by invalid validation of parameters. + */ +public class DukeRuntimeException extends RuntimeException { + private static final String MESSAGE_FATAL_ERROR = "A fatal error has occurred. %s."; + + /** + * Constructs an {@code DukeRuntimeException} object with exception message. + */ + public DukeRuntimeException(String message) { + super(String.format(MESSAGE_FATAL_ERROR, message)); + } + + /** + * Constructs an {@code DukeRuntimeException} object with exception message and cause. + */ + public DukeRuntimeException(String message, Throwable cause) { + super(String.format(MESSAGE_FATAL_ERROR, message), cause); + } +} diff --git a/src/main/java/duke/logic/CommandParams.java b/src/main/java/duke/logic/CommandParams.java new file mode 100644 index 0000000000..b4d591a7ea --- /dev/null +++ b/src/main/java/duke/logic/CommandParams.java @@ -0,0 +1,226 @@ +package duke.logic; + +import duke.exception.DukeException; +import duke.exception.DukeRuntimeException; +import duke.logic.command.DeleteIncomeCommand; +import duke.logic.command.ViewBudgetCommand; +import duke.logic.command.payment.AddPaymentCommand; +import duke.logic.command.payment.ChangePaymentCommand; +import duke.logic.command.payment.DeletePaymentCommand; +import duke.logic.command.payment.FilterPaymentCommand; +import duke.logic.command.payment.SearchPaymentCommand; +import duke.logic.command.payment.SortPaymentCommand; +import duke.logic.command.payment.DonePaymentCommand; +import duke.logic.command.AddExpenseCommand; +import duke.logic.command.AddIncomeCommand; +import duke.logic.command.BudgetCommand; +import duke.logic.command.Command; +import duke.logic.command.ConfirmTentativeCommand; +import duke.logic.command.DeleteExpenseCommand; +import duke.logic.command.ExitCommand; +import duke.logic.command.FilterExpenseCommand; +import duke.logic.command.GoToCommand; +import duke.logic.command.PlanBotCommand; +import duke.logic.command.SortExpenseCommand; +import duke.logic.command.ViewExpenseCommand; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * An object containing information about a command's type and parameters. + */ +public class CommandParams { + // Internal map that stores all secondary parameters + private final Map secondaryParams; + + // The command type i.e. the first word in the command + private final Command command; + + // The main parameter value i.e. everything after the first word, before any secondary parameters are declared + private final String mainParam; + + // The regular expression used to identify secondary parameters. + // Currently matches and replaces any number of spaces followed by a forward slash (\\s+(\\/)), + // which are followed by any word consisting of only lowercase alphabets (not replaced). + // Matches [and replaces]: "[ /]at", "[ /]b", "[ /]test" + // Ignores: "1/1", "a / b", "a/ " + private static final Pattern PARAM_INDICATOR_REGEX = Pattern.compile("(\\s+(\\/(?=[a-z]+)))"); + + // The regular expression used to identify a space. + // Currently matches and replaces any number of spaces. + private static final Pattern SPACE_REGEX = Pattern.compile("(\\s+)"); + + private static final Supplier> COMMANDS = () -> Stream.of( + new AddExpenseCommand(), + new DeleteExpenseCommand(), + new ConfirmTentativeCommand(), + new ExitCommand(), + new FilterExpenseCommand(), + new SortExpenseCommand(), + new ViewExpenseCommand(), + new GoToCommand(), + new PlanBotCommand(), + new BudgetCommand(), + new AddPaymentCommand(), + new ChangePaymentCommand(), + new DeletePaymentCommand(), + new FilterPaymentCommand(), + new SearchPaymentCommand(), + new SortPaymentCommand(), + new AddIncomeCommand(), + new DeleteIncomeCommand(), + new ViewBudgetCommand(), + new DonePaymentCommand() + ); + + /** + * Creates a new {@code CommandParams} object using a {@code String} obtained directly from + * the user. The {@code CommandParams} object cannot have two parameters of the same name, and + * will throw a {@code DukeException} if the user tries to specify two parameters of the same name. + * + * @param fullCommand the full command input by the user, which will be parsed into parameters. + * @throws DukeException if the user specified a parameter twice. + */ + public CommandParams(String fullCommand) throws DukeException { + secondaryParams = new HashMap(); + + // Split the input into an array of Strings, containing concatenated parameter names and values + String[] nameValueStrings = PARAM_INDICATOR_REGEX.split(fullCommand.trim()); + + // Get commandType and mainParam first + command = parseCommand(nameValueStrings[0]); + mainParam = extractMainParam(nameValueStrings[0], SPACE_REGEX.split(command.getName()).length); + + // Get all the others + for (int i = 1; i < nameValueStrings.length; i++) { + String[] nameValuePair = SPACE_REGEX.split(nameValueStrings[i], 2); + List possibleParamNames = command.getSecondaryParams().keySet().stream() + .filter(k -> k.startsWith(nameValuePair[0])) + .collect(Collectors.toList()); + + if (possibleParamNames.size() != 1) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_UNKNOWN, nameValuePair[0])); + } + + String verifiedParamName = possibleParamNames.get(0); + + if (secondaryParams.containsKey(verifiedParamName)) { // can't contain the same key twice + throw new DukeException( + String.format(DukeException.MESSAGE_COMMAND_PARAM_DUPLICATE, verifiedParamName)); + } + + if (nameValuePair.length == 2) { + secondaryParams.put(verifiedParamName, nameValuePair[1]); + } else { + secondaryParams.put(verifiedParamName, null); + } + } + } + + /** + * Returns the command corresponding to this command params object. + * + * @return the command corresponding to this command params object. + */ + public Command getCommand() { + return command; + } + + /** + * Returns the {@code mainParam} parameter that was input by the user. May be null. + * + * @return {@code mainParam}. May be null. + */ + public String getMainParam() { + return mainParam; + } + + /** + * Returns whether the command has a {@code mainParam}. + * + * @return the existence of {@code mainParam}, that is, whether it is null or not. + */ + public boolean containsMainParam() { + return mainParam != null; + } + + /** + * Returns the value of a requested parameter. The parameter's existence should be checked prior if + * the parameter is optional, as this method throws {@code DukeException} if the parameter does not + * exist, or is null. + * + * @param paramName the name of the parameter whose value to return. + * @return the value of the requested parameter. + * @throws DukeRuntimeException if the parameter does not exist, or is null. + */ + public String getParam(String paramName) throws DukeException { + String paramValue = secondaryParams.get(paramName); + if (paramValue == null) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING_VALUE, paramName)); + } else { + return paramValue; + } + } + + /** + * Returns true if all parameters specified by {@code paramNames} exist in the {@code CommandParams} + * object, and false otherwise. + *

+ * Can be used to check for optional flags. + * + * @param paramNames the parameter(s) whose existence to check for. + * @return true if the parameter(s) specified by {@code paramNames} exists, and false otherwise. + */ + public boolean containsParams(String... paramNames) { + for (String paramName : paramNames) { + if (!secondaryParams.containsKey(paramName)) { + return false; + } + } + return true; + } + + private static String extractMainParam(String string, int numberOfWords) { + String[] words = SPACE_REGEX.split(string, numberOfWords + 1); + if (words.length <= numberOfWords) { + return null; + } else { + return words[numberOfWords]; + } + } + + private static Command parseCommand(String commandName) throws DukeException { + // Inelegant solution, but I don't want to have to add a new method to every Command class. + String[] commandNameWords = Arrays.copyOfRange(commandName.split("\\s+"), 0, 2); + + if (commandNameWords.length == 2) { + List validCommands = COMMANDS.get() + .filter(c -> c.getName().split(" ").length == 2) + .filter(c -> (c.getName().split(" ")[0].startsWith(commandNameWords[0]) + && c.getName().split(" ")[1].startsWith(commandNameWords[1]))) + .collect(Collectors.toList()); + + if (validCommands.size() == 1) { + return validCommands.get(0); + } + } + + List validCommands = COMMANDS.get() + .filter(c -> c.getName().split(" ").length == 1) + .filter(c -> (c.getName().split(" ")[0].startsWith(commandNameWords[0]))) + .collect(Collectors.toList()); + + if (validCommands.size() == 1) { + return validCommands.get(0); + } + + throw new DukeException(DukeException.MESSAGE_COMMAND_NAME_UNKNOWN); + } +} diff --git a/src/main/java/duke/logic/CommandResult.java b/src/main/java/duke/logic/CommandResult.java new file mode 100644 index 0000000000..5081871c09 --- /dev/null +++ b/src/main/java/duke/logic/CommandResult.java @@ -0,0 +1,72 @@ +package duke.logic; + +import static java.util.Objects.requireNonNull; + +/** + * Represents the result of a command execution. + */ +public class CommandResult { + + /** + * Represents all panes that can be displayed in the main window. + * This is used when the displayed pane is to be switched due to the command operation. + */ + public enum DisplayedPane { + EXPENSE, + TRENDING, + BUDGET, + PLAN, + PAYMENT, + } + + /** + * The feedback message to be displayed in the console. + */ + private String consoleInfo; + + /** + * The pane to be displayed. + */ + private DisplayedPane displayedPane; + + /** + * The application should exit. + */ + private boolean isExit; + + /** + * Constructs a {@code CommandResult} with the specified fields. + * + * @param consoleInfo The message to be displayed in the console. + * @param displayedPane The pane to be displayed in the main window. + * @param isExit Whether the application should exit. + */ + public CommandResult(String consoleInfo, DisplayedPane displayedPane, boolean isExit) { + this.consoleInfo = requireNonNull(consoleInfo); + this.displayedPane = requireNonNull(displayedPane); + this.isExit = isExit; + } + + /** + * Constructs a {@code CommandResult} with the specified {@code consoleInfo} and {@code displayedPane}. + * The field {@code isExit} is set to its default value false. + * + * @param consoleInfo The message to be displayed in the console. + * @param displayedPane The pane to be displayed in the main window. + */ + public CommandResult(String consoleInfo, DisplayedPane displayedPane) { + this(requireNonNull(consoleInfo), requireNonNull(displayedPane), false); + } + + public String getConsoleInfo() { + return consoleInfo; + } + + public DisplayedPane getDisplayedPane() { + return displayedPane; + } + + public boolean isExit() { + return isExit; + } +} diff --git a/src/main/java/duke/logic/Logic.java b/src/main/java/duke/logic/Logic.java new file mode 100644 index 0000000000..b0f119ce6a --- /dev/null +++ b/src/main/java/duke/logic/Logic.java @@ -0,0 +1,81 @@ +package duke.logic; + +import duke.exception.DukeException; +import duke.model.Expense; +import duke.model.Income; +import duke.model.PlanBot; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.function.Predicate; + +/** + * API of the Logic component. + */ +public interface Logic { + + /** + * Executes the user input and returns the result. + * + * @param userInput The command as entered by the user. + * @return the result of the command execution. + * @throws DukeException If an error occurs during parsing or command execution. + */ + CommandResult execute(String userInput) throws DukeException; + + ObservableList getExternalExpenseList(); + + ObservableList getDialogObservableList(); + + BigDecimal getTagAmount(String tag); + + ObservableList getExternalIncomeList(); + + ObservableList getBudgetObservableList(); + + BigDecimal getMonthlyBudget(); + + BigDecimal getTotalAmount(); + + BigDecimal getRemaining(BigDecimal total); + + Map getBudgetViewCategory(); + + BigDecimal getBudgetTag(String category); + + /** + * Returns an unmodifiable external list of PaymentList. + * + * @return The external list to be displayed. + */ + ObservableList getUnmodifiableFilteredPaymentList(); + + /** + * Returns the sorting criteria used in PaymentList. + * + * @return The sorting criteria being used. + */ + ObjectProperty getPaymentSortingCriteria(); + + /** + * Returns the predicate used in PaymentList. + * + * @return The predicate being used. + */ + ObjectProperty getPaymentPredicate(); + + StringProperty getExpenseListTotalString(); + + StringProperty getSortCriteriaString(); + + StringProperty getViewCriteriaString(); + + StringProperty getFilterCriteriaString(); + + StringProperty getIncomeListTotalString(); +} diff --git a/src/main/java/duke/logic/LogicManager.java b/src/main/java/duke/logic/LogicManager.java new file mode 100644 index 0000000000..c96c53c17e --- /dev/null +++ b/src/main/java/duke/logic/LogicManager.java @@ -0,0 +1,138 @@ +package duke.logic; + +import duke.exception.DukeException; +import duke.logic.command.Command; +import duke.model.Expense; +import duke.model.Income; +import duke.model.Model; +import duke.model.PlanBot; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; +import duke.storage.Storage; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.function.Predicate; + +/** + * The main LogicManager of the app. + */ +public class LogicManager implements Logic { + + private Model model; + private Storage storage; + + /** + * Constructs a {@code LogicManager} with model and storage. + * + * @param model model of Duke++. + * @param storage storage of Duke++. + */ + public LogicManager(Model model, Storage storage) { + this.model = model; + this.storage = storage; + } + + @Override + public CommandResult execute(String userInput) throws DukeException { + CommandResult commandResult; + CommandParams commandParams = new CommandParams(userInput); + Command command = commandParams.getCommand(); + commandResult = command.execute(commandParams, model, storage); + + return commandResult; + } + + @Override + public ObservableList getExternalExpenseList() { + return model.getExpenseExternalList(); + } + + @Override + public ObservableList getDialogObservableList() { + return model.getDialogObservableList(); + } + + @Override + public BigDecimal getTagAmount(String tag) { + return model.getExpenseList().getTagAmount(tag); + } + + @Override + public ObservableList getExternalIncomeList() { + return model.getIncomeExternalList(); + } + + @Override + public ObservableList getBudgetObservableList() { + return model.getBudgetObservableList(); + } + + @Override + public BigDecimal getMonthlyBudget() { + return model.getMonthlyBudget(); + } + + @Override + public BigDecimal getTotalAmount() { + return model.getTotalAmount(); + } + + @Override + public BigDecimal getRemaining(BigDecimal total) { + return model.getRemaining(total); + } + + @Override + public Map getBudgetViewCategory() { + return model.getBudgetViewCategory(); + } + + @Override + public BigDecimal getBudgetTag(String category) { + return model.getBudgetTag(category); + } + + public ObservableList getUnmodifiableFilteredPaymentList() { + return model.getUnmodifiableFilteredPaymentList(); + } + + @Override + public ObjectProperty getPaymentSortingCriteria() { + return model.getPaymentSortingCriteria(); + } + + @Override + public ObjectProperty getPaymentPredicate() { + return model.getPaymentPredicate(); + } + + @Override + public StringProperty getExpenseListTotalString() { + return model.getExpenseListTotalString(); + } + + @Override + public StringProperty getSortCriteriaString() { + return model.getSortCriteriaString(); + } + + @Override + public StringProperty getViewCriteriaString() { + return model.getViewCriteriaString(); + } + + @Override + public StringProperty getFilterCriteriaString() { + return model.getFilterCriteriaString(); + } + + @Override + public StringProperty getIncomeListTotalString() { + return model.getIncomeListTotalString(); + } + +} diff --git a/src/main/java/duke/logic/command/AddExpenseCommand.java b/src/main/java/duke/logic/command/AddExpenseCommand.java new file mode 100644 index 0000000000..3e603cf444 --- /dev/null +++ b/src/main/java/duke/logic/command/AddExpenseCommand.java @@ -0,0 +1,96 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Expense; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a specified command as AddCommand by extending the {@code Command} class. + * Adds various specified type of expensesList into the ExpenseList. e.g event + * Responses with the result. + */ +public class AddExpenseCommand extends Command { + private static final String name = "addExpense"; + private static final String description = "Adds a new Expense"; + private static final String usage = "add $cost"; + + private static final String COMPLETE_MESSAGE = "Added the expense!"; + + private enum SecondaryParam { + DESCRIPTION("description", "a short description or name for the expense"), + TAG("tag", "tags that should be added to the expense"), + TIME("time", "the time of the expense"), + TENTATIVE("tentative", "sets the expense to be tentative"), + RECURRING("recurring", "sets the expense to be recurring"); + + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a new command object, with its name, description, usage and secondary parameters. + */ + public AddExpenseCommand() { + super(name, + description, + usage, + Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description)) + ); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + Expense.Builder expenseBuilder = new Expense.Builder(); + + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "amount")); + } + expenseBuilder.setAmount(commandParams.getMainParam()); + + if (commandParams.containsParams(SecondaryParam.DESCRIPTION.name)) { + expenseBuilder.setDescription(commandParams.getParam(SecondaryParam.DESCRIPTION.name)); + } + + if (commandParams.containsParams(SecondaryParam.TAG.name)) { + expenseBuilder.setTag(commandParams.getParam(SecondaryParam.TAG.name).toUpperCase()); + } + + if (commandParams.containsParams(SecondaryParam.TIME.name)) { + expenseBuilder.setTime(commandParams.getParam(SecondaryParam.TIME.name)); + } + + if (commandParams.containsParams(SecondaryParam.TENTATIVE.name)) { + expenseBuilder.setTentative(true); + } + + if (commandParams.containsParams(SecondaryParam.RECURRING.name)) { + expenseBuilder.setRecurring(true); + } + + model.addExpense(expenseBuilder.build()); + // duke.expenseList.update(); + storage.saveExpenseList(model.getExpenseList()); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + /* + if (commandParams.containsParams(SecondaryParam.TENTATIVE.name)) { + expenseBuilder.setTentative(true); + } + */ + + } + +} diff --git a/src/main/java/duke/logic/command/AddIncomeCommand.java b/src/main/java/duke/logic/command/AddIncomeCommand.java new file mode 100644 index 0000000000..ad1a8b1f9e --- /dev/null +++ b/src/main/java/duke/logic/command/AddIncomeCommand.java @@ -0,0 +1,83 @@ +package duke.logic.command; + +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.exception.DukeException; +import duke.model.Income; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a specified command as AddIncomeCommand by extending the {@code Command} class. + * Adds various specified income into the IncomeList. + * Responses with the result. + */ +public class AddIncomeCommand extends Command { + private static final String name = "addIncome"; + private static final String description = "Adds a new Income"; + private static final String usage = "add $income"; + + private static final String COMPLETE_MESSAGE = "Added the income!"; + + private enum SecondaryParam { + DESCRIPTION("description", "a short description or name for source of the income"); + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a new command object, with its name, description, usage and secondary parameters. + */ + public AddIncomeCommand() { + super(name, + description, + usage, + Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description)) + ); + } + + /** + * Creates a new Income with the corresponding amount and description and + * saves it to storage file {@code income.txt}. + * + * Responses the result to user by using ui of Duke++ in BudgetPane. + * + * @param commandParams the parameters given by the user, parsed into a {@code CommandParams} object. + * @param model {@code Model} which the command should operate on. + * @param storage the storage of Duke++. + * @return CommandResult the result of the command, which is a completed logger message, in budget display pane + * @throws DukeException if amount or description of the income is missing + */ + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + Income.Builder incomeBuilder = new Income.Builder(); + + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "amount of income")); + } + + if (!commandParams.containsParams(SecondaryParam.DESCRIPTION.name)) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "source of income")); + + } + incomeBuilder.setAmount(commandParams.getMainParam()); + incomeBuilder.setDescription(commandParams.getParam(SecondaryParam.DESCRIPTION.name)); + + model.addIncome(incomeBuilder.build()); + storage.saveIncomeList(model.getIncomeList()); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.BUDGET); + } + +} diff --git a/src/main/java/duke/logic/command/BudgetCommand.java b/src/main/java/duke/logic/command/BudgetCommand.java new file mode 100644 index 0000000000..5d1661fbe2 --- /dev/null +++ b/src/main/java/duke/logic/command/BudgetCommand.java @@ -0,0 +1,62 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BudgetCommand extends Command { + private static final String name = "addBudget"; + private static final String description = "sets a budget"; + private static final String usage = "budget $amount"; + + private static final String COMPLETE_MESSAGE = "Set the budget!"; + private static final String DUPLICATE_MESSAGE = "Updated the budget!"; + + + private enum SecondaryParam { + TAG("tag", "tags that we want a budget to be associated with"); + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + public BudgetCommand() { + super(name, description, usage, Stream.of(BudgetCommand.SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "amount")); + } + try { + BigDecimal amount = new BigDecimal(commandParams.getMainParam()); + BigDecimal scaledAmount = amount.setScale(2, RoundingMode.HALF_UP); + if (commandParams.containsParams(SecondaryParam.TAG.name)) { + String category = commandParams.getParam(SecondaryParam.TAG.name).toUpperCase(); + model.setCategoryBudget(category, amount); + } else { + model.setMonthlyBudget(scaledAmount); + } + storage.saveBudget(model.getBudget()); + + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_BUDGET_AMOUNT_INVALID, + commandParams.getMainParam())); + } + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.BUDGET); + } +} \ No newline at end of file diff --git a/src/main/java/duke/logic/command/Command.java b/src/main/java/duke/logic/command/Command.java new file mode 100644 index 0000000000..ea28c76ef1 --- /dev/null +++ b/src/main/java/duke/logic/command/Command.java @@ -0,0 +1,63 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.Map; + +/** + * Acts as the parent class of all commands in the command package, with fields meant to be + * populated by the individual commands. + */ +public abstract class Command { + private String name; + private String description; + private String usage; + private Map secondaryParams; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getUsage() { + return usage; + } + + public Map getSecondaryParams() { + return secondaryParams; + } + + /** + * Creates a new command object, with its name, description, usage and secondary parameters. + * + * @param name the name of the command to create. + * @param description the description of the command to create. + * @param usage the usage of the command to create. + * @param secondaryParams the secondary parameters of the command to create. + */ + protected Command(String name, String description, String usage, Map secondaryParams) { + this.name = name; + this.description = description; + this.usage = usage; + this.secondaryParams = secondaryParams; + } + + /** + * Executes the command with parameters given by the user. + * + * @param commandParams the parameters given by the user, parsed into a {@code CommandParams} object. + * @param model {@code Model} which the command should operate on. + * @param storage the storage of Duke++. + * @return feedback message of the operation result for display + * @throws DukeException If an error occurs during command execution. + */ + public abstract CommandResult execute(CommandParams commandParams, + Model model, Storage storage) throws DukeException; +} \ No newline at end of file diff --git a/src/main/java/duke/logic/command/ConfirmTentativeCommand.java b/src/main/java/duke/logic/command/ConfirmTentativeCommand.java new file mode 100644 index 0000000000..97235579a1 --- /dev/null +++ b/src/main/java/duke/logic/command/ConfirmTentativeCommand.java @@ -0,0 +1,65 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Expense; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ConfirmTentativeCommand extends Command { + /** + * Creates a new command object, with its name, description, usage and secondary parameters. + * + * @param name the name of the command to create. + * @param description the description of the command to create. + * @param usage the usage of the command to create. + * @param secondaryParams the secondary parameters of the command to create. + */ + private static final String name = "confirm"; + private static final String description = "confirm a tentative Expense"; + private static final String usage = "confirms $index, if it is a tentative task"; + private static final String COMPLETE_MESSAGE = "Confirmed the tentative expense!"; + + private enum SecondaryParam { + ; + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + public ConfirmTentativeCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + try { + int index = Integer.parseInt(commandParams.getMainParam()); + Expense expense = model.getExpenseExternalList().get(index - 1); + if (expense.isTentative()) { + model.deleteExpense(index); + expense.setTentative(false); + model.addExpense(expense); + } else { + throw new DukeException(index + " is not a tentative task!"); + } + } catch (NumberFormatException e) { + throw new DukeException("Please enter a number!"); + } catch (IndexOutOfBoundsException e) { + throw new DukeException("The index you've entered is out of range!"); + } + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + } + +} diff --git a/src/main/java/duke/logic/command/DeleteExpenseCommand.java b/src/main/java/duke/logic/command/DeleteExpenseCommand.java new file mode 100644 index 0000000000..47b4e86398 --- /dev/null +++ b/src/main/java/duke/logic/command/DeleteExpenseCommand.java @@ -0,0 +1,100 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a specified command as DeleteCommand by extending the {@code Command} class. + * Deletes the task with given index or specific command from the ExpenseList of Duke. + * Responses with the result. + */ +public class DeleteExpenseCommand extends Command { + private static final String name = "deleteExpense"; + private static final String description = "Deletes an Expense"; + private static final String usage = "delete $index"; + + private static final String COMPLETE_MESSAGE = "Deleted the expense(s)!"; + + private enum SecondaryParam { + ; + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Constructs a {@code DeleteCommand} object + * given the index of the task to be deleted. + */ + public DeleteExpenseCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + /** + + * + * @param + * @throws DukeException If the index given is out of range, invalid, or does not exist. + */ + + /** + * Lets the ExpenseList of Duke delete the task with the given index(s), or the entire task list and + * updates content of storage file according to new ExpenseList. + * + * Responses the result to user by using ui of Duke++ in ExpensePane. + * + * @param commandParams the parameters given by the user, parsed into a {@code CommandParams} object. + * @param model {@code Model} which the command should operate on. + * @param storage the storage of Duke++. + * @return CommandResult the result of the command, which is a completed logger message, in budget display pane + * @throws DukeException If the index given is out of range, invalid, or does not exist. + */ + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "index")); + } + + try { + if (commandParams.getMainParam().contains("-")) { + String[] index = commandParams.getMainParam().split("-"); + int difference = Integer.parseInt(index[1]) - Integer.parseInt(index[0]); + if (difference <= 0) { + throw new DukeException(String.format(DukeException.MESSAGE_DELETE_FORMAT_INVALID, + commandParams.getMainParam())); + } + if (Integer.parseInt(index[1]) > model.getExpenseList().internalSize()) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, + Integer.parseInt(index[1]))); + } else { + int counter = 0; + for (int i = Integer.parseInt(index[0]); counter <= difference; counter++) { + model.deleteExpense(i); + } + } + } else if (commandParams.getMainParam().equals("all")) { + model.clearExpense(); + } else { + model.deleteExpense((Integer.parseInt(commandParams.getMainParam()))); + } + storage.saveExpenseList(model.getExpenseList()); + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, + commandParams.getMainParam())); + } + } +} diff --git a/src/main/java/duke/logic/command/DeleteIncomeCommand.java b/src/main/java/duke/logic/command/DeleteIncomeCommand.java new file mode 100644 index 0000000000..b640786ecc --- /dev/null +++ b/src/main/java/duke/logic/command/DeleteIncomeCommand.java @@ -0,0 +1,91 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a specified command as DeleteIncomeCommand by extending the {@code Command} class. + * Deletes the task with given index or specific command from the IncomeList of Duke. + * Responses with the result. + */ +public class DeleteIncomeCommand extends Command { + private static final String name = "deleteIncome"; + private static final String description = "Deletes an Income"; + private static final String usage = "delete $index"; + + private static final String COMPLETE_MESSAGE = "Deleted the income(s)!"; + + private enum SecondaryParam { + ; + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Constructs a {@code DeleteIncomeCommand} object + * given the index of the task to be deleted. + */ + public DeleteIncomeCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + /** + * Lets the IncomeList of Duke delete the task with the given index(s), or the entire task list and + * updates content of storage file according to new IncomeList. + *

+ * Responses the result to user by using ui of Duke++ in Budget Pane. + * + * @param commandParams the parameters given by the user, parsed into a {@code CommandParams} object. + * @param model {@code Model} which the command should operate on. + * @param storage the storage of Duke++. + * @return CommandResult the result of the command, which is a completed logger message, with display in budget pane + * @throws DukeException If the index given is out of range, invalid, or does not exist. + */ + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "index")); + } + try { + if (commandParams.getMainParam().contains("-")) { + String[] index = commandParams.getMainParam().split("-"); + int difference = Integer.parseInt(index[1]) - Integer.parseInt(index[0]); + if (difference <= 0) { + throw new DukeException(String.format(DukeException.MESSAGE_DELETE_FORMAT_INVALID, + commandParams.getMainParam())); + } + if (Integer.parseInt(index[1]) > model.getIncomeList().internalSize()) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, + Integer.parseInt(index[1]))); + } else { + int counter = 0; + for (int i = Integer.parseInt(index[0]); counter <= difference; counter++) { + model.deleteIncome(i); + } + } + } else if (commandParams.getMainParam().equals("all")) { + model.clearIncome(); + } else { + model.deleteIncome((Integer.parseInt(commandParams.getMainParam()))); + } + storage.saveIncomeList(model.getIncomeList()); + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.BUDGET); + + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, + commandParams.getMainParam())); + } + } +} diff --git a/src/main/java/duke/logic/command/ExitCommand.java b/src/main/java/duke/logic/command/ExitCommand.java new file mode 100644 index 0000000000..6ba5328232 --- /dev/null +++ b/src/main/java/duke/logic/command/ExitCommand.java @@ -0,0 +1,55 @@ +package duke.logic.command; + +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Exits the application. + */ +public class ExitCommand extends Command { + private static final String name = "bye"; + private static final String description = "Exits Duke++"; + private static final String usage = "bye"; + + private static final String COMPLETE_MESSAGE = "Bye. Hope to see you again soon!"; + + /** + * Contains all secondary parameters used by {@code ExitCommand}. + * Here the {@code ExitCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates an ExitCommand, with its name, description, usage and secondary parameters. + */ + public ExitCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) { + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE, true); + } +} diff --git a/src/main/java/duke/logic/command/FilterExpenseCommand.java b/src/main/java/duke/logic/command/FilterExpenseCommand.java new file mode 100644 index 0000000000..9e3605fbbb --- /dev/null +++ b/src/main/java/duke/logic/command/FilterExpenseCommand.java @@ -0,0 +1,48 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FilterExpenseCommand extends Command { + private static final String name = "filterExpense"; + private static final String description = "Filter expenses according to a given criteria"; + private static final String usage = "filter $criteria"; + + private static final String COMPLETE_MESSAGE = "Filtered the expense!"; + + + private enum SecondaryParam { + ; + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Constructs an {@code FilterCommand} object. + */ + public FilterExpenseCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + String mainParam = commandParams.getMainParam(); + model.filterExpense(mainParam); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + } + +} diff --git a/src/main/java/duke/logic/command/GoToCommand.java b/src/main/java/duke/logic/command/GoToCommand.java new file mode 100644 index 0000000000..d3d88f171c --- /dev/null +++ b/src/main/java/duke/logic/command/GoToCommand.java @@ -0,0 +1,69 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Switches panes in the main window. + */ +public class GoToCommand extends Command { + + private static final String name = "goto"; + private static final String description = "go to a desired pane."; + private static final String usage = "goto $paneName"; + + private static final String COMPLETE_MESSAGE = ""; + + /** + * Contains all secondary parameters used by {@code GoToCommand}. + * Here the {@code GoToCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a GoToCommand, with its name, description, usage and secondary parameters. + */ + public GoToCommand() { + super(name, + description, + usage, + Stream.of(GoToCommand.SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description)) + ); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + String desiredPane = commandParams.getMainParam(); + CommandResult.DisplayedPane displayedPane; + try { + displayedPane = CommandResult.DisplayedPane.valueOf(desiredPane.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new DukeException(String.format(DukeException.MESSAGE_PANE_NAME_INVALID, desiredPane)); + } + + return new CommandResult(COMPLETE_MESSAGE, displayedPane); + } +} diff --git a/src/main/java/duke/logic/command/PlanBotCommand.java b/src/main/java/duke/logic/command/PlanBotCommand.java new file mode 100644 index 0000000000..1f486e0eb1 --- /dev/null +++ b/src/main/java/duke/logic/command/PlanBotCommand.java @@ -0,0 +1,59 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Expense; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class PlanBotCommand extends Command { + private static final String name = "plan"; + private static final String description = "a reply to planBot"; + private static final String usage = "sends the user input to planBot"; + + private enum SecondaryParam { + ; + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + + public PlanBotCommand() { + super(name, description, usage, Stream.of(PlanBotCommand.SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (commandParams.getMainParam() != null && commandParams.getMainParam().contains("export")) { + try { + for (String category : model.getRecommendedBudgetPlan().getPlanBudget().keySet()) { + model.setCategoryBudget(category, model.getRecommendedBudgetPlan().getPlanBudget().get(category)); + } + for (Expense recommendedExpense : model.getRecommendedBudgetPlan().getRecommendationExpenseList()) { + model.addExpense(recommendedExpense); + } + storage.saveExpenseList(model.getExpenseList()); + storage.saveBudget(model.getBudget()); + return new CommandResult("Exported successfully!", CommandResult.DisplayedPane.EXPENSE); + } catch (NullPointerException e) { + return new CommandResult("Nothing to export!", CommandResult.DisplayedPane.PLAN); + } + + } else { + model.processPlanInput(commandParams.getMainParam()); + storage.savePlanAttributes(model.getKnownPlanAttributes()); + return new CommandResult("PlanBot replied!", CommandResult.DisplayedPane.PLAN); + } + } +} diff --git a/src/main/java/duke/logic/command/SortExpenseCommand.java b/src/main/java/duke/logic/command/SortExpenseCommand.java new file mode 100644 index 0000000000..db508c6da5 --- /dev/null +++ b/src/main/java/duke/logic/command/SortExpenseCommand.java @@ -0,0 +1,61 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Sets the sorting criteria of visible expenses in expense tracker. + * Sorting criteria include amount, time and description. + * The default sorting criteria is time. + */ +public class SortExpenseCommand extends Command { + private static final String name = "sortExpense"; + private static final String description = "Sort expenses according to a given criteria"; + private static final String usage = "sort $criteria"; + + private static final String COMPLETE_MESSAGE = "Sorted the expense!"; + + /** + * Contains all secondary parameters used by {@code SortExpenseCommand}. + * Here the {@code SortExpenseCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a SortExpenseCommand, with its name, description, usage and secondary parameters. + */ + public SortExpenseCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + String mainParam = commandParams.getMainParam(); + model.sortExpense(mainParam); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + } +} diff --git a/src/main/java/duke/logic/command/ViewBudgetCommand.java b/src/main/java/duke/logic/command/ViewBudgetCommand.java new file mode 100644 index 0000000000..46c949b5eb --- /dev/null +++ b/src/main/java/duke/logic/command/ViewBudgetCommand.java @@ -0,0 +1,80 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a specified command as ViewBudgetCommand by extending the {@code Command} class. + * Adds various specified budget views into the BudgetView. + * Responses with the result. + */ + +public class ViewBudgetCommand extends Command { + private static final String name = "viewBudget"; + private static final String description = "sets a budgetView"; + private static final String usage = "budget $amount"; + + private static final String COMPLETE_MESSAGE = "Set the budget view!"; + + private enum SecondaryParam { + TAG("tag", "tags that we want a budget to be associated with"); + + private String name; + private String description; + + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a new command object, with its name, description, usage and secondary parameters. + */ + public ViewBudgetCommand() { + super(name, description, usage, Stream.of(ViewBudgetCommand.SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + /** + * Creates a new Income with the corresponding amount and description and + * saves it to storage file {@code income.txt}. + * + * Responses the result to user by using ui of Duke++ in BudgetPane. + * + * @param commandParams the parameters given by the user, parsed into a {@code CommandParams} object. + * @param model {@code Model} which the command should operate on. + * @param storage the storage of Duke++. + * @return CommandResult the result of the command, which is a completed logger message, in budget display pane + * @throws DukeException If the tag or pane of budget view is missing, or the specified pane is invalid + */ + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "pane")); + } + + if (!commandParams.containsParams(SecondaryParam.TAG.name)) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, "tag")); + } + + int view = Integer.parseInt(commandParams.getMainParam()); + + if (view < 1 || view > 6) { + throw new DukeException(String.format(DukeException.MESSAGE_BUDGET_VIEW_INVALID, view)); + } + + String category = commandParams.getParam(SecondaryParam.TAG.name).toUpperCase(); + model.setBudgetView(view, category); + + storage.saveBudgetView(model.getBudgetView()); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.BUDGET); + } +} \ No newline at end of file diff --git a/src/main/java/duke/logic/command/ViewExpenseCommand.java b/src/main/java/duke/logic/command/ViewExpenseCommand.java new file mode 100644 index 0000000000..d00a2a7439 --- /dev/null +++ b/src/main/java/duke/logic/command/ViewExpenseCommand.java @@ -0,0 +1,94 @@ +package duke.logic.command; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Sets the time scope of visible expenses in expense tracker. + * Scopes include day, week, month, year and all. + * + * By default, time scope day will only show expense on current day. + * With /previous parameters specified as n, day will show expenses n days ago. + * Same usage is also applicable to week, month and year. + */ +public class ViewExpenseCommand extends Command { + private static final String name = "viewExpense"; + private static final String description = "Change how expenses are displayed"; + private static final String usage = "view $criteria"; + + private static final String COMPLETE_MESSAGE = "Changed view scope of expenses!"; + private static final String EXCEPTION_WORD_TIME_SCOPE = "time scope"; + private static final String PARAM_PREVIOUS_NAME = "previous"; + private static final String PARAM_PREVIOUS_REDUNDANT = "/previous"; + private static final String PARAM_PREVIOUS_ALL = "all"; + private static final int DEFAULT_PREVIOUS_VALUE = 0; + + /** + * Contains all secondary parameters used by {@code ViewExpenseCommand}. + */ + private enum SecondaryParam { + PREVIOUS("previous", "the number of pages to move back by"); + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a ViewExpenseCommand, with its name, description, usage and secondary parameters. + */ + public ViewExpenseCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + EXCEPTION_WORD_TIME_SCOPE)); + } + + String mainParam = commandParams.getMainParam(); + + if (!commandParams.containsParams(PARAM_PREVIOUS_NAME)) { + model.viewExpense(mainParam, DEFAULT_PREVIOUS_VALUE); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + } + + if (commandParams.getMainParam().toLowerCase().equals(PARAM_PREVIOUS_ALL)) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_REDUNDANT, + PARAM_PREVIOUS_REDUNDANT)); + } + + int previous; + try { + previous = Integer.parseInt(commandParams.getParam(PARAM_PREVIOUS_NAME)); + } catch (NumberFormatException e) { + throw new DukeException(String.format( + DukeException.MESSAGE_EXPENSE_VIEW_NUMBER_INVALID, + commandParams.getParam(PARAM_PREVIOUS_NAME))); + } + + model.viewExpense(mainParam, previous); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.EXPENSE); + } +} diff --git a/src/main/java/duke/logic/command/payment/AddPaymentCommand.java b/src/main/java/duke/logic/command/payment/AddPaymentCommand.java new file mode 100644 index 0000000000..ec97608c17 --- /dev/null +++ b/src/main/java/duke/logic/command/payment/AddPaymentCommand.java @@ -0,0 +1,120 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Model; +import duke.model.payment.Payment; +import duke.storage.Storage; + +import java.io.IOException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Adds a payment to the the payments reminder. + */ +public class AddPaymentCommand extends Command { + + private static final String name = "addPayment"; + private static final String description = "Adds a new pending payment"; + private static final String usage = "addPayment $cost"; + + private static final String COMPLETE_MESSAGE = "Added the payment!"; + private static final String PARAM_AMOUNT_NAME = "amount"; + private static final int MAX_TAG_LENGTH = 30; + + /** + * Contains all secondary parameters used by {@code AddPaymentCommand}. + */ + private enum SecondaryParam { + DESCRIPTION("description", "a short description or name of the pending payment"), + DUE("due", "the due date of affording the payment"), + PRIORITY("priority", "the priority of the payment"), + RECEIVER("receiver", "the receiver of the payment"), + TAG("tag", "tag of the pending payment"); + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates an AddPaymentCommand, with its name, description, usage and secondary parameters. + */ + public AddPaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description)) + ); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + Payment.Builder paymentBuilder = new Payment.Builder(); + + // Amount is a compulsory field and also the main param. + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + PARAM_AMOUNT_NAME)); + } + + paymentBuilder.setAmount(commandParams.getMainParam()); + + // Description is a compulsory field. + if (!commandParams.containsParams(SecondaryParam.DESCRIPTION.name)) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + SecondaryParam.DESCRIPTION.name)); + } + + paymentBuilder.setDescription(commandParams.getParam(SecondaryParam.DESCRIPTION.name)); + + // Due is a compulsory field. + if (!commandParams.containsParams(SecondaryParam.DUE.name)) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + SecondaryParam.DUE.name)); + } + + paymentBuilder.setDue(commandParams.getParam(SecondaryParam.DUE.name)); + + // Priority is a optional field. + if (commandParams.containsParams(SecondaryParam.PRIORITY.name)) { + paymentBuilder.setPriority(commandParams.getParam(SecondaryParam.PRIORITY.name)); + } + + // Receiver is a optional field. + if (commandParams.containsParams(SecondaryParam.RECEIVER.name)) { + paymentBuilder.setReceiver(commandParams.getParam(SecondaryParam.RECEIVER.name)); + } + + // Tag is a optional field. + if (commandParams.containsParams(SecondaryParam.TAG.name)) { + String tag = commandParams.getParam(SecondaryParam.TAG.name); + // The length of tag shouldn't exceed 30 chars. + if (tag.length() > MAX_TAG_LENGTH) { + throw new DukeException(DukeException.MESSAGE_TAG_TOO_LONG); + } + paymentBuilder.setTag(commandParams.getParam(SecondaryParam.TAG.name)); + } + + model.addPayment(paymentBuilder.build()); + + try { + storage.savePaymentList(model.getPaymentList()); + } catch (IOException e) { + throw new DukeException(DukeException.MESSAGE_PAYMENT_SAVE_FAILED); + } + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/command/payment/ChangePaymentCommand.java b/src/main/java/duke/logic/command/payment/ChangePaymentCommand.java new file mode 100644 index 0000000000..a64a6eee28 --- /dev/null +++ b/src/main/java/duke/logic/command/payment/ChangePaymentCommand.java @@ -0,0 +1,119 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Model; +import duke.model.payment.Payment; +import duke.storage.Storage; + +import java.io.IOException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Changes details of a payment identified using it's displayed index in the payment reminder. + */ +public class ChangePaymentCommand extends Command { + private static final String name = "changePayment"; + private static final String description = "Changes a pending payment"; + private static final String usage = "changePayment $index"; + + private static final String COMPLETE_MESSAGE = "Changed the payment!"; + private static final String EXCEPTION_WORD_INDEX = "index"; + private static final int MAX_TAG_LENGTH = 30; + + /** + * Contains all secondary parameters used by {@code ChangePaymentCommand}. + */ + private enum SecondaryParam { + DESCRIPTION("description", "a short description or name of the pending payment"), + DUE("due", "the due date of affording the payment"), + PRIORITY("priority", "the priority of the payment"), + RECEIVER("receiver", "the receiver of the payment"), + AMOUNT("amount", "the money of payment"), + TAG("tag", "remark of the pending payment"); + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a ChangePaymentCommand, with its name, description, usage and secondary parameters. + */ + public ChangePaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description)) + ); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + EXCEPTION_WORD_INDEX)); + } + + int index; + String mainParam = commandParams.getMainParam(); + try { + index = Integer.parseInt(mainParam); + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, mainParam)); + } + + // Constructs a builder with same fields as the target payment for modification on fields. + Payment.Builder paymentBuilder = new Payment.Builder(model.getPayment(index)); + + if (commandParams.containsParams(SecondaryParam.AMOUNT.name)) { + paymentBuilder.setAmount(commandParams.getParam(SecondaryParam.AMOUNT.name)); + } + + if (commandParams.containsParams(SecondaryParam.DESCRIPTION.name)) { + paymentBuilder.setDescription(commandParams.getParam(SecondaryParam.DESCRIPTION.name)); + } + + if (commandParams.containsParams(SecondaryParam.DUE.name)) { + paymentBuilder.setDue(commandParams.getParam(SecondaryParam.DUE.name)); + } + + if (commandParams.containsParams(SecondaryParam.PRIORITY.name)) { + paymentBuilder.setPriority(commandParams.getParam(SecondaryParam.PRIORITY.name)); + } + + if (commandParams.containsParams(SecondaryParam.RECEIVER.name)) { + paymentBuilder.setReceiver(commandParams.getParam(SecondaryParam.RECEIVER.name)); + } + + if (commandParams.containsParams(SecondaryParam.TAG.name)) { + String tag = commandParams.getParam(ChangePaymentCommand.SecondaryParam.TAG.name); + // The length of tag should not exceed 30 chars. + if (tag.length() > MAX_TAG_LENGTH) { + throw new DukeException(DukeException.MESSAGE_TAG_TOO_LONG); + } + paymentBuilder.setTag(commandParams.getParam(SecondaryParam.TAG.name)); + } + + model.setPayment(index, paymentBuilder.build()); + try { + storage.savePaymentList(model.getPaymentList()); + } catch (IOException e) { + throw new DukeException(DukeException.MESSAGE_PAYMENT_SAVE_FAILED); + } + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/command/payment/DeletePaymentCommand.java b/src/main/java/duke/logic/command/payment/DeletePaymentCommand.java new file mode 100644 index 0000000000..515fc1d2d1 --- /dev/null +++ b/src/main/java/duke/logic/command/payment/DeletePaymentCommand.java @@ -0,0 +1,79 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Model; +import duke.storage.Storage; + +import java.io.IOException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Deletes a payment identified using it's displayed index from the payments reminder. + */ +public class DeletePaymentCommand extends Command { + + private static final String name = "deletePayment"; + private static final String description = "Deletes a Payment"; + private static final String usage = "deletePayment $index"; + + private static final String COMPLETE_MESSAGE = "Deleted the payment!"; + private static final String EXCEPTION_WORD_INDEX = "index"; + + /** + * Contains all secondary parameters used by {@code DeletePaymentCommand}. + * Here the {@code DeletePaymentCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a DeletePaymentCommand, with its name, description, usage and secondary parameters. + */ + public DeletePaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + + if (!commandParams.containsMainParam()) { + throw new DukeException( + String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, EXCEPTION_WORD_INDEX)); + } + + String mainParam = commandParams.getMainParam(); + try { + model.removePayment(Integer.parseInt(mainParam)); + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, mainParam)); + } + + try { + storage.savePaymentList(model.getPaymentList()); + } catch (IOException e) { + throw new DukeException(DukeException.MESSAGE_PAYMENT_SAVE_FAILED); + } + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/command/payment/DonePaymentCommand.java b/src/main/java/duke/logic/command/payment/DonePaymentCommand.java new file mode 100644 index 0000000000..86d9c0f364 --- /dev/null +++ b/src/main/java/duke/logic/command/payment/DonePaymentCommand.java @@ -0,0 +1,90 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Expense; +import duke.model.Model; +import duke.model.payment.Payment; +import duke.storage.Storage; + +import java.io.IOException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Finishes a payment identified using it's displayed index in the payments reminder. + * The finished payment will be automatically recorded in the expense tracker. + */ +public class DonePaymentCommand extends Command { + + private static final String name = "donePayment"; + private static final String description = "Finish a Payment and add to expenseList"; + private static final String usage = "donePayment $index"; + + private static final String COMPLETE_MESSAGE = "Finished the payment!"; + private static final String EXCEPTION_WORD_INDEX = "index"; + + /** + * Contains all secondary parameters used by {@code DonePaymentCommand}. + * Here the {@code DonePaymentCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a DonePaymentCommand, with its name, description, usage and secondary parameters. + */ + public DonePaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + EXCEPTION_WORD_INDEX)); + } + + String mainParam = commandParams.getMainParam(); + int targetIndex; + try { + targetIndex = Integer.parseInt(mainParam); + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_NUMBER_FORMAT_INVALID, mainParam)); + } + + Payment payment = model.getPayment(targetIndex); // Gets the finished payment. + Expense.Builder expenseBuilder = new Expense.Builder(payment); // Constructs an expense based on payment + + model.addExpense(expenseBuilder.build()); + storage.saveExpenseList(model.getExpenseList()); + + model.removePayment(targetIndex); + try { + storage.savePaymentList(model.getPaymentList()); + } catch (IOException e) { + throw new DukeException(DukeException.MESSAGE_PAYMENT_SAVE_FAILED); + } + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/command/payment/FilterPaymentCommand.java b/src/main/java/duke/logic/command/payment/FilterPaymentCommand.java new file mode 100644 index 0000000000..5618de6a7b --- /dev/null +++ b/src/main/java/duke/logic/command/payment/FilterPaymentCommand.java @@ -0,0 +1,90 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Sets the time scope of visible payments in payment reminder. + * Scopes include overdue payments, coming payments in current week, coming payments in current month + * and all payments without limit. + */ +public class FilterPaymentCommand extends Command { + + private static final String name = "viewPayment"; + private static final String description = "View coming Payments in future"; + private static final String usage = "viewPayment $timeScope"; + + private static final String OVERDUE_SCOPE = "overdue"; + private static final String WEEK_SCOPE = "week"; + private static final String MONTH_SCOPE = "month"; + private static final String ALL_SCOPE = "all"; + + private static final String COMPLETE_MESSAGE = "Here are payments!"; + private static final String EXCEPTION_WORD_TIME_SCOPE = "time scope"; + + /** + * Contains all secondary parameters used by {@code FilterPaymentCommand}. + * Here the {@code FilterPaymentCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a FilterPaymentCommand, with its name, description, usage and secondary parameters. + */ + public FilterPaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + EXCEPTION_WORD_TIME_SCOPE)); + } + + String timeScope = commandParams.getMainParam().toLowerCase(); // case insensitive + + switch (timeScope) { + case OVERDUE_SCOPE: + model.setOverduePredicate(); + break; + case WEEK_SCOPE: + model.setWeekPredicate(); + break; + case MONTH_SCOPE: + model.setMonthPredicate(); + break; + case ALL_SCOPE: + model.setAllPredicate(); + break; + default: + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_SCOPE_INVALID, timeScope)); + } + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/command/payment/SearchPaymentCommand.java b/src/main/java/duke/logic/command/payment/SearchPaymentCommand.java new file mode 100644 index 0000000000..1621909566 --- /dev/null +++ b/src/main/java/duke/logic/command/payment/SearchPaymentCommand.java @@ -0,0 +1,68 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Searches and lists all payments in payment reminder + * whose description, receiver or tag contains the given keyword. + * Keyword matching is case insensitive. + */ +public class SearchPaymentCommand extends Command { + + private static final String name = "searchPayment"; + private static final String description = "searches payment with given keywords"; + private static final String usage = "searchPayment $keyword"; + + private static final String COMPLETE_MESSAGE = "Here are searching results!"; + private static final String EXCEPTION_WORD_KEYWORD = "keyword"; + + /** + * Contains all secondary parameters used by {@code SearchPaymentCommand}. + * Here the {@code SearchPaymentCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a SearchPaymentCommand, with its name, description, usage and secondary parameters. + */ + public SearchPaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + EXCEPTION_WORD_KEYWORD)); + } + + model.setSearchKeyword(commandParams.getMainParam()); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/command/payment/SortPaymentCommand.java b/src/main/java/duke/logic/command/payment/SortPaymentCommand.java new file mode 100644 index 0000000000..90b8ea72ed --- /dev/null +++ b/src/main/java/duke/logic/command/payment/SortPaymentCommand.java @@ -0,0 +1,68 @@ +package duke.logic.command.payment; + +import duke.exception.DukeException; +import duke.logic.CommandParams; +import duke.logic.CommandResult; +import duke.logic.command.Command; +import duke.model.Model; +import duke.storage.Storage; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Sets the sorting criteria of visible payments in payment reminder. + * Sorting criteria include due, amount and priority. + * The default sorting criteria is due. + */ +public class SortPaymentCommand extends Command { + + private static final String name = "sortPayment"; + private static final String description = "Sort Payments with given criteria"; + private static final String usage = "sortPayment $sortCriteria"; + + private static final String COMPLETE_MESSAGE = "Payments are sorted!"; + private static final String EXCEPTION_WORD_SORTING_CRITERIA = "sorting criteria"; + + /** + * Contains all secondary parameters used by {@code SortPaymentCommand}. + * Here the {@code SortPaymentCommand} does not demand secondary parameters. + */ + private enum SecondaryParam { + ; + + private String name; + private String description; + + /** + * Constructs a {@code SecondaryParam} with its name and usage. + * + * @param name The name of the secondary parameter. + * @param description The usage of this parameter. + */ + SecondaryParam(String name, String description) { + this.name = name; + this.description = description; + } + } + + /** + * Creates a SortPaymentCommand, with its name, description, usage and secondary parameters. + */ + public SortPaymentCommand() { + super(name, description, usage, Stream.of(SecondaryParam.values()) + .collect(Collectors.toMap(s -> s.name, s -> s.description))); + } + + @Override + public CommandResult execute(CommandParams commandParams, Model model, Storage storage) throws DukeException { + if (!commandParams.containsMainParam()) { + throw new DukeException(String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING, + EXCEPTION_WORD_SORTING_CRITERIA)); + } + + model.setPaymentSortingCriteria(commandParams.getMainParam()); + + return new CommandResult(COMPLETE_MESSAGE, CommandResult.DisplayedPane.PAYMENT); + } +} diff --git a/src/main/java/duke/logic/parser/Parser.java b/src/main/java/duke/logic/parser/Parser.java new file mode 100644 index 0000000000..4f801cf144 --- /dev/null +++ b/src/main/java/duke/logic/parser/Parser.java @@ -0,0 +1,62 @@ +package duke.logic.parser; + +import duke.exception.DukeException; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Parses the command line from user input to tokens and + * packages the tokens to {@code Command} object. + */ +public class Parser { + + private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm dd/MM/yyyy"); + + /** + * Converts a LocalDateTime to a user readable string. + * + * @param localDateTime LocalDateTime object that we wish to convert + * @return String that is a formatted date and time + */ + public static String formatTime(LocalDateTime localDateTime) { + return localDateTime.format(dateTimeFormatter); + } + + /** + * Converts a {@code String} to a {@code LocalDateTime}. + * + * @param string {@code String} to convert. + * @return {@code LocalDateTime} corresponding to the string. + * @throws DukeException if the string cannot be parsed into a {@code LocalDateTime} object. + */ + public static LocalDateTime parseTime(String string) throws DukeException { + try { + return LocalDateTime.parse(string, dateTimeFormatter); + } catch (DateTimeParseException e) { + throw new DukeException(String.format(DukeException.MESSAGE_PARSER_TIME_INVALID, string)); + } + } + + /** + * Returns a formatted BigDecimal representing Money. + * @param string String we want to format + * @return a formatted BigDecimal representing Money. + * @throws DukeException if {@code string} is null or has invalid format + */ + public static BigDecimal parseMoney(String string) throws DukeException { + try { + double amountDouble = Double.parseDouble(string); + BigDecimal amount = BigDecimal.valueOf(amountDouble); + BigDecimal scaledAmount = amount.setScale(2, RoundingMode.HALF_EVEN); + return scaledAmount; + } catch (NumberFormatException | NullPointerException e) { + throw new DukeException(String.format(DukeException.MESSAGE_EXPENSE_AMOUNT_INVALID, string)); + } + + } +} + diff --git a/src/main/java/duke/logic/util/AutoCompleter.java b/src/main/java/duke/logic/util/AutoCompleter.java new file mode 100644 index 0000000000..9cd530123f --- /dev/null +++ b/src/main/java/duke/logic/util/AutoCompleter.java @@ -0,0 +1,459 @@ +package duke.logic.util; + +import duke.commons.LogsCenter; +import duke.logic.command.AddExpenseCommand; +import duke.logic.command.Command; +import duke.logic.command.ConfirmTentativeCommand; +import duke.logic.command.DeleteExpenseCommand; +import duke.logic.command.ExitCommand; +import duke.logic.command.FilterExpenseCommand; +import duke.logic.command.GoToCommand; +import duke.logic.command.PlanBotCommand; +import duke.logic.command.SortExpenseCommand; +import duke.logic.command.ViewExpenseCommand; +import duke.logic.command.BudgetCommand; +import duke.logic.command.ViewBudgetCommand; +import duke.logic.command.payment.AddPaymentCommand; +import duke.logic.command.payment.ChangePaymentCommand; +import duke.logic.command.payment.DeletePaymentCommand; +import duke.logic.command.payment.DonePaymentCommand; +import duke.logic.command.payment.FilterPaymentCommand; +import duke.logic.command.payment.SearchPaymentCommand; +import duke.logic.command.payment.SortPaymentCommand; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; + +// Solution below adapted from Liu Jiajun (AY1920S1 T12-3) +// https://github.com/AY1920S1-CS2113T-T12-3/main/blob/master/src/main/java/duke/logic/parser/commons/AutoCompleter.java + +/** + * Provides a auto-complete to what the user has typed in userInput when TAB key is pressed. + * It can complete a commandName or iterate through all suitable commandNames. + * It can also produce a parameter name, complete a parameter name and iterate through all suitable parameter names. + * If the given input ends with space, autoCompleter will produce a parameter name. + * If the given input doesn't end with space and is different from last complement, + * autoCompleter will complete the fragment after the last space, which can be a commandName or a parameter. + * Instead, if the given input is same as last complement, + * autoCompleter will iterate through other suitable complements in the list. + *

+ * The mechanism of complement and iteration is firstly getting a suitable token(complement), + * which can be either a complete commandName or parameter, or the fragment after the last space in input + * in case that no suitable complements can be applied to current input. + * Then auto-completer replaces the fragment after the last space of input with this suitable complement, + * and lastly returns the modified input to userInput TextField. + */ +public class AutoCompleter { + + private static final Logger logger = LogsCenter.getLogger(AutoCompleter.class); + + /** + * Works as a detector of an empty string. + * A keyword used to decide the purpose of complement. + **/ + private static final String EMPTY_STRING = ""; + + /** + * Works as a space regex to tokenize the input. + * A keyword used to decide the purpose of complement. + */ + private static final String SPACE = " "; + + /** + * Works as a indicator of secondary parameter. + * A keyword used to decide the purpose of complement. + */ + private static final String PARAMETER_INDICATOR = "/"; + + /** + * Indicates the first element of a list. + */ + private static final int INITIAL_INDEX = 0; + + /** + * Helps conversion between 0-based and 1-based indexes. + */ + private static final int BASE_CONVERSION = 1; + + /** + * The list storing all commands' names. + * Works as the information source to complete command names. + **/ + private final List allCommandNames; + + /** + * Maps each commandNames to their respective collections of secondaryParams' names. + **/ + private final HashMap> allSecondaryParams; + + /** + * The most recent complement provided by auto-completer. + **/ + private String lastComplement; + + /** + * The content inside the userInput TextField when TAB key is pressed. + **/ + private String fromInput; + + /** + * Number of tokens of {@code fromInput}. + **/ + private int numberOfTokens; + + /** + * The starting index of the fragment after the last space in {@code fromInput}. + **/ + private int startIndexOfLastFragment; + + /** + * All suitable tokens that can replace the fragment after the last space of {@code fromInput}. + **/ + private List complementList; + + /** + * The index used to iterate through {@code complementList}. + **/ + private int iteratingIndex; + + /** + * A supplier that supplies streams of all command classes. + * It works as the source of all complements information. + **/ + private static final Supplier> COMMANDS = () -> Stream.of( + new AddExpenseCommand(), + new DeleteExpenseCommand(), + new ConfirmTentativeCommand(), + new ExitCommand(), + new FilterExpenseCommand(), + new SortExpenseCommand(), + new ViewExpenseCommand(), + new GoToCommand(), + new PlanBotCommand(), + new AddPaymentCommand(), + new ChangePaymentCommand(), + new DeletePaymentCommand(), + new FilterPaymentCommand(), + new SearchPaymentCommand(), + new SortPaymentCommand(), + new DonePaymentCommand(), + new BudgetCommand(), + new ViewBudgetCommand() + ); + + /** + * Purposes of complement. + */ + private enum Purpose { + COMPLETE_COMMAND_NAME, + PRODUCE_PARAMETER, + COMPLETE_PARAMETER, + ITERATE, + NOT_DOABLE; + } + + /** + * Constructs a auto-completer. + * All commandNames are stored in {@code allCommandNames}. + * All collections of secondaryParams of each command are stored in {@code allSecondaryParams}. + * The {@code complementList} is initialized as an empty ArrayList. + * The {@code lastComplement} is initialized as an empty String. + */ + public AutoCompleter() { + allCommandNames = COMMANDS.get().map(Command::getName).collect(Collectors.toList()); + assert !allCommandNames.isEmpty(); + + allSecondaryParams = new HashMap>(); + COMMANDS.get().forEach(command -> { + Set secondaryParamSet = command.getSecondaryParams().keySet(); + List secondaryParamList = new ArrayList(secondaryParamSet); + // maps commandName to collections of its all secondaryParams' names + allSecondaryParams.put(command.getName(), secondaryParamList); + }); + assert !allSecondaryParams.isEmpty(); + + complementList = new ArrayList(); + lastComplement = EMPTY_STRING; + + logger.info("Auto Completer is constructed."); + } + + + /** + * Receives the content in userInput TextField when TAB key is pressed. + * Stores it in {@code fromInput}. + * + * @param fromInput The content in userInput TextField. + */ + public void receiveText(String fromInput) { + this.fromInput = requireNonNull(fromInput); + logger.info("Received text for auto-completer."); + } + + /** + * Replaces the fragment after the last space of {@code fromInput} with complement token. + * and stores this modified String in {@code lastComplement}. + * This modified String is the full complement and will be set as text in userInput TextField. + *

+ * i.e full complement = fromInput - fragment after the last space + complement token. + * + * @return Full complement going to be set in userInput TextField. + */ + public String getFullComplement() { + Purpose purpose = getPurpose(); + requireNonNull(purpose); + + String complement = getComplement(purpose); + lastComplement = getTailoredInput() + complement; + return lastComplement; + } + + /** + * Decides the {@code purpose} with various criteria. + */ + private Purpose getPurpose() { + if (isEmpty()) { + return Purpose.NOT_DOABLE; + } + + if (isSameAsLastComplement()) { + return Purpose.ITERATE; + } + + if (!hasValidCommandName()) { + if (numberOfTokens > 1 || endsWithSpace()) { + return Purpose.NOT_DOABLE; + } + return Purpose.COMPLETE_COMMAND_NAME; + } + + if (endsWithSpace()) { + return Purpose.PRODUCE_PARAMETER; + } + + if (inUncompletedParameter()) { + return Purpose.COMPLETE_PARAMETER; + } + + if (numberOfTokens == 1) { + return Purpose.COMPLETE_COMMAND_NAME; + } + + logger.info("The original input itself is already complete."); + return Purpose.NOT_DOABLE; + } + + /** + * Gets the complement token. + * If the {@code purpose} is not {@code ITERATE}, + * it firstly generates {@code complementList} containing all suitable complements(tokens) + * and chooses the first element as complement token. + * If the {@code purpose} is {@code ITERATE}, + * the {@code iteratingIndex} increases by one and the element at {@code iteratingIndex} + * in the existing {@code complementList} will be chose as complement token. + * + * @return The complement token. + */ + private String getComplement(Purpose purpose) { + switch (purpose) { + case COMPLETE_COMMAND_NAME: + completeCommandNameComplements(); + iteratingIndex = INITIAL_INDEX; + break; + + case PRODUCE_PARAMETER: + produceParameterComplements(); + iteratingIndex = INITIAL_INDEX; + break; + + case COMPLETE_PARAMETER: + completeParameterComplements(); + iteratingIndex = INITIAL_INDEX; + break; + + case ITERATE: + iterateIndex(); + break; + + case NOT_DOABLE: + complementList.clear(); + break; + + default: + logger.warning("Purpose takes unexpected value."); + break; + } + + if (complementList.isEmpty()) { // return original last token if there's no suitable complement + return getFragmentAfterLastSpace(); + } + + assert iteratingIndex < complementList.size(); + + return complementList.get(iteratingIndex); + } + + /** + * Cuts off the fragment after the last space from {@code fromInput}. + * + * @return A tailored input without the last fragment. + */ + private String getTailoredInput() { + return fromInput.substring(INITIAL_INDEX, startIndexOfLastFragment); + } + + /** + * Tests whether the given input is same as the most recent complement. + * This is a criteria to help decide purpose. + * + * @return True if {@code fromInput} is same as {@code lastComplement} and false otherwise. + */ + private boolean isSameAsLastComplement() { + return fromInput.equals(lastComplement); + } + + /** + * Tests whether the given input is empty. + * This is a criteria to help decide purpose. + * + * @return True if {@code fromInput} is an empty String and false otherwise. + */ + private boolean isEmpty() { + return fromInput.trim().equals(EMPTY_STRING); + } + + /** + * Tests whether the given input ends with a space. + * This is a criteria to help decide purpose. + * + * @return True if {@code fromInput} ends with space and false otherwise. + */ + private boolean endsWithSpace() { + return fromInput.endsWith(SPACE); + } + + /** + * Tests whether the fragment after the last space of given input starts with {@code PARAMETER_INDICATOR}. + * This is a criteria to help decide purpose. + * + * @return True if the fragment after the last space starts with {@code PARAMETER_INDICATOR}. + */ + private boolean inUncompletedParameter() { + return getFragmentAfterLastSpace().startsWith(PARAMETER_INDICATOR); + } + + /** + * Gets the first token of the given input. + * Updates {@code numberOfTokens} after the given input is tokenized. + * + * @return The first token of {@code fromInput} + */ + private String getCommandName() { + List tokens = Arrays.asList(fromInput.split(SPACE)); + // Gets rid of empty tokens + tokens = tokens.stream().filter(s -> !s.equals(EMPTY_STRING)).collect(Collectors.toList()); + // Gets rid of extra space + tokens = tokens.stream().map(String::trim).collect(Collectors.toList()); + numberOfTokens = tokens.size(); + + return tokens.get(INITIAL_INDEX); + } + + /** + * Tests whether the first token of given input is a valid commandName. + * This is a criteria to help decide purpose. + * + * @return True if the first token of {@code fromInput} is a valid commandName and false otherwise. + */ + private boolean hasValidCommandName() { + return allCommandNames.contains(getCommandName()); + } + + /** + * Gets the fragment after the last space of given input. + * Updates {@code startIndexOfLastFragment}. + * + * @return The last fragment of {@code fromInput}. + */ + private String getFragmentAfterLastSpace() { + int index = fromInput.length() - BASE_CONVERSION; + + boolean isSpaceNotReached; + + // gets the position of the last space in input + while (index >= INITIAL_INDEX) { + isSpaceNotReached = (fromInput.charAt(index) != SPACE.charAt(INITIAL_INDEX)); + + if (isSpaceNotReached) { + index--; + } else { + break; + } + } + + startIndexOfLastFragment = index + BASE_CONVERSION; + assert startIndexOfLastFragment <= fromInput.length(); + + return fromInput.substring(startIndexOfLastFragment); + } + + /** + * Fills the {@code complementList} with all complete versions of current commandName. + * This is called when purpose is {@code COMPLETE_COMMAND_NAME}. + */ + private void completeCommandNameComplements() { + String unCompletedCommandName = getFragmentAfterLastSpace(); + complementList = allCommandNames.stream() + .filter(s -> s.startsWith(unCompletedCommandName)).collect(Collectors.toList()); + + logger.info("ComplementList for command names is constructed."); + } + + /** + * Fills the {@code complementList} with all complete versions of current secondaryParameter. + * This is called when purpose is {@code COMPLETE_PARAMETER}. + */ + private void completeParameterComplements() { + String unCompletedParameter = getFragmentAfterLastSpace().substring(1); // gets rid of "/" at index 0 + List options = allSecondaryParams.get(getCommandName()); + List usableParameters = options.stream() + .filter(s -> s.startsWith(unCompletedParameter)).collect(Collectors.toList()); + complementList = usableParameters.stream().map(s -> PARAMETER_INDICATOR + s).collect(Collectors.toList()); + + logger.info("ComplementList for parameter names is constructed."); + } + + /** + * Fills the {@code complementList} with all secondaryParameters belonging to commandName. + * This is called when purpose is {@code PRODUCE_PARAMETER}. + */ + private void produceParameterComplements() { + getFragmentAfterLastSpace(); + List options = allSecondaryParams.get(getCommandName()); + complementList = options.stream().map(s -> PARAMETER_INDICATOR + s).collect(Collectors.toList()); + + logger.info("ComplementList producing parameter names is constructed."); + } + + /** + * Iterates the index of {@code complementList} by adding it by one. + * This is called when purpose is {@code ITERATE}. + */ + private void iterateIndex() { + iteratingIndex++; + if (iteratingIndex >= complementList.size()) { + iteratingIndex = INITIAL_INDEX; + } + + logger.info("Index has been iterated."); + } + +} diff --git a/src/main/java/duke/logic/util/InputHistory.java b/src/main/java/duke/logic/util/InputHistory.java new file mode 100644 index 0000000000..5aa7902c1a --- /dev/null +++ b/src/main/java/duke/logic/util/InputHistory.java @@ -0,0 +1,106 @@ +package duke.logic.util; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * Enables the user to iterate through previous inputs one by one. + * Pressing UP key once shows one input earlier. + * Pressing more times shows much earlier inputs until the earliest input is reached. + * Pressing DOWN key once traverses back to recent input. + * Pressing more times shows more recent inputs until the most recent input is reached. + * While the most recent input displayed, pressing DOWN Key will clear the textField. + */ +public class InputHistory { + + private static final int INITIAL_INDEX = 0; + private static final String EMPTY_STRING = ""; + + private List inputHistory; + private int iteratingIndex; + + /** + * Creates an {@code InputHistory} to record user commands sent in textField of mainWindow. + */ + public InputHistory() { + inputHistory = new ArrayList(); + iteratingIndex = INITIAL_INDEX; + } + + /** + * Adds the input command from textField into InputHistory after it is executed. + * + * @param newInput The input command to be recorded. + */ + public void add(String newInput) { + requireNonNull(newInput); + if (newInput.isBlank()) { + return; + } + + inputHistory.add(newInput); + iteratingIndex = inputHistory.size(); + } + + /** + * Gets the one earlier command. + * + * @return The earlier command as {@code String} + */ + public String getLastInput() { + if (inputHistory.isEmpty()) { + return EMPTY_STRING; + } + + if (isAbleToLast()) { + iteratingIndex--; + } + assert iteratingIndex >= 0; + + return inputHistory.get(iteratingIndex); + } + + /** + * Gets the one later command. + * + * @return The later command as {@code String} + */ + public String getNextInput() { + if (inputHistory.isEmpty()) { + return EMPTY_STRING; + } + + if (isAbleToNext()) { + iteratingIndex++; + } + assert iteratingIndex < inputHistory.size() + 1; + + if (iteratingIndex == inputHistory.size()) { + return EMPTY_STRING; + } + + return inputHistory.get(iteratingIndex); + } + + /** + * Tests whether there are still available earlier commands. + * + * @return True if earlier commands can be found in the record and false otherwise. + */ + private boolean isAbleToLast() { + return iteratingIndex >= 1; + } + + /** + * Tests whether there are still available later commands. + * + * @return True if later commands can be found in the record and false otherwise. + */ + private boolean isAbleToNext() { + return iteratingIndex < inputHistory.size(); + } +} + + diff --git a/src/main/java/duke/model/Budget.java b/src/main/java/duke/model/Budget.java new file mode 100644 index 0000000000..acbee8c0f3 --- /dev/null +++ b/src/main/java/duke/model/Budget.java @@ -0,0 +1,115 @@ +package duke.model; + +import duke.commons.LogsCenter; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.logging.Logger; + +public class Budget { + + private static final Logger logger = LogsCenter.getLogger(Budget.class); + + private BigDecimal monthlyBudget; + /** + * Maps a category to the budget set for the category. + */ + private Map budgetCategory; + + private ObservableList budgetObservableList; + + /** + * Constructor for Budget Object. + * @param monthlyBudget a BigDecimal the overall monthly budget + * @param budgetCategory A map of String category to the BigDecimal amount + */ + public Budget(BigDecimal monthlyBudget, Map budgetCategory) { + this.monthlyBudget = monthlyBudget; + this.budgetCategory = budgetCategory; + budgetObservableList = FXCollections.observableArrayList(); + updateBudgetObservableList(); + } + + /** + * Setter method for monthlyBudget. + * + * @param monthlyBudget BigDecimal budget set for each month + */ + public void setMonthlyBudget(BigDecimal monthlyBudget) { + this.monthlyBudget = monthlyBudget; + updateBudgetObservableList(); + } + + /** + * Gets a string value for monthlyBudget. + * + * @return a String of the monthly budget + */ + public String getMonthlyBudgetString() { + return monthlyBudget.toPlainString(); + } + + /** + * Gets a BigDecimal value for monthlyBudget. + * + * @return a BigDecimal of the monthly budget + */ + public BigDecimal getMonthlyBudget() { + return monthlyBudget; + } + + /** + * Sets budget to a given category. + * + * @param category the String tag specified that we want to set a budget for + * @param budget a BigDecimal amount for the budget we want to set + */ + public void setCategoryBudget(String category, BigDecimal budget) { + budgetCategory.put(category, budget); + updateBudgetObservableList(); + } + + /** + * Gets the difference between the monthly budget and the total expenses spent. + * + * @param total the BigDecimal total expenditure from expenseList + * @return BigDecimal value fo the difference + */ + public BigDecimal getRemaining(BigDecimal total) { + return monthlyBudget.subtract(total); + } + + /** + * Gets the budget of a specific category. + * + * @param category the String of the specific category + * @return BigDecimal value of the budget set for the category + */ + public BigDecimal getBudgetTag(String category) { + if (budgetCategory.containsKey(category)) { + return budgetCategory.get(category); + } else { + return BigDecimal.ZERO; + } + } + + public Map getBudgetCategory() { + return budgetCategory; + } + + public ObservableList getBudgetObservableList() { + return budgetObservableList; + } + + private void updateBudgetObservableList() { + budgetObservableList.clear(); + budgetObservableList.add("MONTHLY BUDGET: $" + monthlyBudget.toString()); + for (String category : budgetCategory.keySet()) { + budgetObservableList.add(category + ": $" + budgetCategory.get(category)); + } + logger.info("Size of budgetObserverList: $" + budgetObservableList.size()); + } + +} diff --git a/src/main/java/duke/model/BudgetView.java b/src/main/java/duke/model/BudgetView.java new file mode 100644 index 0000000000..dbf413c34f --- /dev/null +++ b/src/main/java/duke/model/BudgetView.java @@ -0,0 +1,34 @@ +package duke.model; + +import java.util.Map; + +public class BudgetView { + + /** + * Maps the category set for the view. + */ + private Map budgetViewCategory; + + /** + * Constructor for Budget Object. + * @param budgetViewCategory A map of view to the category + */ + public BudgetView(Map budgetViewCategory) { + this.budgetViewCategory = budgetViewCategory; + } + + /** + * Sets category to a given view. + * + * @param view a Integer view to set + * @param category the String tag specified that we want to set a view to + */ + public void setBudgetView(int view, String category) { + budgetViewCategory.put(view, category); + } + + public Map getBudgetViewCategory() { + return budgetViewCategory; + } + +} diff --git a/src/main/java/duke/model/DukeItem.java b/src/main/java/duke/model/DukeItem.java new file mode 100644 index 0000000000..b6bd15fc84 --- /dev/null +++ b/src/main/java/duke/model/DukeItem.java @@ -0,0 +1,159 @@ +package duke.model; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The parent class for all {@code DukeItem}s, which are stored in {@code DukeList}s. + */ +abstract class DukeItem implements Serializable { + /** + * The string that separates different fields in the storage string. + */ + protected static final String STORAGE_FIELD_DELIMITER = "\n"; + /** + * The string that separates the names from the values in the storage string. + */ + protected static final String STORAGE_NAME_SEPARATOR = ":"; + + /** + * The string that separates tags from each other in the storage string. + */ + protected static final String STORAGE_TAG_SEPARATOR = " "; + + /** + * The string that separates tags from each other in an input string. + */ + protected static final String TAG_SEPARATOR = " "; + /** + * The item's tags. + */ + protected final String tag; + + /** + * A utility method used to extract fields from a storage string into a map, so that they can be + * easily accessed by the subclasses in order to construct a new builder from the storage string. + * + * @param storageString the storage string representing a subclass. + * @return a map of the storage string's fields. + */ + protected static Map storageStringToMap(String storageString) { + return Stream.of(storageString.split(STORAGE_FIELD_DELIMITER)) + .map(s -> s.split(STORAGE_NAME_SEPARATOR, 2)) + .collect(Collectors.toMap(s -> s[0], s -> s.length > 1 ? s[1] : "")); + } + + /** + * Subclassing solution taken from https://stackoverflow.com/a/17165079 + * A builder class for {@code DukeItem}, making it easier to construct items with + * optional fields. + * + * @param the subclassed builder; see the sof link above. + */ + abstract static class Builder> { + private String tag = ""; + + /** + * Constructs an empty item with default values for all fields. + */ + protected Builder() { + + } + + /** + * Constructs an item from an existing item. + * + * @param i the existing item. + */ + protected Builder(DukeItem i) { + tag = i.tag; + } + + /** + * Constructs an item from its storage string. Used to load items from storage. + * + * @param storageString the item's storage string. + */ + protected Builder(String storageString) { + this(storageStringToMap(storageString)); + } + + /** + * Constructs an item from its mapped storage string. Used internally to load items from storage. + * + * @param mappedStorageString a map of the item's storage string. + */ + protected Builder(Map mappedStorageString) { + if (mappedStorageString.containsKey("tag")) { + this.tag = mappedStorageString.get("tag"); + } + } + + public T setTag(String tag) { + this.tag = tag; + return getThis(); + } + + + + /** + * Method used to limit the scope of suppression; returns {@code this}, type-cast to {@code T}, + * the subclassed builder. + * + * @return {@code this} type-casted to {@code T} + */ + @SuppressWarnings("unchecked") + private T getThis() { + return (T) this; + } + } + + /** + * Constructs an item from the item builder. + * + * @param builder the builder containing information for this object. + */ + protected DukeItem(Builder builder) { + tag = builder.tag; + } + + /** + * Converts the item to a storage string to be saved, then loaded later. + * + * @return the item's storage string. + */ + protected String toStorageString() { + StringJoiner stringJoiner = new StringJoiner(STORAGE_FIELD_DELIMITER); + stringJoiner.add("tag" + STORAGE_NAME_SEPARATOR + String.join(" ", tag)); + return stringJoiner.toString(); + } + + /** + * Returns the set of tags of this item. + * + * @return the set of tags of this item. + */ + public String getTag() { + return tag; + } + + /** + * Returns a single string containing all of the tags. + * + * @return single String of all the tags + */ + public String getTagString() { + StringJoiner stringJoiner = new StringJoiner(" "); + if (tag.isEmpty()) { + return ""; + } else { + return tag; + } + } + +} diff --git a/src/main/java/duke/model/DukeList.java b/src/main/java/duke/model/DukeList.java new file mode 100644 index 0000000000..68a071c97d --- /dev/null +++ b/src/main/java/duke/model/DukeList.java @@ -0,0 +1,156 @@ +package duke.model; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.Stack; + +import duke.exception.DukeException; +import duke.exception.DukeRuntimeException; +import javafx.collections.ObservableList; + +/** + * The generic parent list of all lists in Duke, which are responsible for saving their own information + * and have undo and redo capabilities. + * + * @param The {@code DukeItem} contained in the list. + */ +abstract class DukeList { + private static String STORAGE_DELIMITER = "\n\n"; + + // private final File file; + private final String itemName; + private Stack undoStates; + private byte[] currentState; + private Stack redoStates; + + protected List internalList; + protected ObservableList externalList; + + + public DukeList(List internalList, String itemName) { + this.itemName = itemName; + this.internalList = internalList; + + undoStates = new Stack(); + currentState = toByteArray(internalList); + redoStates = new Stack(); + } + + /** + * Updates, then returns {@code externalList}. + * {@code externalList} should be updated based on filter, sort and view which are implemented + * by the subclasses. + * + * @return the up-to-date externalList. + */ + public abstract ObservableList getExternalList(); + + public abstract List getInternalList(); + + public abstract void setSortCriteria(String sortCriteria) throws DukeException; + + public abstract void setFilterCriteria(String filterCriteria) throws DukeException; + + public abstract void setViewScope(String viewScope, int previous) throws DukeException; + + public abstract List sort(List currentList); + + public abstract List filter(List currentList); + + public abstract List view(List currentList); + + /** + * Adds an item into {@code internalList}. + * + * @param item the item to add. + */ + public void add(T item) { + internalList.add(item); + } + + /** + * Returns an item referenced using its index in {@code externalList}. + * + * @param index the index of the item in @{code externalList}. + * @return the item. + * @throws DukeException if the index is out of bounds. + */ + public T get(int index) throws DukeException { + if (index < 1 || index > externalList.size()) { + throw new DukeException(String.format(DukeException.MESSAGE_NO_ITEM_AT_INDEX, itemName, index)); + } + return externalList.get(index - 1); + } + + /** + * Returns the number of items in {@code internalList}. + * + * @return the number of items in {@code internalList}. + */ + public int internalSize() { + return internalList.size(); + } + + /** + * Removes an item from {@code internalList} using its index in {@code externalList}. + * + * @param index the index of the item to in {@code externalList}. + * @throws DukeException if the index is out of bounds. + */ + public void remove(int index) throws DukeException { + internalList.remove(get(index)); + } + + /** + * Removes all items from {@code internalList}. + */ + public void clear() { + internalList.clear(); + } + + + /** + * Taken from https://stackoverflow.com/a/30968827 + * Converts the current state of {@code internalList} into a byte array so that it can be restored later. + * + * @return the byte array of the current {@code internalList}. + */ + private byte[] toByteArray(List list) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutput out = new ObjectOutputStream(bos)) { + out.writeObject(list); + return bos.toByteArray(); + } catch (IOException e) { + throw new DukeRuntimeException("Failed to create byte array from list.", e); + } + } + + /** + * Taken from https://stackoverflow.com/a/30968827 + * Returns a list corresponding to a previous state of {@code internalList}. + * Casting to {@code List} causes the warning. As the code is self-contained, there is no risk of + * the object in {@code bytes} not being one of type {@code List}. + * + * @param bytes a byte array corresponding to a previous state of {@code internalList}. + * @return the previous state of {@code internalList}. + */ + @SuppressWarnings("unchecked") + private List fromByteArray(byte[] bytes) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInput in = new ObjectInputStream(bis)) { + return (List) in.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new DukeRuntimeException("Failed to load list from byte array.", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/duke/model/DukePP.java b/src/main/java/duke/model/DukePP.java new file mode 100644 index 0000000000..ea2d293c9f --- /dev/null +++ b/src/main/java/duke/model/DukePP.java @@ -0,0 +1,339 @@ +package duke.model; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; +import duke.model.payment.PaymentOverduePredicate; +import duke.model.payment.PaymentInWeekPredicate; +import duke.model.payment.PaymentInMonthPredicate; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.logging.Logger; + +/** + * Wraps all memory data of Duke++ + * Implements the interface of model module. + */ +public class DukePP implements Model { + + private static final Logger logger = LogsCenter.getLogger(DukePP.class); + + private static final Predicate ALL_PAYMENTS_PREDICATE = PaymentList.PREDICATE_SHOW_ALL_PAYMENTS; + + private final ExpenseList expenseList; + private final PlanBot planBot; + private final IncomeList incomeList; + private final Budget budget; + private final BudgetView budgetView; + private final PaymentList payments; + + public ObservableList externalExpenseList; + public ObservableList externalIncomeList; + + + /** + * Creates a DukePP. + * This constructor is used for loading DukePP from storage. + */ + public DukePP(ExpenseList expenseList, Map planAttributes, IncomeList incomeList, + Budget budget, BudgetView budgetView, Optional optionalPayments) throws DukeException { + + this.expenseList = expenseList; + this.planBot = PlanBot.getInstance(planAttributes); + this.incomeList = incomeList; + this.budget = budget; + this.budgetView = budgetView; + + if (optionalPayments.isEmpty()) { + logger.warning("PaymentList is not loaded. It will be starting with a empty PaymentList"); + this.payments = new PaymentList(new ArrayList<>()); + } else { + this.payments = optionalPayments.get(); + } + } + + //******************************** ExpenseList operations + + public void addExpense(Expense expense) { + expenseList.add(expense); + } + + public void deleteExpense(int index) throws DukeException { + expenseList.remove(index); + } + + public void clearExpense() { + expenseList.clear(); + } + + public void filterExpense(String filterCriteria) throws DukeException { + expenseList.setFilterCriteria(filterCriteria); + } + + public void sortExpense(String sortCriteria) throws DukeException { + expenseList.setSortCriteria(sortCriteria); + } + + public void viewExpense(String viewScope, int previous) throws DukeException { + expenseList.setViewScope(viewScope, previous); + } + + /** + * Returns external expense list. + * + * @return externalExpenseList the expense list to be reflected in ExpensePane + */ + public ObservableList getExpenseExternalList() { + logger.info("Model sends external expense list length " + + expenseList.getExternalList().size()); + externalExpenseList = FXCollections.unmodifiableObservableList(expenseList.getExternalList()); + return externalExpenseList; + } + + /** + * Returns the expenseList for storage. + */ + public ExpenseList getExpenseList() { + return expenseList; + } + + public BigDecimal getTotalAmount() { + return expenseList.getTotalAmount(); + } + + //******************************** Budget and budgetView operations + + @Override + public StringProperty getExpenseListTotalString() { + return expenseList.getTotalString(); + } + + @Override + public StringProperty getSortCriteriaString() { + return expenseList.getSortString(); + } + + @Override + public StringProperty getViewCriteriaString() { + return expenseList.getViewString(); + } + + @Override + public StringProperty getFilterCriteriaString() { + return expenseList.getFilterString(); + } + + @Override + public String getMonthlyBudgetString() { + return budget.getMonthlyBudgetString(); + } + + @Override + public BigDecimal getMonthlyBudget() { + return budget.getMonthlyBudget(); + } + + @Override + public void setMonthlyBudget(BigDecimal monthlyBudget) { + budget.setMonthlyBudget(monthlyBudget); + } + + @Override + public void setCategoryBudget(String category, BigDecimal budgetBD) { + budget.setCategoryBudget(category, budgetBD); + } + + @Override + public BigDecimal getRemaining(BigDecimal total) { + return budget.getRemaining(total); + } + + @Override + public Map getBudgetCategory() { + return budget.getBudgetCategory(); + } + + @Override + public Budget getBudget() { + return budget; + } + + @Override + public BigDecimal getBudgetTag(String category) { + return budget.getBudgetTag(category); + } + + @Override + public ObservableList getBudgetObservableList() { + return budget.getBudgetObservableList(); + } + + @Override + public BudgetView getBudgetView() { + return budgetView; + } + + @Override + public void setBudgetView(Integer view, String category) { + budgetView.setBudgetView(view,category); + } + + @Override + public Map getBudgetViewCategory() { + return budgetView.getBudgetViewCategory(); + } + + + //************************************************************ + // PlanBot operations + + public ObservableList getDialogObservableList() { + return planBot.getDialogObservableList(); + } + + public void processPlanInput(String input) throws DukeException { + planBot.processInput(input); + } + + @Override + public Map getKnownPlanAttributes() { + return planBot.getPlanAttributes(); + } + + @Override + public PlanQuestionBank.PlanRecommendation getRecommendedBudgetPlan() { + return planBot.getPlanBudgetRecommendation(); + } + + //************************************************************ IncomeList operations + + @Override + public StringProperty getIncomeListTotalString() { + return incomeList.getTotalString(); + } + + /** + * Adds income. + * @param income Income object to be added + */ + public void addIncome(Income income) { + incomeList.add(income); + logger.info("Model's income externalList length now is " + + externalIncomeList.size()); + } + + public void deleteIncome(int index) throws DukeException { + incomeList.remove(index); + } + + public void clearIncome() { + incomeList.clear(); + } + + public void filterIncome(String filterCriteria) throws DukeException { + expenseList.setFilterCriteria(filterCriteria); + } + + public void sortIncome(String sortCriteria) throws DukeException { + expenseList.setSortCriteria(sortCriteria); + } + + public void viewIncome(String viewScope, int previous) throws DukeException { + expenseList.setViewScope(viewScope, previous); + } + + /** + * Getter method for IncomeExternalList. + * @return ExternalList of Income + */ + public ObservableList getIncomeExternalList() { + logger.info("Model sends external income list length " + + incomeList.getExternalList().size()); + externalIncomeList = FXCollections.unmodifiableObservableList(incomeList.getExternalList()); + return externalIncomeList; + } + + public IncomeList getIncomeList() { + return incomeList; + } + + + //************************************************************ + // Pending Payments operations + + public void addPayment(Payment payment) { + payments.add(payment); + } + + public void setPayment(int index, Payment editedPayment) throws DukeException { + payments.setPayment(index, editedPayment); + } + + public void removePayment(int index) throws DukeException { + payments.remove(index); + } + + public void setPaymentSortingCriteria(String sortCriteria) throws DukeException { + payments.setSortingCriteria(sortCriteria); + } + + public void setAllPredicate() { + payments.setTimePredicate(ALL_PAYMENTS_PREDICATE); + } + + public void setMonthPredicate() { + PaymentInMonthPredicate monthPredicate = new PaymentInMonthPredicate(); + payments.setTimePredicate(monthPredicate); + } + + public void setWeekPredicate() { + PaymentInWeekPredicate weekPredicate = new PaymentInWeekPredicate(); + payments.setTimePredicate(weekPredicate); + } + + public void setOverduePredicate() { + PaymentOverduePredicate overduePredicate = new PaymentOverduePredicate(); + payments.setTimePredicate(overduePredicate); + } + + public void setSearchKeyword(String keyword) { + payments.setSearchPredicate(keyword); + } + + public Payment getPayment(int index) throws DukeException { + return payments.getPayment(index); + } + + public ObservableList getUnmodifiableFilteredPaymentList() { + return payments.asUnmodifiableFilteredList(); + } + + /** + * Returns the paymentList itself for storage update ONLY. + * + * @return the paymentList + */ + public PaymentList getPaymentList() { + return payments; + } + + @Override + public ObjectProperty getPaymentSortingCriteria() { + return payments.getSortingCriteriaIndicator(); + } + + @Override + public ObjectProperty getPaymentPredicate() { + return payments.getPredicateIndicator(); + } + +} diff --git a/src/main/java/duke/model/Expense.java b/src/main/java/duke/model/Expense.java new file mode 100644 index 0000000000..3c59a03a07 --- /dev/null +++ b/src/main/java/duke/model/Expense.java @@ -0,0 +1,300 @@ +package duke.model; + +import duke.exception.DukeException; +import duke.logic.parser.Parser; +import duke.model.payment.Payment; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.StringJoiner; + +public class Expense extends DukeItem { + /** + * The amount of money of the expense. + */ + private final BigDecimal amount; + /** + * The description of the expense. + */ + private final String description; + /** + * Whether or not the expense is tentative. + */ + private boolean isTentative; + /** + * The time of the expense. + */ + private final LocalDateTime time; + /** + * Is true if expense is a recurring one. + */ + private boolean isRecurring; + + /** + * {@inheritDoc} + */ + public static class Builder extends DukeItem.Builder { + private BigDecimal amount = BigDecimal.ZERO; + private String description = ""; + private boolean isTentative = false; + private boolean isRecurring = false; + private LocalDateTime time = LocalDateTime.now(); + + public Builder() { + + } + + /** + * Constructs a builder from an existing expense. + * + * @param expense the expense whose values to use as the builder's default values. + */ + public Builder(Expense expense) { + super(expense); + amount = expense.amount; + description = expense.description; + isTentative = expense.isTentative; + time = expense.time; + } + + /** + * Builder object for Payment. + * @param payment Payment object + */ + public Builder(Payment payment) { + setTag(payment.getTag()); + amount = payment.getAmount(); + description = payment.getDescription(); + isTentative = false; + time = LocalDateTime.now(); + } + + /** + * {@inheritDoc} + */ + Builder(String storageString) throws DukeException { + this(storageStringToMap(storageString)); + } + + /** + * {@inheritDoc} + */ + Builder(Map mappedStorageString) throws DukeException { + super(mappedStorageString); + if (mappedStorageString.containsKey("amount")) { + setAmount(mappedStorageString.get("amount")); + } + if (mappedStorageString.containsKey("description")) { + setDescription(mappedStorageString.get("description")); + } + if (mappedStorageString.containsKey("isTentative")) { + setTentative(Boolean.parseBoolean(mappedStorageString.get("isTentative"))); + } + if (mappedStorageString.containsKey("time")) { + setTime(Parser.parseTime(mappedStorageString.get("time"))); + } + if (mappedStorageString.containsKey("isRecurring")) { + setRecurring(Boolean.parseBoolean(mappedStorageString.get("isRecurring"))); + } + } + + /** + * Sets the amount of the expense using a string. + * + * @param amount the amount of the expense as a string. + * @return this builder. + * @throws DukeException if the value in amount cannot be converted into a {@code BigDecimal}, + * or if the {@code BigDecimal} does not represent a valid amount. + * @see #setAmount(BigDecimal) + */ + public Builder setAmount(String amount) throws DukeException { + try { + return setAmount(new BigDecimal(amount)); + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_EXPENSE_AMOUNT_INVALID, amount)); + } + } + + /** + * Sets the amount of the expense. + * + * @param amount the amount of the expense. + * @return this builder. + * @throws DukeException if the {@code BigDecimal} does not represent a valid amount. + */ + public Builder setAmount(BigDecimal amount) throws DukeException { + if (amount.scale() > 2) { + throw new DukeException( + String.format(DukeException.MESSAGE_EXPENSE_AMOUNT_INVALID, amount.toPlainString())); + } + this.amount = amount.setScale(2, RoundingMode.UNNECESSARY); + return this; + } + + /** + * Sets the description of the expense. + * + * @param description the description of the expense. + * @return this builder. + */ + public Builder setDescription(String description) { + this.description = description; + return this; + } + + /** + * Sets the tentativeness of the expense. + * + * @param tentative whether the expense is tentative. + * @return this builder. + */ + public Builder setTentative(boolean tentative) { + isTentative = tentative; + return this; + } + + /** + * Sets the expense as a recurring expense. + * + * @param recurring whether the expense is tentative. + * @return this builder. + */ + public Builder setRecurring(boolean recurring) { + isRecurring = recurring; + return this; + } + + /** + * Sets the time of the expense using a string. + * + * @param time the time of the expense as a string. + * @return this builder. + * @throws DukeException if the time string cannot be parsed into a {@code LocalDateTime} object. + * @see #setTime(LocalDateTime) + */ + public Builder setTime(String time) throws DukeException { + try { + return setTime(Parser.parseTime(time)); + } catch (DukeException e) { + throw new DukeException(String.format(DukeException.MESSAGE_EXPENSE_TIME_INVALID, time)); + } + } + + + /** + * Sets the time of the expense. + * + * @param time the time of the expense. + * @return this builder. + */ + public Builder setTime(LocalDateTime time) { + this.time = time; + return this; + } + + /** + * Builds the expense. + * + * @return the expense. + */ + public Expense build() { + return new Expense(this); + } + } + + /** + * Constructs an expense from the expense builder. + * + * @param builder the expense builder. + */ + private Expense(Builder builder) { + super(builder); + amount = builder.amount; + description = builder.description; + isTentative = builder.isTentative; + isRecurring = builder.isRecurring; + time = builder.time; + } + + /** + * Returns the amount of the expense. + * + * @return {@link #amount}. + */ + public BigDecimal getAmount() { + return amount; + } + + /** + * Returns the description of the expense. + * + * @return {@link #description}. + */ + public String getDescription() { + return description; + } + + /** + * Returns whether the expense is tentative. + * + * @return {@link #isTentative}. + */ + public boolean isTentative() { + return isTentative; + } + + public void setTentative(boolean val) { + isTentative = val; + } + + /** + * Returns the date of the expense. + * + * @return {@link #time}. + */ + public LocalDateTime getTime() { + return time; + } + + /** + * Returns whether the expense is recurring. + * + * @return {@link #isTentative}. + */ + public boolean isRecurring() { + return isRecurring; + } + + + /** + * Return the formatted time. + * + * @return String of time that is formatted + */ + public String getTimeString() { + if (isRecurring) { + return "recurring"; + } + return Parser.formatTime(time); + } + + + /** + * Converts the expense into a storage string. + * + * @return the expense as a storage string. + */ + @Override + public String toStorageString() { + StringJoiner stringJoiner = new StringJoiner(STORAGE_FIELD_DELIMITER); + stringJoiner.add(super.toStorageString()); + stringJoiner.add("amount" + STORAGE_NAME_SEPARATOR + amount); + stringJoiner.add("description" + STORAGE_NAME_SEPARATOR + description); + stringJoiner.add("time" + STORAGE_NAME_SEPARATOR + Parser.formatTime(time)); + stringJoiner.add("isTentative" + STORAGE_NAME_SEPARATOR + isTentative); + stringJoiner.add("isRecurring" + STORAGE_NAME_SEPARATOR + isRecurring); + return stringJoiner.toString(); + } +} diff --git a/src/main/java/duke/model/ExpenseList.java b/src/main/java/duke/model/ExpenseList.java new file mode 100644 index 0000000000..ec68c63a65 --- /dev/null +++ b/src/main/java/duke/model/ExpenseList.java @@ -0,0 +1,372 @@ +package duke.model; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class ExpenseList extends DukeList { + + + private static final Logger logger = LogsCenter.getLogger(ExpenseList.class); + + private enum SortCriteria { + AMOUNT(Comparator.comparing(Expense::getAmount).reversed()), + TIME(Comparator.comparing(Expense::getTime).reversed()), + DESCRIPTION(Comparator.comparing(Expense::getDescription)); + + private Comparator comparator; + + SortCriteria(Comparator comparator) { + this.comparator = comparator; + } + } + + public enum ViewScopeName { + DAY, WEEK, MONTH, YEAR, ALL; + } + + public class ViewScope { + private int viewScopeNumber; + private ViewScopeName viewScopeName; + + /** + * Constructor for ViewScope. + * @param viewScopeName String name of the viewScope + * @param viewScopeNumber int number of viewScope + * @throws DukeException when invalld viewScope name + */ + public ViewScope(String viewScopeName, int viewScopeNumber) throws DukeException { + this.viewScopeNumber = viewScopeNumber; + try { + this.viewScopeName = ViewScopeName.valueOf(viewScopeName.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new DukeException(String.format( + DukeException.MESSAGE_EXPENSE_VIEW_NAME_INVALID, viewScopeName)); + } + } + + public ViewScope(ViewScopeName viewScopeName) { + this.viewScopeNumber = 0; + this.viewScopeName = viewScopeName; + } + + + private List dayView(List currentList) { + return currentList.stream() + .filter(e -> { + boolean isRecurring = e.isRecurring(); + LocalDate dateOfExpense = e.getTime().toLocalDate(); + LocalDate current = LocalDate.now().minusDays(viewScopeNumber); + return dateOfExpense.equals(current) && !isRecurring; + }) + .collect(Collectors.toList()); + } + + private List weekView(List currentList) { + return currentList.stream() + .filter(e -> { + boolean isRecurring = e.isRecurring(); + int dayOfWeek = e.getTime().getDayOfWeek().getValue(); + LocalDate start = e.getTime().minusDays(dayOfWeek - 1).toLocalDate(); + // Sunday of week of expense. + LocalDate end = e.getTime().plusDays(7 - dayOfWeek).toLocalDate(); + // Monday of week of expense. + LocalDate current = LocalDate.now().minusWeeks(viewScopeNumber); + + return (current.equals(end) || current.equals(start) + || (current.isAfter(start) && current.isBefore(end)) && !isRecurring); + }) + .collect(Collectors.toList()); + } + + private List monthView(List currentList) { + return currentList.stream() + .filter(e -> { + boolean isRecurring = e.isRecurring(); + LocalDate dateOfExpense = e.getTime().toLocalDate(); + LocalDate current = LocalDate.now().minusMonths(viewScopeNumber); + boolean isSameYear = dateOfExpense.getYear() == current.getYear(); + boolean isSameMonth = dateOfExpense.getMonth().equals(current.getMonth()); + return (isSameYear && isSameMonth || isRecurring); + }) + .collect(Collectors.toList()); + } + + private List yearView(List currentList) { + return currentList.stream() + .filter(e -> { + boolean isRecurring = e.isRecurring(); + LocalDate dateOfExpense = e.getTime().toLocalDate(); + LocalDate current = LocalDate.now().minusYears(viewScopeNumber); + return dateOfExpense.getYear() == current.getYear() || isRecurring; + }) + .collect(Collectors.toList()); + } + + /** + * Returns a filtered list based on the view scope. + * + * @param currentList List of Expenses we want to filter down + * @return the filtered List of Expense + */ + public List view(List currentList) { + switch (viewScopeName) { + case DAY: + return dayView(currentList); + + case WEEK: + return weekView(currentList); + + case MONTH: + return monthView(currentList); + + case YEAR: + return yearView(currentList); + + default: // case ALL: + return currentList; // the viewScope here is ALL. + } + } + + public ViewScopeName getViewScopeName() { + return viewScopeName; + } + } + + private SortCriteria sortCriteria; + private ViewScope viewScope; + private String filterCriteria; + + private ObservableList externalFinalList; + private StringProperty totalString; + private StringProperty filterString; + private StringProperty sortString; + private StringProperty viewString; + + + /** + * Constructor for ExpenseList. + * @param internalList the List<Expense> object we want to populate the list with + */ + public ExpenseList(List internalList) { + super(internalList, "expense"); + filterCriteria = ""; + viewScope = new ViewScope(ViewScopeName.ALL); + sortCriteria = SortCriteria.TIME; + externalList = FXCollections.observableArrayList(); + externalFinalList = FXCollections.unmodifiableObservableList(externalList); + totalString = new SimpleStringProperty(); + filterString = new SimpleStringProperty(); + sortString = new SimpleStringProperty(); + viewString = new SimpleStringProperty(); + updateExternalList(); + } + + private void updateExternalList() { + List filteredSortedViewedList = filter(sort(view(internalList))); + ObservableList internalFinalList = FXCollections.observableArrayList(filteredSortedViewedList); + externalList.setAll(internalFinalList); + totalString.setValue("Total: $" + getTotalExternalAmount()); + filterString.setValue("Filter: " + filterCriteria); + switch (sortCriteria) { + case AMOUNT: + sortString.setValue("Sort by: Largest"); + break; + case DESCRIPTION: + sortString.setValue("Sort by: Alphabetical"); + break; + default: + sortString.setValue("Sort by: Newest"); + break; + } + viewString.set("Viewscope: " + viewScope.getViewScopeName()); + } + + @Override + public void add(Expense expense) { + super.add(expense); + updateExternalList(); + logger.info("externalList lengths " + externalList.size()); + } + + @Override + public void remove(int index) throws DukeException { + super.remove(index); + updateExternalList(); + } + + @Override + public void clear() { + super.clear(); + updateExternalList(); + } + + /** + * Updates {@code externalList}, then returns it. + * + * @return {@code externalList}. + */ + @Override + public ObservableList getExternalList() { + return externalFinalList; + } + + @Override + public List getInternalList() { + return internalList; + } + + /** + * Sets the sort criteria. + * Sort criteria include AMOUNT, TIME, DESCRIPTION. + * + * @param sortCriteria The String indicating the criteria for sorting. + * @throws DukeException If the format of sort criteria is incorrect. + */ + @Override + public void setSortCriteria(String sortCriteria) throws DukeException { + try { + this.sortCriteria = SortCriteria.valueOf(sortCriteria.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new DukeException(String.format(DukeException.MESSAGE_SORT_CRITERIA_INVALID, sortCriteria)); + } + updateExternalList(); + } + + @Override + public void setFilterCriteria(String filterCriteria) throws DukeException { + this.filterCriteria = filterCriteria; + updateExternalList(); + } + + /** + * Sets the view scope. + * View scopes include DAY, WEEK, MONTH, YEAR, ALL; + * + * @param viewScopeName The string indicating the time scope of displayed list. + * @throws DukeException If the format of view scope is incorrect. + */ + @Override + public void setViewScope(String viewScopeName, int viewScopeNumber) throws DukeException { + this.viewScope = new ViewScope(viewScopeName, viewScopeNumber); + updateExternalList(); + } + + /** + * Sorts the given List with the given criteria and returns the sorted List. + * + * @param currentList The List going to be sorted. + * @return The sorted List. + */ + @Override + public List sort(List currentList) { + currentList.sort(sortCriteria.comparator); + return currentList; + } + + /** + * To be implemented when tags are specified. + * + * @param currentList The List going to be filtered. + * @return The filtered List. + */ + @Override + public List filter(List currentList) { + return currentList; + } + + /** + * Tailors the given List so that only {@code Expense} within the given time scope are preserved. + * The time scope is composed of time unit(e.g. week) and how many (e.g. weeks) ago. + * Returns the tailored List. + * + * @param currentList The list going to be modified. + * @return The tailored List. + */ + @Override + public List view(List currentList) { + return viewScope.view(currentList); + } + + /** + * Returns an item from its storage string. Although this method is present in the item builders, + * it is declared here to make it easier to implement (otherwise requires reflection). + * + * @param storageString the storage string of the item. + * @return the item. + * @throws DukeException if the item could not be created from the storage string. + */ + public static Expense itemFromStorageString(String storageString) throws DukeException { + return new Expense.Builder(storageString).build(); + } + + /** + * Returns the total amount of money spent. + * + * @return BigDecimal of the total amount of money spent. + */ + public BigDecimal getTotalAmount() { + return internalList.stream() + .filter(expense -> !expense.isTentative()) + .map(Expense::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * returns the total Amount given a specific tag. + * + * @param tag the tag of + * @return A BigDecimal which is the sum of all items of a single tag + */ + public BigDecimal getTagAmount(String tag) { + try { + return externalList.stream() + .filter(expense -> expense.getTag().contains(tag)) + .filter(expense -> !expense.isTentative()) + .map(Expense::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } catch (NullPointerException e) { + return BigDecimal.ZERO; + } + } + + /** + * Returns the total amount of money spent on currently visible expenses i.e. those in {@code externalList}. + * + * @return BigDecimal of the total amount of money spent on currently visible expenses. + */ + public BigDecimal getTotalExternalAmount() { + return externalList.stream() + .filter(expense -> !expense.isTentative()) + .map(Expense::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + + public StringProperty getTotalString() { + return totalString; + } + + public StringProperty getFilterString() { + return filterString; + } + + public StringProperty getSortString() { + return sortString; + } + + public StringProperty getViewString() { + return viewString; + } + +} \ No newline at end of file diff --git a/src/main/java/duke/model/Income.java b/src/main/java/duke/model/Income.java new file mode 100644 index 0000000000..e820fbe675 --- /dev/null +++ b/src/main/java/duke/model/Income.java @@ -0,0 +1,165 @@ +package duke.model; + +import duke.exception.DukeException; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Map; +import java.util.StringJoiner; + +/** + * Represents an income of the user. + */ +public class Income extends DukeItem { + /** + * The amount of money of the income. + */ + private final BigDecimal amount; + /** + * The description of the income. + */ + private final String description; + + /** + * {@inheritDoc} + */ + public static class Builder extends DukeItem.Builder { + private BigDecimal amount = BigDecimal.ZERO; + private String description = ""; + + public Builder() { + } + + /** + * Constructs a builder from an existing income. + */ + public Builder(Income income) { + super(income); + amount = income.amount; + description = income.description; + } + + /** + * {@inheritDoc} + */ + Builder(String storageString) throws DukeException { + this(storageStringToMap(storageString)); + } + + /** + * {@inheritDoc} + */ + Builder(Map mappedStorageString) throws DukeException { + super(mappedStorageString); + if (mappedStorageString.containsKey("amount")) { + setAmount(mappedStorageString.get("amount")); + } + + if (mappedStorageString.containsKey("description")) { + setDescription(mappedStorageString.get("description")); + } + + if (!mappedStorageString.containsKey("amount") | !mappedStorageString.containsKey("description")) { + + throw new DukeException(String.format(DukeException.MESSAGE_LOAD_FILE_FAILED, "income.txt")); + } + } + + /** + * Sets the amount of the income using a string. + * + * @param amount the amount of the income as a string. + * @return this builder. + * @throws DukeException if the value in amount cannot be converted into a {@code BigDecimal}, + * or if the {@code BigDecimal} does not represent a valid amount. + * @see #setAmount(BigDecimal) + */ + public Builder setAmount(String amount) throws DukeException { + try { + return setAmount(new BigDecimal(amount)); + } catch (NumberFormatException | NullPointerException e) { + throw new DukeException(String.format(DukeException.MESSAGE_INCOME_AMOUNT_INVALID, amount)); + } + } + + /** + * Sets the amount of the income. + * + * @param amount the amount of the income. + * @return this builder. + * @throws DukeException if the {@code BigDecimal} does not represent a valid amount. + */ + public Builder setAmount(BigDecimal amount) throws DukeException { + if (amount.scale() > 2) { + throw new DukeException( + String.format(DukeException.MESSAGE_INCOME_AMOUNT_INVALID, amount.toPlainString())); + } + this.amount = amount.setScale(2, RoundingMode.UNNECESSARY); + return this; + } + + /** + * Sets the description of the income. + * + * @param description the description of the income. + * @return this builder. + */ + public Builder setDescription(String description) { + this.description = description; + return this; + } + + /** + * Builds the income. + * + * @return the income. + */ + public Income build() { + return new Income(this); + } + } + + /** + * Constructs an income from the income builder. + * + * @param builder the income builder. + */ + private Income(Builder builder) { + super(builder); + amount = builder.amount; + description = builder.description; + } + + /** + * Returns the amount of the income. + * + * @return {@link #amount}. + */ + public BigDecimal getAmount() { + return amount; + } + + /** + * Returns the description of the income. + * + * @return {@link #description}. + */ + public String getDescription() { + return description; + } + + /** + * Converts the income into a storage string. + * + * @return the income as a storage string. + */ + @Override + public String toStorageString() { + StringJoiner stringJoiner = new StringJoiner(STORAGE_FIELD_DELIMITER); + stringJoiner.add("amount" + STORAGE_NAME_SEPARATOR + amount); + stringJoiner.add("description" + STORAGE_NAME_SEPARATOR + description); + + return stringJoiner.toString(); + } +} + diff --git a/src/main/java/duke/model/IncomeList.java b/src/main/java/duke/model/IncomeList.java new file mode 100644 index 0000000000..ed7d97960f --- /dev/null +++ b/src/main/java/duke/model/IncomeList.java @@ -0,0 +1,164 @@ +package duke.model; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.util.List; +import java.util.logging.Logger; + +/** + * IncomeList keeps the list of incomes input by the user. + * It supports a set of basic operations such as adding incomes, removing incomes, + * and getting the entire list. It inherits from DukeList. + * + * It is reflected in BudgetPane for users to keep track of. + */ + +public class IncomeList extends DukeList { + + private static final Logger logger = LogsCenter.getLogger(IncomeList.class); + + private List internalIncomeList; + private ObservableList externalIncomeList; + private StringProperty totalString; + + /** + * Constructor for IncomeList. + * + * @param internalList loaded income list from storage + */ + public IncomeList(List internalList) { + super(internalList, "income"); + + externalList = FXCollections.observableArrayList(); + externalIncomeList = FXCollections.unmodifiableObservableList(externalList); + totalString = new SimpleStringProperty(); + updateExternalList(); + } + + /** + * Method to update the list upon any changes to income list. + */ + private void updateExternalList() { + internalIncomeList = internalList; + externalList.setAll(FXCollections.observableArrayList(internalIncomeList)); + totalString.setValue("Total Income: $" + getTotalExternalAmount()); + } + + /** + * Adds an income to incomeList. + * + * @param income income to be added + */ + @Override + public void add(Income income) { + super.add(income); + updateExternalList(); + logger.info("externalList lengths " + externalList.size()); + } + + /** + * Deletes an income from the incomeList according to its index. + * + * @param index the index of the item to in {@code externalList}. + * @throws DukeException if index is not valid + */ + @Override + public void remove(int index) throws DukeException { + super.remove(index); + updateExternalList(); + } + + /** + * Clears the entire incomeList. + */ + @Override + public void clear() { + super.clear(); + updateExternalList(); + } + + /** + * Returns list as reflected in BudgetPane. + * + * @return externalIncomeList incomeList in the form of ObservableList + */ + @Override + public ObservableList getExternalList() { + return externalIncomeList; + } + + /** + * Returns internal income list. + * + * @return internalIncomeList incomeList as a List + */ + @Override + public List getInternalList() { + return internalIncomeList; + } + + /** + * Returns an item from its storage string. Although this method is present in the item builders, + * it is declared here to make it easier to implement (otherwise requires reflection). + * + * @param storageString the storage string of the item. + * @return the item. + * @throws DukeException if the item could not be created from the storage string. + */ + public static Income itemFromStorageString(String storageString) throws DukeException { + return new Income.Builder(storageString).build(); + } + + /** + * Returns the total amount of money spent on currently visible incomes i.e. those in {@code externalList}. + * + * @return BigDecimal of the total amount of money spent on currently visible incomes. + */ + public BigDecimal getTotalExternalAmount() { + return externalList.stream() + .map(Income::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * Returns the total income as a StringProperty. + * + * @return totalString + */ + StringProperty getTotalString() { + return totalString; + } + + @Override + public void setSortCriteria(String sortCriteria) { + } + + @Override + public void setFilterCriteria(String filterCriteria) { + } + + @Override + public void setViewScope(String viewScope, int previous) { + } + + @Override + public List sort(List currentList) { + return null; + } + + @Override + public List filter(List currentList) { + return null; + } + + @Override + public List view(List currentList) { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/duke/model/Model.java b/src/main/java/duke/model/Model.java new file mode 100644 index 0000000000..704574fe20 --- /dev/null +++ b/src/main/java/duke/model/Model.java @@ -0,0 +1,132 @@ +package duke.model; + +import duke.exception.DukeException; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.function.Predicate; + +/** + * The API of the Model component. + */ +public interface Model { + + //******************************** ExpenseList operations + + public void addExpense(Expense expense); + + public void deleteExpense(int index) throws DukeException; + + public void clearExpense(); + + public void filterExpense(String filterCriteria) throws DukeException; + + public void sortExpense(String sortCriteria) throws DukeException; + + public void viewExpense(String viewScope, int previous) throws DukeException; + + public ObservableList getExpenseExternalList(); + + public ExpenseList getExpenseList(); + + public BigDecimal getTotalAmount(); + + public StringProperty getExpenseListTotalString(); + + public StringProperty getSortCriteriaString(); + + public StringProperty getViewCriteriaString(); + + public StringProperty getFilterCriteriaString(); + + //******************************** Budget and BudgetView operations + + public String getMonthlyBudgetString(); + + public BigDecimal getMonthlyBudget(); + + public void setMonthlyBudget(BigDecimal monthlyBudget); + + public void setCategoryBudget(String category, BigDecimal budgetBD); + + public BigDecimal getRemaining(BigDecimal total); + + public Map getBudgetCategory(); + + public Budget getBudget(); + + public BigDecimal getBudgetTag(String category); + + public ObservableList getBudgetObservableList(); + + public BudgetView getBudgetView(); + + public Map getBudgetViewCategory(); + + public void setBudgetView(Integer view, String category); + + //PlanBot + public ObservableList getDialogObservableList(); + + public void processPlanInput(String input) throws DukeException; + + public Map getKnownPlanAttributes(); + + public PlanQuestionBank.PlanRecommendation getRecommendedBudgetPlan(); + + //************************************************************ + // Pending Payments operations + + public void addPayment(Payment payment); + + public void setPayment(int index, Payment editedPayment) throws DukeException; + + public void removePayment(int index) throws DukeException; + + public void setPaymentSortingCriteria(String sortingCriteria) throws DukeException; + + public void setAllPredicate(); + + public void setMonthPredicate(); + + public void setWeekPredicate(); + + public void setOverduePredicate(); + + public void setSearchKeyword(String keyword); + + public Payment getPayment(int index) throws DukeException; + + public ObservableList getUnmodifiableFilteredPaymentList(); + + public PaymentList getPaymentList(); + + public ObjectProperty getPaymentSortingCriteria(); + + public ObjectProperty getPaymentPredicate(); + + //******************************** IncomeList operations + + public void addIncome(Income income); + + public void deleteIncome(int index) throws DukeException; + + public void clearIncome(); + + public void filterIncome(String filterCriteria) throws DukeException; + + public void sortIncome(String sortCriteria) throws DukeException; + + public void viewIncome(String viewScope, int previous) throws DukeException; + + public ObservableList getIncomeExternalList(); + + public IncomeList getIncomeList(); + + public StringProperty getIncomeListTotalString(); +} diff --git a/src/main/java/duke/model/PlanBot.java b/src/main/java/duke/model/PlanBot.java new file mode 100644 index 0000000000..b89abfc82a --- /dev/null +++ b/src/main/java/duke/model/PlanBot.java @@ -0,0 +1,211 @@ +package duke.model; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.logging.Logger; + +/** + * PlanBot is the overall budget recommendation class. + * It's responsible for displaying the dialog to the user in a list, + * printing error messages as a dialog from the bot + * and asking users answered questions form question bank. + * It ensures that if its asking a question, only one question is being asked at a time. + */ +public class PlanBot { + + private static PlanBot planBot; + + private static final Logger logger = LogsCenter.getLogger(PlanQuestion.class); + + /** + * Denotes if the dialog is from the user or from the Bot. + */ + public enum Agent { + USER, + BOT + } + + /** + * Contains a List of PlanDialog which is the chatBot's history. + */ + private List dialogList; + + /** + * The a ObservableList of PlanDialog history + * so that the GUI can automatically be updated without having to repopulate the entire list. + */ + private ObservableList dialogObservableList; + + /** + * Contains all the PlanQuestion that we are going to ask the user. + */ + private PlanQuestionBank planQuestionBank; + + /** + * A Map of the user's attributes that we have already found out. + */ + private Map planAttributes; + + /** + * The buffer of questions we are asking the user. + */ + private Queue questionQueue; + + /** + * The current question being asked, contains null when no more questions are being asked. + */ + private PlanQuestion currentQuestion; + + private PlanQuestionBank.PlanRecommendation planBudgetRecommendation; + + /** + * Constructor for PlanBot. + * + * @param planAttributes the loaded Map<String, String> planAttributes from Storage + * @throws DukeException when there is an error loading questions based on the loaded planAttributes + */ + private PlanBot(Map planAttributes) { + this.dialogList = new ArrayList<>(); + dialogObservableList = FXCollections.observableList(dialogList); + this.planAttributes = planAttributes; + this.questionQueue = new LinkedList<>(); + if (planAttributes.isEmpty()) { + dialogObservableList.add(new PlanDialog("Hi, seems like this is the first time using Duke++. \n" + + "Let me plan your budget for you! \n" + + " Alternatively, type \"goto expense\" to start using Duke++!", + Agent.BOT)); + } + try { + planQuestionBank = PlanQuestionBank.getInstance(); + } catch (DukeException e) { + dialogObservableList.add(new PlanDialog(e.getMessage(), Agent.BOT)); + } + try { + getNextQuestions(); + } catch (DukeException e) { + dialogObservableList.add(new PlanDialog(e.getMessage(), Agent.BOT)); + } + if (questionQueue.isEmpty()) { + currentQuestion = null; + sendCompletedMessage(); + } else { + dialogList.add(new PlanDialog("Tell me more about yourself to give you recommendations", Agent.BOT)); + PlanQuestion firstQuestion = questionQueue.peek(); + currentQuestion = firstQuestion; + questionQueue.remove(); + assert firstQuestion != null; + PlanDialog initial = new PlanDialog(firstQuestion.getQuestion(), Agent.BOT); + dialogObservableList.add(initial); + } + } + + /** + * Constructor/ Getter method for this Singleton Object. + * @param planAttributes the loaded Map<String, String> planAttributes from Storage + * @return this PlanBot object + */ + public static PlanBot getInstance(Map planAttributes) { + if (planBot == null) { + planBot = new PlanBot(planAttributes); + } + return planBot; + } + + public ObservableList getDialogObservableList() { + return dialogObservableList; + } + + /** + * Processes the input String of the user. + * + * @param input the input String from the user + */ + public void processInput(String input) { + dialogObservableList.add(new PlanDialog(input, Agent.USER)); + if (currentQuestion == null) { + sendCompletedMessage(); + } else { + try { + PlanQuestion.Reply reply = currentQuestion.getReply(input, planAttributes); + planAttributes = reply.getAttributes(); + getNextQuestions(); + logger.info("\n\n\nQueue size: " + questionQueue.size()); + dialogObservableList.add(new PlanDialog(reply.getText(), Agent.BOT)); + if (questionQueue.isEmpty()) { + sendCompletedMessage(); + } else { + currentQuestion = questionQueue.peek(); + questionQueue.remove(); + dialogObservableList.add(new PlanDialog(currentQuestion.getQuestion(), Agent.BOT)); + } + } catch (DukeException e) { + dialogObservableList.add(new PlanDialog(e.getMessage(), Agent.BOT)); + } + } + } + + public PlanQuestionBank.PlanRecommendation getPlanBudgetRecommendation() { + return planBudgetRecommendation; + } + + public Map getPlanAttributes() { + return planAttributes; + } + + /** + * Puts the recommendation into the dialog list. + */ + private void sendCompletedMessage() { + logger.info("Completed Plan Bot"); + try { + dialogList.add(new PlanDialog(planQuestionBank + .makeRecommendation(planAttributes) + .getRecommendation(), Agent.BOT)); + planBudgetRecommendation = planQuestionBank + .makeRecommendation(planAttributes); + StringBuilder recommendedBudgetStringBuilder = new StringBuilder(); + for (String category : planBudgetRecommendation.getPlanBudget().keySet()) { + recommendedBudgetStringBuilder.append(category) + .append(" : ") + .append(planBudgetRecommendation.getPlanBudget().get(category)) + .append("\n"); + } + dialogList.add(new PlanDialog("Here's a recommended budget for you: \n" + + recommendedBudgetStringBuilder.toString() + + "type \"export\" to export the budget", + Agent.BOT)); + } catch (DukeException e) { + dialogList.add(new PlanDialog(e.getMessage(), Agent.BOT)); + } + + } + + private void getNextQuestions() throws DukeException { + questionQueue.clear(); + questionQueue.addAll(planQuestionBank.getQuestions(planAttributes)); + } + + + /** + * A container for an individual chat history. + */ + public static class PlanDialog { + public String text; + public Agent agent; + + public PlanDialog(String text, Agent agent) { + this.agent = agent; + this.text = text; + } + } + + +} diff --git a/src/main/java/duke/model/PlanQuestion.java b/src/main/java/duke/model/PlanQuestion.java new file mode 100644 index 0000000000..508d2acb92 --- /dev/null +++ b/src/main/java/duke/model/PlanQuestion.java @@ -0,0 +1,166 @@ +package duke.model; + +import duke.exception.DukeException; +import duke.logic.parser.Parser; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A single question that is being asked to the user. + */ +public class PlanQuestion { + private String question; + private Map answersAttributesValue; + private Map> neighbouringQuestions; + private String attribute; + + private static final String SUCCESS_MESSAGE = "Ok noted!"; + private static final String DOUBLE = "DOUBLE"; + + /** + * Constructor for PlanQuestion. + * + * @param question String the question we are asking the user + * @param answers an Array of strings of the possible answers + * @param attributeValue an Array of Attributes the attribute could take, + * its size should be the same as the answer array + * @param attribute the attribute String of the user we want to determine from the question + * @throws DukeException when there are errors in the construction of the question + */ + public PlanQuestion(String question, + String[] answers, + String[] attributeValue, + String attribute) throws DukeException { + this.question = question; + this.answersAttributesValue = new HashMap<>(); + int answersSize = answers.length; + if (attributeValue.length < answersSize) { + throw new DukeException("Some question was set up incorrectly!!! This shouldn't have happened!"); + } + for (int i = 0; i < answersSize; ++i) { + answersAttributesValue.put(answers[i], attributeValue[i]); + } + this.attribute = attribute; + this.neighbouringQuestions = new HashMap<>(); + } + + public String getQuestion() { + return question; + } + + public String getAttribute() { + return attribute; + } + + /** + * Returns a set of Integers of neighbouring questions given an attribute. + * + * @param attribute the attribute we want to get the neighbours of. + * @return a set of int indexes of neighbouring questions, an empty set if no enighboring quesion + */ + public Set getNeighbouringQuestions(String attribute) { + if (answersAttributesValue.containsKey(DOUBLE) && (neighbouringQuestions.get(DOUBLE) != null)) { + return neighbouringQuestions.get(DOUBLE); + } + if (neighbouringQuestions.containsKey(attribute)) { + return neighbouringQuestions.get(attribute); + } + return new HashSet<>(); + } + + /** + * Returns a success message if the input provided is a valid one and + * the question is successfully processed. + * + * @param input the input string for the question + * @param attributes the currently known attributes about the user + * @return Reply containing the updated attributes and success message + * @throws DukeException when there the reply is not valid + */ + Reply getReply(String input, Map attributes) throws DukeException { + try { + if (answersAttributesValue.size() == 1) { + if (answersAttributesValue.containsKey(DOUBLE)) { + BigDecimal scaledAmount = Parser.parseMoney(input); + String attributeVal = scaledAmount.toString(); + attributes.put(attribute, attributeVal); + return new Reply(SUCCESS_MESSAGE, attributes); + } + } else { + if (!answersAttributesValue.containsKey(input.toUpperCase())) { + throw new NoSuchElementException(); + } + String attributeVal = answersAttributesValue.get(input.toUpperCase()); + attributes.put(attribute, attributeVal); + return new Reply(SUCCESS_MESSAGE, attributes); + } + } catch (NoSuchElementException | NumberFormatException | NullPointerException e) { + throw new DukeException(DukeException.MESSAGE_PLANBOT_INVALID_REPLY); + } + return new Reply("Something strange happened", attributes); + } + + /** + * Adds a neighbouring question's index to every attribute value. + * + * @param neighbouring Integer index of neighbouring question + */ + public void addNeighbouring(Integer neighbouring) { + if (answersAttributesValue.containsKey(DOUBLE)) { + if (neighbouringQuestions.containsKey(DOUBLE)) { + neighbouringQuestions.get(DOUBLE).add(neighbouring); + } else { + neighbouringQuestions.put(DOUBLE, new HashSet<>(Collections.singletonList(neighbouring))); + } + } + for (String attributeValue : answersAttributesValue.values()) { + if (neighbouringQuestions.containsKey(attributeValue)) { + neighbouringQuestions.get(attributeValue).add(neighbouring); + } else { + neighbouringQuestions.put(attributeValue, new HashSet<>(Collections.singletonList(neighbouring))); + } + } + } + + /** + * Adds a neighbouring question's index to a specific attribute value. + * + * @param neighbouring Integer index of neighbouring question + * @param attributeValue String of the attributeValue we want our questions to be mapped to + */ + public void addNeighbouring(String attributeValue, Integer neighbouring) throws DukeException { + if (!answersAttributesValue.containsValue(attributeValue)) { + throw new DukeException(attributeValue + " is not a valid attribute value for " + attribute); + } else if (neighbouringQuestions.containsKey(attributeValue)) { + neighbouringQuestions.get(attributeValue).add(neighbouring); + } else { + neighbouringQuestions.put(attributeValue, new HashSet<>(Collections.singletonList(neighbouring))); + } + } + + static class Reply { + private String text; + private Map attributes; + + Reply(String text, Map attributes) { + this.text = text; + this.attributes = attributes; + } + + String getText() { + return text; + } + + Map getAttributes() { + return attributes; + } + } + + +} diff --git a/src/main/java/duke/model/PlanQuestionBank.java b/src/main/java/duke/model/PlanQuestionBank.java new file mode 100644 index 0000000000..6c49722377 --- /dev/null +++ b/src/main/java/duke/model/PlanQuestionBank.java @@ -0,0 +1,396 @@ +package duke.model; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.logic.parser.Parser; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.logging.Logger; + +public class PlanQuestionBank { + private static PlanQuestionBank planQuestionBank; + private Map questionList; + + private static final Logger logger = LogsCenter.getLogger(PlanQuestionBank.class); + + private static final String[] BOOL_ANSWERS = {"YES", "Y", "NO", "N"}; + private static final String[] BOOL_ATTRIBUTE_VALUES = {"TRUE", "TRUE", "FALSE", "FALSE"}; + private static final String[] DOUBLE = {"DOUBLE"}; + + + /** + * Constructor of the question bank, developers should add new questions inside here. + * + * @throws DukeException on Error constructing the QuestionBank + */ + private PlanQuestionBank() throws DukeException { + this.questionList = new HashMap<>(); + PlanQuestion question1 = new PlanQuestion("Are you a student from NUS? ", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "NUS_STUDENT"); + question1.addNeighbouring("TRUE", 2); + question1.addNeighbouring("TRUE", 9); + questionList.put(1, question1); + + PlanQuestion question2 = new PlanQuestion("Do you live on campus? ", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "CAMPUS_LIFE"); + question2.addNeighbouring("FALSE", 3); + question2.addNeighbouring("TRUE", 6); + questionList.put(2, question2); + + PlanQuestion question3 = new PlanQuestion( + "How many days of the week do you travel go to school? <0 - 7>", + generateIntRange(0, 7), + generateIntRange(0, 7), + "TRAVEL_DAYS"); + question3.addNeighbouring(4); + questionList.put(3, question3); + + PlanQuestion question4 = new PlanQuestion("How do you go to school? ", + new String[]{"BUS", "MRT", "BOTH"}, + new String[]{"BUS", "MRT", "BOTH"}, + "TRANSPORT_METHOD"); + question4.addNeighbouring(5); + questionList.put(4, question4); + + PlanQuestion question5 = new PlanQuestion("How much does your trip cost each way? \"", + DOUBLE, + DOUBLE, + "TRIP_COST"); + question5.addNeighbouring(7); + questionList.put(5, question5); + + + PlanQuestion question6 = new PlanQuestion("Do you eat at your Hall/RC often? ", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "DINE_IN_HALL"); + question6.addNeighbouring("FALSE", 7); + question6.addNeighbouring("TRUE", 8); + questionList.put(6, question6); + + PlanQuestion question7 = new PlanQuestion("How many meals per day do pay for daily? <0 - 3>", + generateIntRange(0, 3), + generateIntRange(0, 3), + "MEALS_PER_DAY"); + question7.addNeighbouring("1", 8); + question7.addNeighbouring("2", 8); + question7.addNeighbouring("3", 8); + questionList.put(7, question7); + + PlanQuestion question8 = new PlanQuestion("How much does each meal that you pay for " + + "cost on average? ", + DOUBLE, + DOUBLE, + "AVERAGE_MEAL_COST"); + question8.addNeighbouring(9); + questionList.put(8, question8); + + PlanQuestion question9 = new PlanQuestion("How much do you pay for your phone bill? ", + DOUBLE, + DOUBLE, + "PHONE_BILL"); + question9.addNeighbouring(10); + questionList.put(9, question9); + + PlanQuestion question10 = new PlanQuestion("Do you subscribe to netflix? ", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "NETFLIX"); + question10.addNeighbouring(11); + questionList.put(10, question10); + + PlanQuestion question11 = new PlanQuestion("Do you subscribe to a music subscription service? ", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "MUSIC_SUBSCRIPTION"); + question11.addNeighbouring(12); + questionList.put(11, question11); + + questionList.put(12, new PlanQuestion("How much do you want to " + + "spend on online shopping monthly? ", + DOUBLE, + DOUBLE, + "ONLINE_SHOPPING")); + logger.info("QuestionBank generated successfully!"); + } + + /** + * Constructor/getter method for this Singleton Object. + * @return this PlanQuestionBank object + * @throws DukeException on errors loading questions + */ + public static PlanQuestionBank getInstance() throws DukeException { + if (planQuestionBank == null) { + planQuestionBank = new PlanQuestionBank(); + } + return planQuestionBank; + } + + /** + * Gets a Queue of questions to ask a user in PlanBot. + * + * @param knownAttributes Map of String to String of what we already know about the users + * @return a Queue of questions to ask the user + */ + public Set getQuestions(Map knownAttributes) throws DukeException { + Map attributeQuestion = new HashMap<>(); + Queue questionsToAdd = new LinkedList<>(); + questionsToAdd.add(1); + while (!questionsToAdd.isEmpty()) { + Integer index = questionsToAdd.peek(); + questionsToAdd.remove(); + try { + PlanQuestion question = questionList.get(index); + String questionAttribute = question.getAttribute(); + attributeQuestion.put(question.getAttribute(), question); + if (knownAttributes.containsKey(questionAttribute)) { + String attributeValue = knownAttributes.get(questionAttribute); + Set children = questionList.get(index).getNeighbouringQuestions(attributeValue); + questionsToAdd.addAll(children); + } + } catch (NullPointerException e) { + throw new DukeException("Error getting neighbouring questions!"); + } + } + for (String knownAttribute : knownAttributes.keySet()) { + attributeQuestion.remove(knownAttribute); + } + + return new HashSet<>(attributeQuestion.values()); + } + + /** + * Makes recommendations for the user. + * This should only be called when we know every attribute of the user. + * + * @return String budget recommendations. + * @throws DukeException when there is an error in constructing the recommendation based on the knownAttributes + */ + PlanRecommendation makeRecommendation(Map planAttributes) throws DukeException { + Map budgetRecommendation = new HashMap<>(); + StringBuilder recommendation = new StringBuilder(); + List recommendationExpenseList = new ArrayList<>(); + try { + if (planAttributes.get("NUS_STUDENT").equals("FALSE")) { + return new PlanRecommendation("This program is designed for NUS students. \n " + + "Since you're not a NUS student, I can't make any recommendations for you :( \n" + + "However, you can still use the program! Type \"goto expense\" to start using.", + budgetRecommendation, + recommendationExpenseList); + } else { + //NUS STUDENT + if (planAttributes.get("CAMPUS_LIFE").equals("FALSE")) { + String tripCostString = planAttributes.get("TRIP_COST"); + String tripsPerWeekString = planAttributes.get("TRAVEL_DAYS"); + int tripsPerWeek = Integer.parseInt(tripsPerWeekString); + BigDecimal tripsPerWeekBD = BigDecimal.valueOf(tripsPerWeek); + BigDecimal tripCost = Parser.parseMoney(tripCostString); + BigDecimal monthlyCost = tripCost + .multiply(tripsPerWeekBD) + .multiply(BigDecimal.valueOf(8)); + switch (planAttributes.get("TRANSPORT_METHOD")) { + case "MRT": + if (monthlyCost.compareTo(BigDecimal.valueOf(48)) > 0) { + recommendation.append("Based on your travelling habits, " + + "it is cheaper to buy concession!\n") + .append("MRT concession costs: $48.00 monthly.\n") + .append("You should set your transport budget at $48.00 monthly\n\n"); + budgetRecommendation.put("TRANSPORT", Parser.parseMoney("48.00")); + } else if (monthlyCost.compareTo(BigDecimal.ZERO) == 1) { + recommendation.append("You should set transport budget at $") + .append(monthlyCost) + .append(" monthly. \n\n"); + } + break; + case "BUS": + if (monthlyCost.compareTo(BigDecimal.valueOf(52)) > 0) { + recommendation.append("Based on your travelling habits, " + + "it is cheaper to buy concession!\n") + .append("MRT concession costs: $52.00 monthly.\n") + .append("You should set your transport budget at $52.00 monthly\n"); + budgetRecommendation.put("TRANSPORT", Parser.parseMoney("52.00")); + } else { + recommendation.append("You should set transport budget at $") + .append(monthlyCost) + .append(" monthly. \n"); + budgetRecommendation.put("TRANSPORT", monthlyCost); + } + break; + default: + if (monthlyCost.compareTo(BigDecimal.valueOf(85)) > 0) { + recommendation.append("Based on your travelling habits, " + + "it is cheaper to buy concession!\n" + + "Combined concession costs: $85.00 monthly.\n" + + "You should set your transport budget at $85.00 monthly\n\n"); + budgetRecommendation.put("TRANSPORT", Parser.parseMoney("85.00")); + } else { + recommendation.append("You should set transport budget at $") + .append(monthlyCost) + .append(" monthly. \n\n"); + budgetRecommendation.put("TRANSPORT", monthlyCost); + } + break; + } + int mealsPerDay = Integer.parseInt(planAttributes.get("MEALS_PER_DAY")); + if (mealsPerDay > 0) { + BigDecimal costPerMeal = Parser.parseMoney(planAttributes.get("AVERAGE_MEAL_COST")); + BigDecimal monthlyFoodBudget = costPerMeal + .multiply(BigDecimal.valueOf(mealsPerDay)) + .multiply(BigDecimal.valueOf(30)); + if (monthlyFoodBudget.compareTo(BigDecimal.ZERO) == 1) { + recommendation.append("I'd suggest you set your food budget at $") + .append(monthlyFoodBudget) + .append(" monthly. \n\n"); + budgetRecommendation.put("FOOD ", monthlyFoodBudget); + } + } + } else { + //Stays on campus + recommendation.append("Since you live in campus, " + + "you can just allocate a small budget of $10 to transport! \n\n"); + budgetRecommendation.put("transport", Parser.parseMoney("10")); + if (planAttributes.get("DINE_IN_HALL").equals("TRUE")) { + BigDecimal costPerMeal = Parser.parseMoney(planAttributes.get("AVERAGE_MEAL_COST")); + BigDecimal monthlyFoodBudget = costPerMeal + .multiply(BigDecimal.valueOf(4)) + .multiply(BigDecimal.valueOf(11)); + //11 since 3 meals during each weekend * 1 meal per day + if (monthlyFoodBudget.compareTo(BigDecimal.ZERO) == 1) { + recommendation.append("I'd suggest you set your food budget at $") + .append(monthlyFoodBudget).append(" monthly. \n\n"); + budgetRecommendation.put("FOOD", monthlyFoodBudget); + } + } else { + //Eats all meals outside of hall + int mealsPerDay = Integer.parseInt(planAttributes.get("MEALS_PER_DAY")); + BigDecimal costPerMeal = Parser.parseMoney(planAttributes.get("AVERAGE_MEAL_COST")); + BigDecimal monthlyFoodBudget = costPerMeal.multiply(BigDecimal + .valueOf(mealsPerDay)) + .multiply(BigDecimal.valueOf(30)); + if (monthlyFoodBudget.compareTo(BigDecimal.ZERO) == 1) { + recommendation.append("I'd suggest you set your food budget at $") + .append(monthlyFoodBudget).append(" monthly. \n\n"); + budgetRecommendation.put("FOOD", monthlyFoodBudget); + } + } + } + BigDecimal phoneBill = Parser.parseMoney(planAttributes.get("PHONE_BILL")); + if (!phoneBill.equals(Parser.parseMoney("0"))) { + recommendation.append("You set set a budget of $") + .append(phoneBill) + .append(" for your phone bill.\n\n"); + budgetRecommendation.put("phone bill", phoneBill); + Expense.Builder phoneBillExpenseBuilder = new Expense.Builder(); + phoneBillExpenseBuilder.setAmount(phoneBill); + phoneBillExpenseBuilder.setDescription("Phone bill"); + phoneBillExpenseBuilder.setRecurring(true); + phoneBillExpenseBuilder.setTag("PHONE BILL"); + recommendationExpenseList.add(phoneBillExpenseBuilder.build()); + } + if (planAttributes.get("NETFLIX").equals("TRUE")) { + recommendation.append("Netflix has a family plan that is $17.00 per month," + + " so its cheaper if you can find friends to share!\n" + + "You should allocate $4.25 to netflix\n\n"); + budgetRecommendation.put("NETFLIX", Parser.parseMoney("4.25")); + budgetRecommendation.put("NETFLIX bill", phoneBill); + Expense.Builder netflixExpenseBuilder = new Expense.Builder(); + netflixExpenseBuilder.setAmount("4.25"); + netflixExpenseBuilder.setDescription("Netflix"); + netflixExpenseBuilder.setRecurring(true); + netflixExpenseBuilder.setTag("NETFLIX"); + recommendationExpenseList.add(netflixExpenseBuilder.build()); + } + if (planAttributes.get("MUSIC_SUBSCRIPTION").equals("TRUE")) { + recommendation.append("Spotify has a student plan that is only $5 a month! \n" + + "You should allocate $5 to Spotify\n\n"); + budgetRecommendation.put("SPOTIFY", Parser.parseMoney("5")); + Expense.Builder spotifyExpenseBuilder = new Expense.Builder(); + spotifyExpenseBuilder.setAmount("5.00"); + spotifyExpenseBuilder.setDescription("Spotify"); + spotifyExpenseBuilder.setRecurring(true); + spotifyExpenseBuilder.setTag("SPOTIFY"); + recommendationExpenseList.add(spotifyExpenseBuilder.build()); + } + if (Parser.parseMoney(planAttributes.get("ONLINE_SHOPPING")).compareTo(BigDecimal.ZERO) == 1) { + budgetRecommendation.put("online shopping", + Parser.parseMoney(planAttributes.get("ONLINE_SHOPPING"))); + recommendation.append("You should allocate $" + + planAttributes.get("ONLINE_SHOPPING") + + " to online shopping."); + } + } + } catch (NullPointerException | NumberFormatException e) { + throw new DukeException("Error in making recommendation! " + + "Most likely there's something wrong in the the storage file" + + e.getMessage()); + } + if (recommendation.toString().isEmpty()) { + return new PlanRecommendation("I can't make any recommendations for you" + + " :(. Something probably went wrong", + budgetRecommendation, + recommendationExpenseList); + } + logger.info("Recommendation made successfully!"); + return new PlanRecommendation(recommendation.toString(), + budgetRecommendation, + recommendationExpenseList); + } + + private String[] generateIntRange(int start, int end) { + List strings = new ArrayList(); + for (int i = start; i <= end; ++i) { + strings.add(Integer.toString(i)); + } + return strings.toArray(new String[0]); + } + + /** + * Simple container for recommendation. + */ + public class PlanRecommendation { + private String recommendation; + private Map budget; + private List recommendationExpenseList; + + /** + * Constructor for PlanRecommendation. + * + * @param recommendation String of the recommendation, text that will appear in the dialog + * @param budget Map<String, BigDecimal> map of category as key and amount as the value + * @param recommendationExpenseList List of expenses to add into expense list. + */ + public PlanRecommendation(String recommendation, + Map budget, + List recommendationExpenseList) { + this.recommendation = recommendation; + this.budget = budget; + this.recommendationExpenseList = recommendationExpenseList; + } + + public String getRecommendation() { + return recommendation; + } + + public Map getPlanBudget() { + return budget; + } + + public List getRecommendationExpenseList() { + return recommendationExpenseList; + } + } + + +} diff --git a/src/main/java/duke/model/payment/Payment.java b/src/main/java/duke/model/payment/Payment.java new file mode 100644 index 0000000000..cbcfd37235 --- /dev/null +++ b/src/main/java/duke/model/payment/Payment.java @@ -0,0 +1,281 @@ +package duke.model.payment; + +import duke.exception.DukeException; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a Payment to pay. + */ +public class Payment { + + // Initializes optional string fields with empty String + private static final String NOT_ASSIGNED = ""; + + // Initializes due as default + private static final LocalDate DEFAULT_DUE = LocalDate.MIN; + + // Initializes amount as default + private static final BigDecimal DEFAULT_AMOUNT = BigDecimal.ZERO; + + // Initializes Priority as default + private static final Priority DEFAULT_PRIORITY = Priority.MEDIUM; + + // Compulsory fields + private String description; + private LocalDate due; + private BigDecimal amount; + + // Optional fields + private String receiver; + private String tag; + private Priority priority; + + /** + * Represents the Priority of the Payment. + */ + public enum Priority { + HIGH("High", 3), + MEDIUM("Medium", 2), + LOW("Low", 1); + + /** + * Helps parse Priority parameter in user input. + * Also defines how Priority is displayed in Ui. + */ + private String nameShowed; + + // Makes Priority comparable. + private int numeratedLevel; + + public String toString() { + return nameShowed; + } + + public int getNumeratedLevel() { + return numeratedLevel; + } + + Priority(String nameShowed, int numeratedLevel) { + this.nameShowed = nameShowed; + this.numeratedLevel = numeratedLevel; + } + } + + /** + * A Builder class for Payment. + * Enables construction with optional fields. + */ + public static class Builder { + + // Compulsory fields + private String description = NOT_ASSIGNED; + private LocalDate due = DEFAULT_DUE; + private BigDecimal amount = DEFAULT_AMOUNT; + + // Optional fields + private String receiver = NOT_ASSIGNED; + private String tag = NOT_ASSIGNED; + private Priority priority = DEFAULT_PRIORITY; + + /** + * Initializes a builder with all properties undefined. + */ + public Builder() { + + } + + /** + * Constructs a builder from an existing Payment. + * This enables modification on an existing Payment with optional fields. + * + * @param payment the existing Payment + */ + public Builder(Payment payment) { + requireNonNull(payment); + + description = payment.description; + receiver = payment.receiver; + due = payment.due; + tag = payment.tag; + amount = payment.amount; + priority = payment.priority; + } + + /** + * Sets the description in builder. + * The {@code description} cannot be empty. + * + * @param description the description to set + * @return The builder with the description set + */ + public Builder setDescription(String description) { + requireNonNull(description); + assert !description.isEmpty(); + + this.description = description; + return this; + } + + /** + * Sets the receiver in builder. + * The {@code receiver} cannot be empty. + * + * @param receiver the receiver to set + * @return The builder with the receiver set + */ + public Builder setReceiver(String receiver) { + requireNonNull(receiver); + assert !receiver.isEmpty(); + + this.receiver = receiver; + return this; + } + + /** + * Parses and sets the field due in builder. + * + * @param due String expected to follow format dd/mm/yyyy + * @return a builder with the due already set + * @throws DukeException if the String has incorrect time format + */ + public Builder setDue(String due) throws DukeException { + requireNonNull(due); + assert !due.isEmpty(); + + try { + this.due = LocalDate.parse(due, DateTimeFormatter.ofPattern("dd/MM/yyyy")); + } catch (DateTimeParseException e) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_TIME_INVALID, due)); + } + return this; + } + + /** + * Sets the tag in builder. + * The {@code tag} cannot be empty. + * + * @param tag the tag to set + * @return The builder with the tag set + */ + public Builder setTag(String tag) { + requireNonNull(tag); + assert !tag.isEmpty(); + + this.tag = tag.toUpperCase(); + return this; + } + + /** + * Parses and sets the field amount in builder. + * + * @param amount String expected to follow BigDecimal format + * @return a builder with the amount already set + * @throws DukeException if the String has incorrect BigDecimal format + */ + public Builder setAmount(String amount) throws DukeException { + requireNonNull(amount); + assert !amount.isEmpty(); + + try { + this.amount = new BigDecimal(amount); + } catch (NumberFormatException e) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_AMOUNT_INVALID, amount)); + } + return this; + } + + /** + * Parses and sets the field priority in builder. + * + * @param priority String expected to follow Priority format + * @return a builder with the priority already set + * @throws DukeException if the String has incorrect Priority format + */ + public Builder setPriority(String priority) throws DukeException { + requireNonNull(priority); + assert !priority.isEmpty(); + + try { + this.priority = Priority.valueOf(priority.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new DukeException(String.format(DukeException.MESSAGE_PRIORITY_NAME_INVALID, priority)); + } + return this; + } + + /** + * Builds a Payment with given fields. + * + * @return the built Payment + */ + public Payment build() { + return new Payment(this); + } + } + + /** + * Constructs a Payment with a builder. + * + * @param builder the builder containing fields information + */ + public Payment(Builder builder) { + requireNonNull(builder); + + description = builder.description; + receiver = builder.receiver; + due = builder.due; + tag = builder.tag; + amount = builder.amount; + priority = builder.priority; + } + + public String getDescription() { + return description; + } + + public String getReceiver() { + return receiver; + } + + public LocalDate getDue() { + return due; + } + + public String getTag() { + return this.tag; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public Priority getPriority() { + return priority; + } + + public int getNumeratedPriority() { + return priority.getNumeratedLevel(); + } + + /** + * Tests whether any of description, receiver and tag contains the keyword. + * The case of letter is ignored. + * + * @param keyword the keyword being searched + * @return true if the keyword is found + */ + public boolean containsKeyword(String keyword) { + requireNonNull(keyword); + assert !keyword.isEmpty(); + + return description.toLowerCase().contains(keyword.toLowerCase()) + || receiver.toLowerCase().contains(keyword.toLowerCase()) + || tag.toLowerCase().contains(keyword.toLowerCase()); + } +} diff --git a/src/main/java/duke/model/payment/PaymentInMonthPredicate.java b/src/main/java/duke/model/payment/PaymentInMonthPredicate.java new file mode 100644 index 0000000000..2019939362 --- /dev/null +++ b/src/main/java/duke/model/payment/PaymentInMonthPredicate.java @@ -0,0 +1,30 @@ +package duke.model.payment; + +import java.time.LocalDate; +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * Tests whether a {@code payment} is coming to due in current month. + */ +public class PaymentInMonthPredicate implements Predicate { + + public PaymentInMonthPredicate() { + + } + + @Override + public boolean test(Payment payment) { + requireNonNull(payment); + + LocalDate due = payment.getDue(); + LocalDate now = LocalDate.now(); + + boolean isSameYear = (due.getYear() == now.getYear()); + boolean isSameMonth = due.getMonth().equals(now.getMonth()); + boolean isOverdue = due.isBefore(now); + + return (isSameYear && isSameMonth && !isOverdue); + } +} diff --git a/src/main/java/duke/model/payment/PaymentInWeekPredicate.java b/src/main/java/duke/model/payment/PaymentInWeekPredicate.java new file mode 100644 index 0000000000..06b3811e5e --- /dev/null +++ b/src/main/java/duke/model/payment/PaymentInWeekPredicate.java @@ -0,0 +1,40 @@ +package duke.model.payment; + +import java.time.LocalDate; +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * Tests whether a {@code payment} is coming to due in current week. + */ +public class PaymentInWeekPredicate implements Predicate { + + public PaymentInWeekPredicate() { + + } + + @Override + public boolean test(Payment payment) { + requireNonNull(payment); + + LocalDate now = LocalDate.now(); + LocalDate due = payment.getDue(); + + // The current day of week. e.g. Wednesday corresponds to 3 + int dayOfWeek = now.getDayOfWeek().getValue(); + + // Monday of current week + LocalDate thisMonday = now.minusDays(dayOfWeek - 1); + + // Sunday of current week + LocalDate thisSunday = now.plusDays(7 - dayOfWeek); + + boolean isInCurrentWeek = due.equals(thisSunday) || due.equals(thisMonday) + || (due.isAfter(thisMonday) && due.isBefore(thisSunday)); + + boolean isOverdue = due.isBefore(now); + + return (!isOverdue && isInCurrentWeek); + } +} diff --git a/src/main/java/duke/model/payment/PaymentList.java b/src/main/java/duke/model/payment/PaymentList.java new file mode 100644 index 0000000000..d09da4c8fc --- /dev/null +++ b/src/main/java/duke/model/payment/PaymentList.java @@ -0,0 +1,287 @@ +package duke.model.payment; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +/** + * A list of Payments that does not allow nulls. + * Supports a set of basic list operations such as adding, removing and editing and + * supports sorting, changing time scope and searching by keywords operations. + * + * Payments can be sorted according to their amounts, due or priorities, + * where payments with higher amounts, priorities and closer due will be placed at prior. + * + * Time scope of payments can be altered such that it can choose to only shows payments + * overdue, coming in current week, coming in current month or in all time. + * + * Payments can be searched by keyword. Those containing keyword in their + * description, receiver, or tag will be found out. + */ +public class PaymentList { + + private static final Logger logger = LogsCenter.getLogger(PaymentList.class); + + private static final String ITEM_NAME = "payment"; + + private static final SortingCriteria DEFAULT_SORTING_CRITERIA = SortingCriteria.TIME; + + public static final Predicate PREDICATE_SHOW_ALL_PAYMENTS = unused -> true; + + /** + * The list containing all the payments. + */ + private ObservableList internalList; + + /** + * The filtered list containing sorted and filtered payments. + */ + private FilteredList filteredList; + + /** + * The external list containing sorted and filtered payments. + */ + private ObservableList unmodifiableFilteredList; + + /** + * Sorting criteria used to sort payments. + */ + private SortingCriteria sortingCriteria; + + /** + * Helps auto fetch sorting criteria to Ui. + */ + private ObjectProperty sortingCriteriaIndicator = new SimpleObjectProperty(); + + /** + * Helps auto fetch predicate to Ui. + */ + private ObjectProperty predicateIndicator = new SimpleObjectProperty(); + + /** + * Sorting criteria of payments. + */ + public enum SortingCriteria { + TIME, + AMOUNT, + PRIORITY; + } + + /** + * Constructs a PaymentList with a list of payments. + * + * Initializes the list with default time predicate "all" + * and default sorting criteria TIME. + * + * @param payments an empty list or a list of payments from storage + */ + public PaymentList(List payments) { + requireNonNull(payments); + + // Fills the internal list + this.internalList = FXCollections.observableList(payments); + sortingCriteria = DEFAULT_SORTING_CRITERIA; // TIME + sortInternalList(); + + // Fills the filtered list + filteredList = new FilteredList(internalList); + filteredList.setPredicate(PREDICATE_SHOW_ALL_PAYMENTS); + + // Fills the external unmodifiable list + unmodifiableFilteredList = FXCollections.unmodifiableObservableList(filteredList); + + // Sets the fetcher of Ui + predicateIndicator.setValue(PREDICATE_SHOW_ALL_PAYMENTS); + sortingCriteriaIndicator.setValue(sortingCriteria); + } + + /** + * Initialize an empty PaymentList. + */ + public PaymentList() { + this(new ArrayList<>()); + } + + /** + * Adds a payment to the list. + * The list will then be sorted. + */ + public void add(Payment payment) { + requireNonNull(payment); + + internalList.add(payment); + sortInternalList(); + } + + /** + * Removes the payment at {@code index} from the list. + * The payment must exist in the list. + * The list will then be sorted. + */ + public void remove(int index) throws DukeException { + Payment target = getPayment(index); + internalList.remove(target); + sortInternalList(); + } + + /** + * Gets the payment at the {@code} index. + * The payment must exist in the list. + * + * @param index the index of the target payment + * @return the target payment + * @throws DukeException if the index is out of scope + */ + public Payment getPayment(int index) throws DukeException { + Payment target; + try { + target = filteredList.get(index - 1); + } catch (IndexOutOfBoundsException e) { + throw new DukeException(String.format(DukeException.MESSAGE_NO_ITEM_AT_INDEX, ITEM_NAME, index)); + } + return target; + } + + /** + * Replaces the payment at {@code index} in the list with {@code editedPayment}. + * The {@code index} must be a valid index in scope. + * The list will then be sorted. + */ + public void setPayment(int index, Payment editedPayment) throws DukeException { + requireNonNull(editedPayment); + + remove(index); + add(editedPayment); + } + + /** + * Sets the sorting criteria of the internal list. + * The {@code sortingCriteria} must literally corresponds to an element of enum ignoring the case. + * Updates the {@code sortingCriteriaIndicator}. + * + * @param sortingCriteria the string of sorting criteria to be set + * @throws DukeException if no element in enum has the same name (case ignored) as {@code sortingCriteria} + */ + public void setSortingCriteria(String sortingCriteria) throws DukeException { + requireNonNull(sortingCriteria); + + try { + this.sortingCriteria = SortingCriteria.valueOf(sortingCriteria.toUpperCase()); + sortInternalList(); + } catch (IllegalArgumentException e) { + throw new DukeException(String.format(DukeException.MESSAGE_SORT_CRITERIA_INVALID, sortingCriteria)); + } + + // Updates the fetcher of Ui + sortingCriteriaIndicator.setValue(this.sortingCriteria); + } + + /** + * Sets the time predicate of the filtered list. + * Updates the {@code predicateIndicator}. + * + * @param predicate the time predicate to be set + */ + public void setTimePredicate(Predicate predicate) { + requireNonNull(predicate); + + assert (predicate instanceof PaymentOverduePredicate) + || (predicate instanceof PaymentInWeekPredicate) + || (predicate instanceof PaymentInMonthPredicate) + || (predicate.equals(PREDICATE_SHOW_ALL_PAYMENTS)); + + filteredList.setPredicate(predicate); + + // Updates the fetcher of Ui + predicateIndicator.setValue(predicate); + } + + /** + * Sets the search predicate to the filtered list by specifying the {@code keyword}. + * + * @param keyword the keyword to search + */ + public void setSearchPredicate(String keyword) { + requireNonNull(keyword); + + SearchKeywordPredicate searchPredicate = new SearchKeywordPredicate(keyword); + filteredList.setPredicate(searchPredicate); + predicateIndicator.set(searchPredicate); + } + + /** + * Returns the filtered list as an unmodifiable {@code ObservableList}. + * + * @return the unmodifiable filtered list + */ + public ObservableList asUnmodifiableFilteredList() { + return unmodifiableFilteredList; + } + + /** + * Returns the indicator of sorting criteria. + * It helps Ui auto fetch the sorting criteria used in {@code PaymentList}. + * + * @return an {@code ObjectProperty} of the {@code sortingCriteria}. + */ + public ObjectProperty getSortingCriteriaIndicator() { + return sortingCriteriaIndicator; + } + + /** + * Returns the indicator of predicate, which can be time or search predicate. + * It helps Ui auto fetch the predicate used in {@code PaymentList}. + * + * @return an {@code ObjectProperty} of {@code Predicate}. + */ + public ObjectProperty getPredicateIndicator() { + return predicateIndicator; + } + + /** + * Returns all internal payments as a list. + * This is for storage ONLY! + * + * @return a list containing all internal payments. + */ + public List getInternalList() { + return internalList; + } + + /** + * Sorts the internal list with the current {@code sortingCriteria}. + */ + private void sortInternalList() { + switch (sortingCriteria) { + case TIME: + internalList.sort(Comparator.comparing(Payment::getDue)); + break; + + case AMOUNT: + internalList.sort(Comparator.comparing(Payment::getAmount)); + FXCollections.reverse(internalList); // payments with higher amounts will be prior. + break; + + case PRIORITY: + internalList.sort(Comparator.comparing(Payment::getNumeratedPriority)); + FXCollections.reverse(internalList); // payments with higher priority will be prior. + break; + + default: + logger.warning("Unknown errors occur on sortingCriteria"); + break; + } + } +} diff --git a/src/main/java/duke/model/payment/PaymentOverduePredicate.java b/src/main/java/duke/model/payment/PaymentOverduePredicate.java new file mode 100644 index 0000000000..705457b5c3 --- /dev/null +++ b/src/main/java/duke/model/payment/PaymentOverduePredicate.java @@ -0,0 +1,26 @@ +package duke.model.payment; + +import java.time.LocalDate; +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * Tests whether a {@code payment} is overdue. + */ +public class PaymentOverduePredicate implements Predicate { + + public PaymentOverduePredicate() { + + } + + @Override + public boolean test(Payment payment) { + requireNonNull(payment); + + LocalDate due = payment.getDue(); + LocalDate now = LocalDate.now(); + + return due.isBefore(now); + } +} diff --git a/src/main/java/duke/model/payment/SearchKeywordPredicate.java b/src/main/java/duke/model/payment/SearchKeywordPredicate.java new file mode 100644 index 0000000000..e12f5a4a44 --- /dev/null +++ b/src/main/java/duke/model/payment/SearchKeywordPredicate.java @@ -0,0 +1,33 @@ +package duke.model.payment; + +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * Tests whether a {@code Payment}'s description, receiver or tag contains the keyword given. + * Ignores the letter case. + */ +public class SearchKeywordPredicate implements Predicate { + + private String keyword; + + /** + * Constructs an object of {@code SearchKeywordPredicate} + * with the keyword set as the given {@code keyword}. + * + * @param keyword the keyword used for searching + */ + public SearchKeywordPredicate(String keyword) { + requireNonNull(keyword); + + this.keyword = keyword; + } + + @Override + public boolean test(Payment payment) { + requireNonNull(payment); + + return payment.containsKeyword(keyword); + } +} diff --git a/src/main/java/duke/storage/BudgetStorage.java b/src/main/java/duke/storage/BudgetStorage.java new file mode 100644 index 0000000000..39b1a249cb --- /dev/null +++ b/src/main/java/duke/storage/BudgetStorage.java @@ -0,0 +1,91 @@ +package duke.storage; + +import duke.exception.DukeException; +import duke.logic.parser.Parser; +import duke.model.Budget; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +public class BudgetStorage { + private static final String STORAGE_DELIMITER = "\n"; + + private static final File DEFAULT_USER_DIRECTORY = new File("data" + File.separator + "duke"); + private static final File BUDGET_FILE = new File(DEFAULT_USER_DIRECTORY, "budget.txt"); + + /** + * Constructor of Budget object. + * + * @throws DukeException if the file cannot be created or read. + * @throws IOException if the file cannot be created or read. + */ + public BudgetStorage() throws DukeException, IOException { + DEFAULT_USER_DIRECTORY.mkdirs(); + } + + /** + * Writes to the save file. + * + * @throws DukeException if unable to save the file successfully + */ + public void saveBudget(Budget budget) throws DukeException { + try { + Map budgetCategory = budget.getBudgetCategory(); + BUDGET_FILE.createNewFile(); + try (FileWriter fileWriter = new FileWriter(BUDGET_FILE)) { + fileWriter.write(budget.getMonthlyBudgetString()); + fileWriter.write(STORAGE_DELIMITER); + if (!budgetCategory.isEmpty()) { + for (String category : budgetCategory.keySet()) { + BigDecimal budgetBD = budgetCategory.get(category); + fileWriter.write(category + " " + budgetBD); + fileWriter.write(STORAGE_DELIMITER); + } + } + } + } catch (IOException e) { + throw new DukeException(String.format(DukeException.MESSAGE_SAVE_FILE_FAILED, BUDGET_FILE.getPath())); + } + } + + /** + * loads from the save file. + * + * @throws DukeException if the file cannot be created or read.if the file cannot be created or read. + * @throws IOException if the file cannot be created or read. + */ + public Budget loadBudget() throws DukeException, IOException { + BUDGET_FILE.createNewFile(); + BigDecimal monthlyBudget = BigDecimal.ZERO; + Map budgetCategory = new HashMap<>(); + try (Scanner fileReader = new Scanner(BUDGET_FILE).useDelimiter(STORAGE_DELIMITER)) { + if (fileReader.hasNext()) { + String monthlyBudgetString = fileReader.next(); + monthlyBudget = new BigDecimal(monthlyBudgetString); + } + while (fileReader.hasNext()) { + String line = fileReader.next(); + String[] separatedLine = line.split(" "); + int lineLength = separatedLine.length; + StringBuilder categoryBuilder = new StringBuilder(); + for (int wordNumber = 0; wordNumber < lineLength - 2; ++wordNumber) { + categoryBuilder.append(separatedLine[wordNumber]).append(" "); + } + categoryBuilder.append(separatedLine[lineLength - 2]); + String budgetString = separatedLine[lineLength - 1]; + BigDecimal budget = Parser.parseMoney(budgetString); + budget.setScale(2, RoundingMode.HALF_UP); + budgetCategory.put(categoryBuilder.toString(), budget); + } + } catch (IOException e) { + throw new DukeException(String.format(DukeException.MESSAGE_LOAD_FILE_FAILED, BUDGET_FILE.getPath())); + } + return new Budget(monthlyBudget, budgetCategory); + } +} diff --git a/src/main/java/duke/storage/BudgetViewStorage.java b/src/main/java/duke/storage/BudgetViewStorage.java new file mode 100644 index 0000000000..97b551853a --- /dev/null +++ b/src/main/java/duke/storage/BudgetViewStorage.java @@ -0,0 +1,79 @@ +package duke.storage; + +import duke.commons.FileUtil; +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.model.BudgetView; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.logging.Logger; + +public class BudgetViewStorage { + + private static final Logger logger = LogsCenter.getLogger(BudgetViewStorage.class); + + private static final String STORAGE_DELIMITER = "\n"; + + private static final File DEFAULT_USER_DIRECTORY = new File("data" + File.separator + "duke"); + private static final File BUDGETVIEW_FILE = new File(DEFAULT_USER_DIRECTORY, "budgetView.txt"); + + /** + * Constructor of BudgetViewStorage object. + * + * @throws IOException if the file cannot be created or read. + */ + public BudgetViewStorage() throws IOException { + FileUtil.createIfMissing(BUDGETVIEW_FILE.toPath()); + logger.info("budgetView.txt file created"); + } + + /** + * Writes to the save file. + * + * @throws DukeException if unable to save the file successfully + */ + public void saveBudgetView(BudgetView budgetView) throws DukeException { + try { + Map budgetViewCategory = budgetView.getBudgetViewCategory(); + BUDGETVIEW_FILE.createNewFile(); + try (FileWriter fileWriter = new FileWriter(BUDGETVIEW_FILE)) { + if (!budgetViewCategory.isEmpty()) { + for (Integer view : budgetViewCategory.keySet()) { + String category = budgetViewCategory.get(view); + fileWriter.write(view + " " + category); + fileWriter.write(STORAGE_DELIMITER); + } + } + } + } catch (IOException e) { + throw new DukeException(String.format(DukeException.MESSAGE_SAVE_FILE_FAILED, BUDGETVIEW_FILE.getPath())); + } + } + + /** + * loads from the save file. + * + * @throws DukeException if the file cannot be created or read.if the file cannot be created or read. + * @throws IOException if the file cannot be created or read. + */ + public BudgetView loadBudgetView() throws DukeException, IOException { + BUDGETVIEW_FILE.createNewFile(); + Map budgetViewCategory = new HashMap<>(); + try (Scanner fileReader = new Scanner(BUDGETVIEW_FILE).useDelimiter(STORAGE_DELIMITER)) { + while (fileReader.hasNext()) { + String line = fileReader.next(); + String[] separatedLine = line.split(" ", 2); + int view = Integer.parseInt(separatedLine[0]); + budgetViewCategory.put(view, separatedLine[1]); + } + } catch (IOException | NumberFormatException | IllegalStateException e) { + logger.info("BudgetView file is corrupted! Overwriting budgetView.txt..."); + } + return new BudgetView(budgetViewCategory); + } +} diff --git a/src/main/java/duke/storage/ExpenseListStorage.java b/src/main/java/duke/storage/ExpenseListStorage.java new file mode 100644 index 0000000000..ed72089474 --- /dev/null +++ b/src/main/java/duke/storage/ExpenseListStorage.java @@ -0,0 +1,11 @@ +package duke.storage; + +import duke.exception.DukeException; +import duke.model.ExpenseList; + +public interface ExpenseListStorage { + + public void saveExpenseList(ExpenseList expenseList) throws DukeException; + + public ExpenseList loadExpenseList() throws DukeException; +} diff --git a/src/main/java/duke/storage/ExpenseListStorageManager.java b/src/main/java/duke/storage/ExpenseListStorageManager.java new file mode 100644 index 0000000000..15508a2328 --- /dev/null +++ b/src/main/java/duke/storage/ExpenseListStorageManager.java @@ -0,0 +1,58 @@ +package duke.storage; + +import duke.exception.DukeException; +import duke.model.Expense; +import duke.model.ExpenseList; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class ExpenseListStorageManager implements ExpenseListStorage { + + private static final File DEFAULT_USER_DIRECTORY = new File("data" + File.separator + "duke"); + private static final File EXPENSES_FILE = new File(DEFAULT_USER_DIRECTORY, "expenses.txt"); + + + + private static String STORAGE_DELIMITER = "\n\n"; + + public ExpenseListStorageManager() { + DEFAULT_USER_DIRECTORY.mkdirs(); + } + + @Override + public void saveExpenseList(ExpenseList expenseList) throws DukeException { + try { + EXPENSES_FILE.createNewFile(); + try (FileWriter fileWriter = new FileWriter(EXPENSES_FILE)) { + for (Expense expense : expenseList.getInternalList()) { + fileWriter.write(expense.toStorageString()); + fileWriter.write(STORAGE_DELIMITER); + } + } + } catch (IOException e) { + throw new DukeException(String.format(DukeException.MESSAGE_SAVE_FILE_FAILED, EXPENSES_FILE.getPath())); + } + } + + @Override + public ExpenseList loadExpenseList() { + List internalList = new ArrayList(); + try { + EXPENSES_FILE.createNewFile(); + try (Scanner fileReader = new Scanner(EXPENSES_FILE).useDelimiter(STORAGE_DELIMITER)) { + while (fileReader.hasNext()) { + internalList.add(ExpenseList.itemFromStorageString(fileReader.next())); + } + } + } catch (IOException | DukeException e) { + return new ExpenseList(internalList); + } + return new ExpenseList(internalList); + } +} diff --git a/src/main/java/duke/storage/IncomeListStorage.java b/src/main/java/duke/storage/IncomeListStorage.java new file mode 100644 index 0000000000..73de6da2cf --- /dev/null +++ b/src/main/java/duke/storage/IncomeListStorage.java @@ -0,0 +1,11 @@ +package duke.storage; + +import duke.exception.DukeException; +import duke.model.IncomeList; + +public interface IncomeListStorage { + + public void saveIncomeList(IncomeList incomeList) throws DukeException; + + public IncomeList loadIncomeList() throws DukeException; +} diff --git a/src/main/java/duke/storage/IncomeListStorageManager.java b/src/main/java/duke/storage/IncomeListStorageManager.java new file mode 100644 index 0000000000..2c2b21b22f --- /dev/null +++ b/src/main/java/duke/storage/IncomeListStorageManager.java @@ -0,0 +1,86 @@ +package duke.storage; + +import duke.commons.FileUtil; +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.model.Income; +import duke.model.IncomeList; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +/** + * Settles the storage of incomeList. + */ +public class IncomeListStorageManager implements IncomeListStorage { + + private static final Logger logger = LogsCenter.getLogger(IncomeListStorageManager.class); + + private static final File DEFAULT_USER_DIRECTORY = new File("data" + File.separator + "duke"); + private static final File INCOME_FILE = new File(DEFAULT_USER_DIRECTORY, "income.txt"); + + private static String STORAGE_DELIMITER = "\n\n"; + + /** + * Constructor of IncomeListStorageManager. + * + * @throws IOException if I/O error is encountered + */ + public IncomeListStorageManager() throws IOException { + FileUtil.createIfMissing(INCOME_FILE.toPath()); + logger.info("income.txt file created"); + } + + /** + * Saves and stores the incomeList to income.txt by overwriting income.txt. + * + * @param incomeList the updated incomeList right before termination of Duke++ + * @throws DukeException if I/O error is encountered + */ + @Override + public void saveIncomeList(IncomeList incomeList) throws DukeException { + try { + try (FileWriter fileWriter = new FileWriter(INCOME_FILE)) { + for (Income income : incomeList.getInternalList()) { + fileWriter.write(income.toStorageString()); + fileWriter.write(STORAGE_DELIMITER); + } + } + } catch (IOException e) { + throw new DukeException(String.format( + DukeException.MESSAGE_SAVE_FILE_FAILED, INCOME_FILE.getPath())); + } + } + + /** + * Loads incomeList from income.txt. + * Creates a new incomeList if income.txt is corrupted. + * + * @return IncomeList(newList) new incomeList + * @throws DukeException if file is corrupted + */ + @Override + public IncomeList loadIncomeList() throws DukeException { + List internalList = new ArrayList(); + try { + try (Scanner fileReader = new Scanner(INCOME_FILE).useDelimiter(STORAGE_DELIMITER)) { + while (fileReader.hasNext()) { + internalList.add(IncomeList.itemFromStorageString(fileReader.next())); + } + } + requireNonNull(internalList); + return new IncomeList(internalList); + } catch (IOException | DukeException | IllegalStateException e) { + logger.info("Income file is corrupted! Overwriting current income.txt file..."); + List newList = new ArrayList(); + return new IncomeList(newList); + } + } +} diff --git a/src/main/java/duke/storage/PlanAttributesStorage.java b/src/main/java/duke/storage/PlanAttributesStorage.java new file mode 100644 index 0000000000..f610b315ed --- /dev/null +++ b/src/main/java/duke/storage/PlanAttributesStorage.java @@ -0,0 +1,12 @@ +package duke.storage; + +import duke.exception.DukeException; + +import java.util.Map; + +public interface PlanAttributesStorage { + + public void savePlanAttributes(Map attributes) throws DukeException; + + public Map loadAttributes(); +} diff --git a/src/main/java/duke/storage/PlanAttributesStorageManager.java b/src/main/java/duke/storage/PlanAttributesStorageManager.java new file mode 100644 index 0000000000..a5ce7d66fd --- /dev/null +++ b/src/main/java/duke/storage/PlanAttributesStorageManager.java @@ -0,0 +1,65 @@ +package duke.storage; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.logging.Logger; + +public class PlanAttributesStorageManager implements PlanAttributesStorage { + + private static final Logger logger = LogsCenter.getLogger(PlanAttributesStorageManager.class); + + private static final File DEFAULT_USER_DIRECTORY = new File("data" + File.separator + "duke"); + private static final File PLAN_ATTRIBUTES_FILE = new File(DEFAULT_USER_DIRECTORY, "planAttributes.txt"); + + private static String STORAGE_DELIMITER = "\n\n"; + + public PlanAttributesStorageManager() { + DEFAULT_USER_DIRECTORY.mkdir(); + } + + @Override + public void savePlanAttributes(Map attributes) throws DukeException { + try { + PLAN_ATTRIBUTES_FILE.createNewFile(); + try (FileWriter fileWriter = new FileWriter(PLAN_ATTRIBUTES_FILE)) { + for (String key : attributes.keySet()) { + String value = attributes.get(key); + fileWriter.write(key + " " + value); + fileWriter.write(STORAGE_DELIMITER); + } + } + } catch (IOException e) { + throw new DukeException(String.format(DukeException + .MESSAGE_SAVE_FILE_FAILED, PLAN_ATTRIBUTES_FILE.getPath())); + } + + } + + @Override + public Map loadAttributes() { + Map attributes = new HashMap<>(); + try { + PLAN_ATTRIBUTES_FILE.createNewFile(); + try (Scanner scanner = new Scanner(PLAN_ATTRIBUTES_FILE).useDelimiter(STORAGE_DELIMITER)) { + while (scanner.hasNext()) { + String keyValue = scanner.next(); + String[] keyValueArr = keyValue.split(" "); + if (keyValueArr.length == 2) { + attributes.put(keyValueArr[0], keyValueArr[1]); + } + } + } + } catch (IOException e) { + logger.warning("Error loading planAttributes Storage, starting with a empty file"); + return new HashMap<>(); + } + return attributes; + } +} diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java new file mode 100644 index 0000000000..fc9fba7630 --- /dev/null +++ b/src/main/java/duke/storage/Storage.java @@ -0,0 +1,56 @@ +package duke.storage; + +import duke.exception.DukeException; +import duke.model.Budget; +import duke.model.BudgetView; +import duke.model.ExpenseList; +import duke.model.payment.PaymentList; +import duke.model.IncomeList; + + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +/** + * API of the Storage component. + */ +public interface Storage { + + void saveExpenseList(ExpenseList expenseList) throws DukeException; + + ExpenseList loadExpenseList() throws DukeException; + + void savePlanAttributes(Map attributes) throws DukeException; + + Map loadPlanAttributes(); + + void saveIncomeList(IncomeList incomeList) throws DukeException; + + IncomeList loadIncomeList() throws DukeException; + + Budget loadBudget() throws IOException, DukeException; + + void saveBudget(Budget budget) throws DukeException; + + BudgetView loadBudgetView() throws IOException, DukeException; + + void saveBudgetView(BudgetView budgetView) throws DukeException; + + /** + * Loads paymentList from storage. + * + * @return PaymentList as Optional. + * @throws DukeException If errors occur during loading process. + */ + Optional loadPaymentList() throws DukeException; + + /** + * Saves paymentList into storage. + * + * @param paymentList The paymentList to be saved in storage. + * @throws IOException If errors occur during saving process. + */ + void savePaymentList(PaymentList paymentList) throws IOException; + +} diff --git a/src/main/java/duke/storage/StorageManager.java b/src/main/java/duke/storage/StorageManager.java new file mode 100644 index 0000000000..c9dd600c10 --- /dev/null +++ b/src/main/java/duke/storage/StorageManager.java @@ -0,0 +1,117 @@ +package duke.storage; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.model.Budget; +import duke.model.BudgetView; +import duke.model.ExpenseList; +import duke.model.payment.PaymentList; +import duke.storage.payment.PaymentListStorage; +import duke.model.IncomeList; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * Manages storage of Duke++ data in local storage. + */ +public class StorageManager implements Storage { + + private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + + private ExpenseListStorage expenseListStorage; + private PlanAttributesStorage planAttributesStorage; + private IncomeListStorage incomeListStorage; + private BudgetStorage budgetStorage; + private BudgetViewStorage budgetViewStorage; + private PaymentListStorage paymentListStorage; + + + /** + * Constructs StorageManager with storage of each models. + * + * @param expenseListStorage storage for expenseList + * @param planAttributesStorage storage for PlanAttributes from PlanBot + * @param incomeListStorage storage for IncomeList + * @param budgetStorage storage for budget + * @param budgetViewStorage storage for budgetView + * @param paymentListStorage storage for paymentList + */ + public StorageManager(ExpenseListStorage expenseListStorage, + PlanAttributesStorage planAttributesStorage, + IncomeListStorage incomeListStorage, + BudgetStorage budgetStorage, + BudgetViewStorage budgetViewStorage, + PaymentListStorage paymentListStorage) { + + this.expenseListStorage = expenseListStorage; + this.planAttributesStorage = planAttributesStorage; + this.incomeListStorage = incomeListStorage; + this.budgetStorage = budgetStorage; + this.budgetViewStorage = budgetViewStorage; + this.paymentListStorage = paymentListStorage; + } + + @Override + public void saveExpenseList(ExpenseList expenseList) throws DukeException { + expenseListStorage.saveExpenseList(expenseList); + } + + @Override + public ExpenseList loadExpenseList() throws DukeException { + return expenseListStorage.loadExpenseList(); + } + + @Override + public void savePlanAttributes(Map attributes) throws DukeException { + planAttributesStorage.savePlanAttributes(attributes); + } + + @Override + public Map loadPlanAttributes() { + return planAttributesStorage.loadAttributes(); + } + + @Override + public void saveIncomeList(IncomeList incomeList) throws DukeException { + incomeListStorage.saveIncomeList(incomeList); + } + + @Override + public IncomeList loadIncomeList() throws DukeException { + return incomeListStorage.loadIncomeList(); + } + + @Override + public Budget loadBudget() throws IOException, DukeException { + return budgetStorage.loadBudget(); + } + + @Override + public void saveBudget(Budget budget) throws DukeException { + budgetStorage.saveBudget(budget); + } + + @Override + public BudgetView loadBudgetView() throws IOException, DukeException { + return budgetViewStorage.loadBudgetView(); + } + + @Override + public void saveBudgetView(BudgetView budgetView) throws DukeException { + budgetViewStorage.saveBudgetView(budgetView); + } + + @Override + public Optional loadPaymentList() { + logger.info("start loading paymentList"); + return paymentListStorage.readPaymentList(); + } + + @Override + public void savePaymentList(PaymentList paymentList) throws IOException { + paymentListStorage.savePaymentList(paymentList); + } +} diff --git a/src/main/java/duke/storage/payment/JsonAdaptedPayment.java b/src/main/java/duke/storage/payment/JsonAdaptedPayment.java new file mode 100644 index 0000000000..5f9493df7a --- /dev/null +++ b/src/main/java/duke/storage/payment/JsonAdaptedPayment.java @@ -0,0 +1,98 @@ +package duke.storage.payment; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import duke.exception.DukeException; +import duke.model.payment.Payment; + +import java.time.format.DateTimeFormatter; + +import static java.util.Objects.requireNonNull; + +/** + * Jackson-friendly version of Payment. + */ +public class JsonAdaptedPayment { + + private String description; + private String receiver; + private String due; + private String remark; + private String amount; + private String priority; + + /** + * Constructs a {@code JsonAdaptedPayment} with the given payment details. + */ + @JsonCreator + public JsonAdaptedPayment(@JsonProperty("description") String description, + @JsonProperty("receiver") String receiver, + @JsonProperty("due") String due, + @JsonProperty("remark") String remark, + @JsonProperty("amount") String amount, + @JsonProperty("priority") String priority) { + + this.description = description; + this.receiver = receiver; + this.due = due; + this.remark = remark; + this.amount = amount; + this.priority = priority; + } + + /** + * Converts a given {@code Payment} into this class for Jackson use. + */ + public JsonAdaptedPayment(Payment source) { + requireNonNull(source); + + description = source.getDescription(); + receiver = source.getReceiver(); + due = source.getDue().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + remark = source.getTag(); + amount = source.getAmount().toString(); + priority = source.getPriority().toString(); + } + + /** + * Converts this Jackson-friendly adapted payment object into the model's {@code Payment} object. + * + * @throws DukeException if there were any data constraints violated in the adapted payment. + */ + public Payment toModelType() throws DukeException { + Payment.Builder paymentBuilder = new Payment.Builder(); + + if (description == null) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_STORAGE_MISSING_FIELD, "description")); + } + paymentBuilder.setDescription(description); + + if (receiver == null) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_STORAGE_MISSING_FIELD, "receiver")); + } + paymentBuilder.setReceiver(receiver); + + if (due == null) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_STORAGE_MISSING_FIELD, "due")); + } + paymentBuilder.setDue(due); + + if (remark == null) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_STORAGE_MISSING_FIELD, "remark")); + } + paymentBuilder.setTag(remark); + + if (amount == null) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_STORAGE_MISSING_FIELD, "amount")); + } + paymentBuilder.setAmount(amount); + + if (priority == null) { + throw new DukeException(String.format(DukeException.MESSAGE_PAYMENT_STORAGE_MISSING_FIELD, "priority")); + } + paymentBuilder.setPriority(priority); + + return paymentBuilder.build(); + } + +} diff --git a/src/main/java/duke/storage/payment/JsonSerializablePaymentList.java b/src/main/java/duke/storage/payment/JsonSerializablePaymentList.java new file mode 100644 index 0000000000..d2cb0575f3 --- /dev/null +++ b/src/main/java/duke/storage/payment/JsonSerializablePaymentList.java @@ -0,0 +1,58 @@ +package duke.storage.payment; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import duke.exception.DukeException; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; + +/** + * A PaymentList that is serializable to JSON format. + */ +@JsonRootName(value = "paymentList") +public class JsonSerializablePaymentList { + + private final List payments = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializablePaymentList} with the given payments. + */ + @JsonCreator + public JsonSerializablePaymentList(@JsonProperty("payments") List payments) { + this.payments.addAll(payments); + } + + /** + * Converts a given {@code PaymentList} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializablePaymentList}. + */ + public JsonSerializablePaymentList(PaymentList source) { + requireNonNull(source); + + payments.addAll(source.getInternalList().stream() + .map(JsonAdaptedPayment::new).collect(Collectors.toList())); + } + + /** + * Converts this {@code payments} into the model's {@code PaymentList} object. + * + * @throws DukeException if there were any data constraints violated. + */ + public PaymentList toModelType() throws DukeException { + List internalPaymentList = new ArrayList<>(); + for (JsonAdaptedPayment jsonAdaptedPayment : payments) { + Payment payment = jsonAdaptedPayment.toModelType(); + internalPaymentList.add(payment); + } + return new PaymentList(internalPaymentList); + } + +} diff --git a/src/main/java/duke/storage/payment/PaymentListStorage.java b/src/main/java/duke/storage/payment/PaymentListStorage.java new file mode 100644 index 0000000000..e22bb88614 --- /dev/null +++ b/src/main/java/duke/storage/payment/PaymentListStorage.java @@ -0,0 +1,30 @@ +package duke.storage.payment; + +import duke.model.payment.PaymentList; + +import java.io.IOException; +import java.util.Optional; + +/** + * Represents a storage for duke.model.payment.PaymentList. + */ +public interface PaymentListStorage { + + /** + * Returns PaymentList data as a PaymentList. + * + * Returns {@code Optional.empty()} if storage file is not found, + * if the data in storage is not in the expected format, or + * if there was any problem when reading from the storage. + */ + Optional readPaymentList(); + + /** + * Saves the given PaymentList to the storage. + * + * @param paymentList cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void savePaymentList(PaymentList paymentList) throws IOException; + +} diff --git a/src/main/java/duke/storage/payment/PaymentListStorageManager.java b/src/main/java/duke/storage/payment/PaymentListStorageManager.java new file mode 100644 index 0000000000..05e75418c1 --- /dev/null +++ b/src/main/java/duke/storage/payment/PaymentListStorageManager.java @@ -0,0 +1,75 @@ +package duke.storage.payment; + +import static java.util.Objects.requireNonNull; + +import duke.commons.FileUtil; +import duke.commons.JsonUtil; +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.model.payment.PaymentList; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * A class to access PaymentList data stored as a json file on the hard disk. + */ +public class PaymentListStorageManager implements PaymentListStorage { + + private static final Logger logger = LogsCenter.getLogger(PaymentListStorageManager.class); + + private static final File DEFAULT_USER_DIRECTORY = new File("data" + File.separator + "duke"); + private static final File PAYMENTS_FILE = new File(DEFAULT_USER_DIRECTORY, "payments.txt"); + private static final Path filePath = PAYMENTS_FILE.toPath(); + + /** + * Creates a {@code PaymentListStorageManager}. + * Locates the file storing the PaymentList data. + * If the file is not found, it will create a new file at the location. + * + * @throws IOException if errors occur when creating the file. + */ + public PaymentListStorageManager() throws IOException { + FileUtil.createIfMissing(filePath); + logger.info("PaymentList.txt has been located."); + } + + @Override + public Optional readPaymentList() { + + // Returns a new empty paymentList if the file is blank. + if (PAYMENTS_FILE.length() == 0) { + return Optional.of(new PaymentList()); + } + + Optional jsonPaymentList; + try { + jsonPaymentList = JsonUtil.readJsonFile(filePath, JsonSerializablePaymentList.class); + } catch (DukeException e) { + logger.warning("Json file has format errors!"); + return Optional.of(new PaymentList()); // Returns an empty paymentList as alternative. + } + + if (jsonPaymentList.isEmpty()) { + return Optional.of(new PaymentList()); // Returns an empty paymentList as alternative. + } + + try { + return Optional.of(jsonPaymentList.get().toModelType()); + } catch (DukeException e) { + logger.warning("Illegal values found in " + filePath + ": " + e.getMessage()); + return Optional.of(new PaymentList()); // Returns an empty paymentList as alternative. + } + + } + + @Override + public void savePaymentList(PaymentList paymentList) throws IOException { + requireNonNull(paymentList); + JsonUtil.saveJsonFile(new JsonSerializablePaymentList(paymentList), filePath); + } + +} diff --git a/src/main/java/duke/ui/BudgetBar.java b/src/main/java/duke/ui/BudgetBar.java new file mode 100644 index 0000000000..918e271321 --- /dev/null +++ b/src/main/java/duke/ui/BudgetBar.java @@ -0,0 +1,167 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.logic.Logic; +import javafx.fxml.FXML; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * Pane that reflects the various budget views. + */ +public class BudgetBar extends UiPart { + private static final Logger logger = LogsCenter.getLogger(BudgetBar.class); + + private static final String FXML_FILE_NAME = "BudgetBar.fxml"; + public Logic logic; + private Map budgetBars = new HashMap<>(); + + @FXML + GridPane gridPane; + + @FXML + VBox vbox1; + + @FXML + VBox vbox2; + + @FXML + VBox vbox3; + + @FXML + VBox vbox4; + + @FXML + VBox vbox5; + + @FXML + VBox vbox6; + + /** + * Constructor of BudgetBar. + * + * @param logic logic + */ + public BudgetBar(Logic logic) { + super(FXML_FILE_NAME,null); + this.logic = logic; + gridPane.setStyle("-fx-background-color: mintcream;"); + gridPane.setGridLinesVisible(true); + gridPane.setSnapToPixel(true); + + for (int viewPane = 1; viewPane <= 6; viewPane++) { + ProgressBar bar = new ProgressBar(); + Text category = new Text(); + Text remaining = new Text(); + + remaining.setStyle("-fx-font-size: 16px;"); + category.setStyle("-fx-font-size: 25px;"); + + bar.setPrefWidth(250); + bar.setPrefHeight(30); + bar.setProgress(percentage(viewPane,logic)); + budgetBars.put(viewPane,bar); + + if (percentage(viewPane,logic) > 0.9) { + bar.setStyle("-fx-accent: red;"); + } else if (percentage(viewPane,logic) > 0.65) { + bar.setStyle("-fx-accent: orange;"); + } else if (percentage(viewPane,logic) > 0.40) { + bar.setStyle("-fx-accent: yellow"); + } else { + bar.setStyle("-fx-accent: green"); + } + + if (percentage(viewPane,logic) < 1) { + if (remainder(viewPane,logic).compareTo(BigDecimal.ZERO) == 0) { + remaining.setText(" No budget set."); + } else { + remaining.setText(" Remaining budget: $" + remainder(viewPane, logic)); + } + } else if (percentage(viewPane,logic) == 1) { + remaining.setText(" Budget of " + logic.getBudgetTag( + logic.getBudgetViewCategory().get(viewPane)) + " reached!"); + } else { + remaining.setText(" Exceeded budget by $" + remainder(viewPane,logic).negate() + "!"); + } + + if (!logic.getBudgetViewCategory().containsKey(viewPane)) { + bar.setVisible(false); + remaining.setVisible(false); + category.setText("Type \"viewBudget " + viewPane + + " /tag #category\" to add a budget view in this pane."); + category.setWrappingWidth(140); + category.setStyle("-fx-font-size: 12px;"); + category.setTextAlignment(TextAlignment.CENTER); + } else { + String tag = logic.getBudgetViewCategory().get(viewPane); + category.setText(tag.toUpperCase()); + } + + if (viewPane == 1) { + vbox1.getChildren().addAll(category, bar, remaining); + vbox1.setSpacing(10); + } else if (viewPane == 2) { + vbox2.getChildren().addAll(category, bar, remaining); + vbox2.setSpacing(10); + } else if (viewPane == 3) { + vbox3.getChildren().addAll(category, bar, remaining); + vbox3.setSpacing(10); + } else if (viewPane == 4) { + vbox4.getChildren().addAll(category, bar, remaining); + vbox4.setSpacing(10); + } else if (viewPane == 5) { + vbox5.getChildren().addAll(category, bar, remaining); + vbox5.setSpacing(10); + } else { + vbox6.getChildren().addAll(category, bar, remaining); + vbox6.setSpacing(10); + } + } + } + + /** + * Returns a double percentage of the budget set that has been spent already. + * Further reflected in the individual bars. + * + * @param viewPane the specified pane + * @param logic logic + * @return percent of budget spent + */ + private double percentage(int viewPane, Logic logic) { + String category = logic.getBudgetViewCategory().get(viewPane); + double percent = logic.getTagAmount(category).doubleValue() / logic.getBudgetTag(category).doubleValue(); + + //In the case where percent is NaN (Not a Number) + if (Double.isNaN(percent)) { + percent = 0.0; + } + + return percent; + } + + /** + * Returns a BigDecimal remainder of the budget set that has not been spent + * Further reflected in the individual panes. + * + * @param viewPane the specified pane + * @param logic logic + * @return remaining leftover of the budget yet to be spent + */ + private BigDecimal remainder(int viewPane, Logic logic) { + String category = logic.getBudgetViewCategory().get(viewPane); + BigDecimal remaining = logic.getBudgetTag(category).subtract(logic.getTagAmount(category)); + + return remaining; + } + +} diff --git a/src/main/java/duke/ui/BudgetPane.java b/src/main/java/duke/ui/BudgetPane.java new file mode 100644 index 0000000000..e565a654af --- /dev/null +++ b/src/main/java/duke/ui/BudgetPane.java @@ -0,0 +1,125 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.logic.Logic; +import duke.model.Income; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; + +import java.math.BigDecimal; +import java.util.logging.Logger; + +public class BudgetPane extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(BudgetPane.class); + + private static final String FXML_FILE_NAME = "BudgetPane.fxml"; + + @FXML + private ListView incomeListView; + + @FXML + private Label incomeLabel; + + @FXML + Label totalIncomeLabel; + + @FXML + private Pane paneView; + + @FXML + private Pane paneBudgetView; + + @FXML + private ListView budgetListView; + + public Logic logic; + + /** + * Constructor for BudgetPane. + * + * @param incomeList the list of income from storage + * @param logic logic + * @param totalIncome total income calculated from storage + */ + BudgetPane(ObservableList incomeList, Logic logic, StringProperty totalIncome) { + super(FXML_FILE_NAME, null); + logger.info("incomeList has length " + incomeList.size()); + Label emptyIncomeListPlaceholder = new Label(); + emptyIncomeListPlaceholder.setText("No income entered yet! " + + "Type \"addIncome #amount /d #source\" to add."); + emptyIncomeListPlaceholder.setWrapText(true); + emptyIncomeListPlaceholder.setTextAlignment(TextAlignment.CENTER); + incomeListView.setPlaceholder(emptyIncomeListPlaceholder); + incomeListView.setItems(incomeList); + logger.info("Items are set."); + incomeListView.setCellFactory(incomeListView -> new IncomeListViewCell()); + logger.info("cell factory is set."); + incomeLabel.setText("Income"); + incomeLabel.setStyle("-fx-text-fill:gold; -fx-font-size: 20px;"); + + this.logic = logic; + + Text text = new Text(); + ProgressBar overallBudget = new ProgressBar(); + double percent = logic.getTotalAmount().doubleValue() / logic.getMonthlyBudget().doubleValue(); + overallBudget.setProgress(percent); + BigDecimal remaining = logic.getRemaining(logic.getTotalAmount()); + if ((remaining.compareTo(BigDecimal.ZERO) < 0)) { + text.setText("Remaining: -$" + remaining.negate()); + } else { + text.setText("Remaining: $" + remaining); + } + text.setStyle("-fx-font-size: 20px;"); + if (percent > 0.9) { + overallBudget.setStyle("-fx-accent: red;"); + } else if (percent > 0.65) { + overallBudget.setStyle("-fx-accent: orange;"); + } else if (percent > 0.40) { + overallBudget.setStyle("-fx-accent: yellow"); + } else { + overallBudget.setStyle("-fx-accent: green"); + } + overallBudget.setLayoutX(150); + overallBudget.setPrefWidth(500); + overallBudget.setPrefHeight(30); + text.setLayoutX(300); + text.setLayoutY(60); + paneView.getChildren().clear(); + paneView.getChildren().add(overallBudget); + paneView.getChildren().add(text); + + paneBudgetView.getChildren().clear(); + paneBudgetView.getChildren().add(new BudgetBar(logic).getRoot()); + + budgetListView.setItems(logic.getBudgetObservableList()); + totalIncomeLabel.textProperty().bindBidirectional(totalIncome); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Income} + * using a {@code IncomeList}. + */ + class IncomeListViewCell extends ListCell { + @Override + protected void updateItem(Income income, boolean empty) { + super.updateItem(income, empty); + if (empty || income == null) { + setGraphic(null); + setText(null); + } else { + int index = incomeListView.getItems().indexOf(income) + 1; + setGraphic(new IncomeCard(income, index).getRoot()); + } + } + } +} diff --git a/src/main/java/duke/ui/DialogBox.java b/src/main/java/duke/ui/DialogBox.java new file mode 100644 index 0000000000..75d4b5564c --- /dev/null +++ b/src/main/java/duke/ui/DialogBox.java @@ -0,0 +1,46 @@ +package duke.ui; + +import duke.model.PlanBot; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + +public class DialogBox extends UiPart { + private static final String FXML_FILE_NAME = "DialogBox.fxml"; + public final PlanBot.PlanDialog dialog; + + + @FXML + private Label text; + @FXML + VBox textContainer; + + /** + * Controller for dialog box in PlanPane. + * @param dialog the dialog object given by PlanBot + */ + public DialogBox(PlanBot.PlanDialog dialog) { + super(FXML_FILE_NAME, null); + this.dialog = dialog; + text.setText(dialog.text); + text.setBackground(new Background(new BackgroundFill(Color.rgb(233,170,27), + new CornerRadii(5), Insets.EMPTY))); + if (dialog.agent == PlanBot.Agent.USER) { + textContainer.setAlignment(Pos.BOTTOM_RIGHT); + } else { + text.setAlignment(Pos.BOTTOM_LEFT); + text.setTextFill(Color.rgb(253,214,108)); + text.setBackground(new Background(new BackgroundFill(Color.rgb(49,66,86), + new CornerRadii(5), Insets.EMPTY))); + } + } + + +} diff --git a/src/main/java/duke/ui/ExpenseCard.java b/src/main/java/duke/ui/ExpenseCard.java new file mode 100644 index 0000000000..0ada5cc819 --- /dev/null +++ b/src/main/java/duke/ui/ExpenseCard.java @@ -0,0 +1,50 @@ +package duke.ui; + +import duke.model.Expense; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + + +public class ExpenseCard extends UiPart { + private static final String FXML_FILE_NAME = "ExpenseCard.fxml"; + public final Expense expense; + + @FXML + private Label description; + @FXML + private Label amount; + @FXML + private Label tag; + @FXML + private Label date; + @FXML + private VBox expenseContainer; + + /** + * Constructor of controller for ExpenseCard.fxml. + * @param expense The Expense object we wish to display + * @param index the int index of the current Expense in list of Expenses we are displaying + */ + public ExpenseCard(Expense expense, int index) { + super(FXML_FILE_NAME, null); + this.expense = expense; + description.setText(index + ". " + expense.getDescription()); + amount.setText("$" + expense.getAmount().toString()); + tag.setText("Tag: " + expense.getTag()); + date.setText(expense.getTimeString()); + if (expense.isRecurring()) { + description.setTextFill(Color.GREEN); + amount.setTextFill(Color.GREEN); + tag.setTextFill(Color.GREEN); + date.setTextFill(Color.GREEN); + } else if (expense.isTentative()) { + description.setTextFill(Color.GRAY); + amount.setTextFill(Color.GRAY); + tag.setTextFill(Color.GRAY); + date.setTextFill(Color.GRAY); + } + } +} diff --git a/src/main/java/duke/ui/ExpensePane.java b/src/main/java/duke/ui/ExpensePane.java new file mode 100644 index 0000000000..f1580c94a4 --- /dev/null +++ b/src/main/java/duke/ui/ExpensePane.java @@ -0,0 +1,134 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.logic.Logic; +import duke.model.Expense; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.chart.PieChart; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; + +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +public class ExpensePane extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(ExpensePane.class); + + private static final String FXML_FILE_NAME = "ExpensePane.fxml"; + + @FXML + private Pane paneView; + private PieChart pieChartSample; + + @FXML + private ListView expenseListView; + + @FXML + private Label sortLabel; + + @FXML + private Label viewLabel; + + @FXML + private Label totalLabel; + + public Logic logic; + public Set tags; + + /** + * Constructor for ExpensePane, the controller class for ExpensePane.fxml. + * @param expenseList ObservableList of Expense objects. + * @param logic the Logic Object of Duke + * @param totalExpense StringProperty of the sum of displayed expenses + * @param filterCriteria StringProperty of the current filter criteria of ExpenseList + * (broken for now, to be fixed in future builds) + * @param sortCriteria StringProperty of the sortCriteria of ExpenseList + * @param viewCriteria StringProperty of the viewCriteria of ExpenseList + */ + public ExpensePane(ObservableList expenseList, Logic logic, + StringProperty totalExpense, + StringProperty filterCriteria, + StringProperty sortCriteria, + StringProperty viewCriteria) { + super(FXML_FILE_NAME, null); + logger.info("expenseList has length " + expenseList.size()); + logger.info("expenseList has length " + expenseList.size()); + Label emptyExpenseListPlaceholder = new Label(); + emptyExpenseListPlaceholder.setText("No Expenses yet. " + + "Type \"addExpense #amount\" to add one!"); + expenseListView.setPlaceholder(emptyExpenseListPlaceholder); + expenseListView.setItems(expenseList); + logger.info("Items are set."); + expenseListView.setCellFactory(expenseListView -> new ExpenseListViewCell()); + logger.info("cell factory is set."); + + sortLabel.textProperty().bindBidirectional(sortCriteria); + viewLabel.textProperty().bindBidirectional(viewCriteria); + totalLabel.textProperty().bindBidirectional(totalExpense); + + this.logic = logic; + PieChart pieChartSample = new PieChart(); + pieChartSample.setData(getData()); + paneView.getChildren().clear(); + pieChartSample.setTitle("Expenditure"); + paneView.getChildren().add(pieChartSample); + logger.info("Pie chart is set."); + } + + /** + * Retrieves the amounts for each specific tag. + * + * @return ObservableList of type PieChart.data to be passed into the pie chart + */ + private ObservableList getData() { + getTags(); + + ObservableList dataList = FXCollections.observableArrayList(); + + for (Object tag : this.tags) { + dataList.add(new PieChart.Data((String) tag, logic.getTagAmount((String) tag).doubleValue())); + } + return dataList; + } + + /** + * Retrieves all tags as shown in external list and stores in a set {@code tags}. + */ + private void getTags() { + tags = new HashSet<>(); + for (Expense expense : logic.getExternalExpenseList()) { + if (!expense.getTag().isEmpty()) { + tags.add(expense.getTag()); + } + } + } + + + /** + * Custom {@code ListCell} that displays the graphics of a {@code PlanBot.PlanDialog} + * using a {@code PlanBot.PlanDialog}. + */ + class ExpenseListViewCell extends ListCell { + @Override + protected void updateItem(Expense expense, boolean empty) { + super.updateItem(expense, empty); + if (empty || expense == null) { + setGraphic(null); + setText(null); + } else { + int index = expenseListView.getItems().indexOf(expense) + 1; + setGraphic(new ExpenseCard(expense, index).getRoot()); + } + } + } + + +} diff --git a/src/main/java/duke/ui/IncomeCard.java b/src/main/java/duke/ui/IncomeCard.java new file mode 100644 index 0000000000..9f2e2c13ff --- /dev/null +++ b/src/main/java/duke/ui/IncomeCard.java @@ -0,0 +1,33 @@ +package duke.ui; + +import duke.model.Income; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + + +public class IncomeCard extends UiPart { + private static final String FXML_FILE_NAME = "IncomeCard.fxml"; + public final Income income; + + @FXML + private Label description; + @FXML + private Label amount; + @FXML + private VBox incomeContainer; + + /** + * Constructor for incomeCard. + * + * @param income income from incomeList + * @param index the specific number of income in the list + */ + public IncomeCard(Income income, int index) { + super(FXML_FILE_NAME, null); + this.income = income; + description.setText(index + ". " + income.getDescription()); + amount.setText("$" + income.getAmount().toString()); + } +} diff --git a/src/main/java/duke/ui/MainWindow.java b/src/main/java/duke/ui/MainWindow.java new file mode 100644 index 0000000000..48c9d4dcb3 --- /dev/null +++ b/src/main/java/duke/ui/MainWindow.java @@ -0,0 +1,258 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.logic.CommandResult; +import duke.logic.Logic; +import duke.logic.util.AutoCompleter; +import duke.logic.util.InputHistory; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import java.util.logging.Logger; + +public class MainWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(MainWindow.class); + + private static final String FXML_FILE_NAME = "MainWindow.fxml"; + + private Stage primaryStage; + private Logic logic; + + private AutoCompleter autoCompleter; + private InputHistory inputHistory; + + private ExpensePane expensePane; + private TrendingPane trendingPane; + private PaymentPane paymentPane; + private BudgetPane budgetPane; + private PlanPane planPane; + + private CommandResult.DisplayedPane displayedPane; + + // The area that can be switched. + @FXML + private StackPane paneStack; + + // TextInput and TextOutput + @FXML + private Label console; + + @FXML + private TextField userInput; + + public void show() { + primaryStage.show(); + } + + /** + * Constructor for controller of the mainWindow. + * @param primaryStage Stage of Duke + * @param logic Logic object of duke + */ + public MainWindow(Stage primaryStage, Logic logic) { + super(FXML_FILE_NAME, primaryStage); + this.primaryStage = primaryStage; + this.logic = logic; + + displayedPane = CommandResult.DisplayedPane.EXPENSE; + if (logic.getExternalExpenseList().isEmpty()) { + //initial boot + displayedPane = CommandResult.DisplayedPane.PLAN; + } + fillInnerPart(); + + inputHistory = new InputHistory(); + autoCompleter = new AutoCompleter(); + + this.userInput.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.TAB) { + autoCompleter.receiveText(userInput.getText()); + String fullSuggestion = autoCompleter.getFullComplement(); + userInput.setText(fullSuggestion); + userInput.positionCaret(userInput.getText().length()); + event.consume(); + logger.info("Autocomplete finish"); + } + }); + } + + private void fillInnerPart() { + expensePane = new ExpensePane(logic.getExternalExpenseList(), + logic, + logic.getExpenseListTotalString(), + logic.getFilterCriteriaString(), + logic.getSortCriteriaString(), + logic.getViewCriteriaString()); + logger.info("The filled externalList length " + logic.getExternalExpenseList().size()); + trendingPane = new TrendingPane(); + logger.info("trendingPane is constructed."); + planPane = new PlanPane(logic.getDialogObservableList()); + logger.info("planPane is constructed." + logic.getDialogObservableList().size()); + budgetPane = new BudgetPane(logic.getExternalIncomeList(),logic,logic.getIncomeListTotalString()); + paymentPane = new PaymentPane(logic.getUnmodifiableFilteredPaymentList(), + logic.getPaymentSortingCriteria(), + logic.getPaymentPredicate()); + logger.info("Budget plane is constructed."); + + expensePane.getRoot().setVisible(false); + planPane.getRoot().setVisible(false); + trendingPane.getRoot().setVisible(false); + paymentPane.getRoot().setVisible(false); + budgetPane.getRoot().setVisible(false); + + paneStack.getChildren().add(expensePane.getRoot()); + paneStack.getChildren().add(planPane.getRoot()); + paneStack.getChildren().add(trendingPane.getRoot()); + paneStack.getChildren().add(paymentPane.getRoot()); + paneStack.getChildren().add(budgetPane.getRoot()); + + switch (displayedPane) { + case TRENDING: + trendingPane.getRoot().setVisible(true); + break; + + case PLAN: + planPane.getRoot().setVisible(true); + break; + + case BUDGET: + budgetPane.getRoot().setVisible(true); + break; + + case PAYMENT: + paymentPane.getRoot().setVisible(true); + break; + + default: //Expense pane + expensePane.getRoot().setVisible(true); + break; + } + } + + @FXML + private void handleUserInput() { + String inputString = userInput.getText(); + try { + CommandResult commandResult; + if (displayedPane == CommandResult.DisplayedPane.PLAN + && !inputString.contains("goto") + && !inputString.contains("bye")) { + commandResult = logic.execute("plan " + inputString); + } else { + commandResult = logic.execute(inputString); + } + console.setText(commandResult.getConsoleInfo()); + paneStack.getChildren().clear(); + fillInnerPart(); + showPane(commandResult); + + if (commandResult.isExit()) { + Platform.exit(); + } + } catch (DukeException e) { + console.setText(e.getMessage()); + } + + inputHistory.add(inputString); + userInput.clear(); + } + + @FXML + private void handleKeyPressed(KeyEvent keyEvent) { + switch (keyEvent.getCode()) { + case UP: + userInput.setText(inputHistory.getLastInput()); + userInput.positionCaret(userInput.getText().length()); + break; + + case DOWN: + userInput.setText(inputHistory.getNextInput()); + userInput.positionCaret(userInput.getText().length()); + break; + + default: + // other key events will be ignored. + break; + } + } + + + private void showPane(CommandResult commandResult) { + displayedPane = commandResult.getDisplayedPane(); + switch (displayedPane) { + case EXPENSE: + showExpensePane(); + break; + + case TRENDING: + showTrendingPane(); + break; + + case PLAN: + showPlanPane(); + break; + + case PAYMENT: + showPaymentPane(); + break; + + case BUDGET: + showBudgetPane(); + break; + + default: + break; + } + } + + private void showExpensePane() { + expensePane.getRoot().setVisible(true); + planPane.getRoot().setVisible(false); + trendingPane.getRoot().setVisible(false); + paymentPane.getRoot().setVisible(false); + budgetPane.getRoot().setVisible(false); + } + + private void showTrendingPane() { + expensePane.getRoot().setVisible(false); + planPane.getRoot().setVisible(false); + trendingPane.getRoot().setVisible(true); + paymentPane.getRoot().setVisible(false); + budgetPane.getRoot().setVisible(false); + } + + + private void showPlanPane() { + expensePane.getRoot().setVisible(false); + planPane.getRoot().setVisible(true); + trendingPane.getRoot().setVisible(false); + paymentPane.getRoot().setVisible(false); + budgetPane.getRoot().setVisible(false); + } + + private void showBudgetPane() { + expensePane.getRoot().setVisible(false); + planPane.getRoot().setVisible(false); + trendingPane.getRoot().setVisible(false); + budgetPane.getRoot().setVisible(true); + paymentPane.getRoot().setVisible(false); + } + + private void showPaymentPane() { + expensePane.getRoot().setVisible(false); + planPane.getRoot().setVisible(false); + trendingPane.getRoot().setVisible(false); + budgetPane.getRoot().setVisible(false); + paymentPane.getRoot().setVisible(true); + } + + +} diff --git a/src/main/java/duke/ui/PaymentBox.java b/src/main/java/duke/ui/PaymentBox.java new file mode 100644 index 0000000000..c9b7ee90e6 --- /dev/null +++ b/src/main/java/duke/ui/PaymentBox.java @@ -0,0 +1,91 @@ +package duke.ui; + +import duke.model.payment.Payment; +import duke.model.payment.Payment.Priority; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.paint.Color; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class PaymentBox extends UiPart { + + private static final String FXML_FILE_NAME = "PaymentBox.fxml"; + + private static final String PRIORITY_PREFIX = "Priority."; + + private final Payment payment; + + @FXML + private Label indexLabel; + + @FXML + private Label amountLabel; + + @FXML + private Label receiverLabel; + + @FXML + private Label dueLabel; + + @FXML + private Label descriptionLabel; + + @FXML + private Label priorityLabel; + + @FXML + private Label tagLabel; + + @FXML + private Label overdueLabel; + + /** + * + * PaymentBox. + * @param payment Payment + * @param displayedIndex Index + */ + public PaymentBox(Payment payment, int displayedIndex) { + super(FXML_FILE_NAME, null); + this.payment = payment; + + indexLabel.setText(displayedIndex + ". "); + amountLabel.setText("S$" + payment.getAmount().toString()); + receiverLabel.setText(payment.getReceiver()); + String due = payment.getDue().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + dueLabel.setText(due); + descriptionLabel.setText(payment.getDescription()); + + Priority priority = payment.getPriority(); + BackgroundFill backgroundFill; + switch (priority) { + case HIGH: + backgroundFill = new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY); + break; + case MEDIUM: + backgroundFill = new BackgroundFill(Color.ORANGE, CornerRadii.EMPTY, Insets.EMPTY); + break; + case LOW: + backgroundFill = new BackgroundFill(Color.GREEN, CornerRadii.EMPTY, Insets.EMPTY); + break; + default: + backgroundFill = new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY); + } + priorityLabel.setBackground(new Background(backgroundFill)); + priorityLabel.setText(PRIORITY_PREFIX + priority.toString()); + tagLabel.setText(payment.getTag()); + if (payment.getDue().isBefore(LocalDate.now())) { + overdueLabel.setVisible(true); + } + } + + + +} diff --git a/src/main/java/duke/ui/PaymentPane.java b/src/main/java/duke/ui/PaymentPane.java new file mode 100644 index 0000000000..8877e25397 --- /dev/null +++ b/src/main/java/duke/ui/PaymentPane.java @@ -0,0 +1,172 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.model.payment.Payment; +import duke.model.payment.PaymentList; +import duke.model.payment.PaymentInMonthPredicate; +import duke.model.payment.PaymentInWeekPredicate; +import duke.model.payment.PaymentOverduePredicate; +import duke.model.payment.SearchKeywordPredicate; +import javafx.beans.property.ObjectProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.AnchorPane; + +import java.util.function.Predicate; +import java.util.logging.Logger; + +public class PaymentPane extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(PaymentPane.class); + + private static final double FULL_OPACITY = 1; + private static final double FADED_OPACITY = 0.2; + + private static final String FXML_FILE_NAME = "PaymentPane.fxml"; + + private ObjectProperty sortingCriteria; + + private ObjectProperty predicate; + + // private ObservableList searchKeywordIndicator; + + @FXML + private Label overdueLabel; + + @FXML + private Label weekLabel; + + @FXML + private Label monthLabel; + + @FXML + private Label allLabel; + + @FXML + private Label searchLabel; + + @FXML + private Label timeLabel; + + @FXML + private Label amountLabel; + + @FXML + private Label priorityLabel; + + @FXML + private ListView paymentListView; + + + /** + * Constructor for PaymentPane. + * @param paymentList ObservableList<Payment> + * @param sortingCriteria ObjectProperty<PaymentList.SortingCriteria> + * @param predicate ObjectProperty<Predicate> + */ + public PaymentPane(ObservableList paymentList, + ObjectProperty sortingCriteria, + ObjectProperty predicate) { + super(FXML_FILE_NAME, null); + paymentListView.setItems(paymentList); + paymentListView.setCellFactory(listView -> new PaymentListViewCell()); + + this.sortingCriteria = sortingCriteria; + this.sortingCriteria.addListener((observable, oldValue, newValue) -> { + highlightSortLabel(); + }); + + this.predicate = predicate; + this.predicate.addListener((observable, oldValue, newValue) -> { + highlightPredicateLabel(); + }); + + highlightSortLabel(); + highlightPredicateLabel(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Payment} using a {@code PaymentBox}. + */ + class PaymentListViewCell extends ListCell { + @Override + protected void updateItem(Payment payment, boolean empty) { + super.updateItem(payment, empty); + + if (empty || payment == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new PaymentBox(payment, getIndex() + 1).getRoot()); + } + } + } + + private void highlightSortLabel() { + switch (sortingCriteria.getValue()) { + case TIME: + timeLabel.setOpacity(FULL_OPACITY); + amountLabel.setOpacity(FADED_OPACITY); + priorityLabel.setOpacity(FADED_OPACITY); + break; + + case AMOUNT: + timeLabel.setOpacity(FADED_OPACITY); + amountLabel.setOpacity(FULL_OPACITY); + priorityLabel.setOpacity(FADED_OPACITY); + break; + + case PRIORITY: + timeLabel.setOpacity(FADED_OPACITY); + amountLabel.setOpacity(FADED_OPACITY); + priorityLabel.setOpacity(FULL_OPACITY); + break; + + default: + logger.warning("Sorting Criteria takes unexpected value."); + break; + } + } + + private void highlightPredicateLabel() { + if (predicate.getValue() instanceof PaymentOverduePredicate) { + overdueLabel.setOpacity(FULL_OPACITY); + weekLabel.setOpacity(FADED_OPACITY); + monthLabel.setOpacity(FADED_OPACITY); + allLabel.setOpacity(FADED_OPACITY); + searchLabel.setOpacity(FADED_OPACITY); + + } else if (predicate.getValue() instanceof PaymentInWeekPredicate) { + overdueLabel.setOpacity(FADED_OPACITY); + weekLabel.setOpacity(FULL_OPACITY); + monthLabel.setOpacity(FADED_OPACITY); + allLabel.setOpacity(FADED_OPACITY); + searchLabel.setOpacity(FADED_OPACITY); + + } else if (predicate.getValue() instanceof PaymentInMonthPredicate) { + overdueLabel.setOpacity(FADED_OPACITY); + weekLabel.setOpacity(FADED_OPACITY); + monthLabel.setOpacity(FULL_OPACITY); + allLabel.setOpacity(FADED_OPACITY); + searchLabel.setOpacity(FADED_OPACITY); + + } else if (predicate.getValue() instanceof SearchKeywordPredicate) { + overdueLabel.setOpacity(FADED_OPACITY); + weekLabel.setOpacity(FADED_OPACITY); + monthLabel.setOpacity(FADED_OPACITY); + allLabel.setOpacity(FADED_OPACITY); + searchLabel.setOpacity(FULL_OPACITY); + + } else { + overdueLabel.setOpacity(FADED_OPACITY); + weekLabel.setOpacity(FADED_OPACITY); + monthLabel.setOpacity(FADED_OPACITY); + allLabel.setOpacity(FULL_OPACITY); + searchLabel.setOpacity(FADED_OPACITY); + } + } + +} diff --git a/src/main/java/duke/ui/PlanPane.java b/src/main/java/duke/ui/PlanPane.java new file mode 100644 index 0000000000..31dc85722e --- /dev/null +++ b/src/main/java/duke/ui/PlanPane.java @@ -0,0 +1,55 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.model.PlanBot; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.BorderPane; + +import java.util.logging.Logger; + +public class PlanPane extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(PlanPane.class); + + private static final String FXML_FILE_NAME = "PlanPane.fxml"; + + @FXML + private ListView dialogListView; + + /** + * Constructor for the controller. + * + * @param dialogObservableList a ObservableList of PlanDialog from PlanBot + */ + public PlanPane(ObservableList dialogObservableList) { + super(FXML_FILE_NAME, null); + dialogListView.setItems(dialogObservableList); + logger.info("DialogList set"); + dialogListView.setCellFactory(planDialogListView -> new PlanDialogListViewCell()); + Platform.runLater(() -> dialogListView.scrollTo(dialogObservableList.size() - 1)); + } + + + /** + * Custom {@code ListCell} that displays the graphics of a {@code PlanBot.PlanDialog} + * using a {@code PlanBot.PlanDialog}. + */ + static class PlanDialogListViewCell extends ListCell { + @Override + protected void updateItem(PlanBot.PlanDialog dialog, boolean empty) { + super.updateItem(dialog, empty); + if (empty || dialog == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new DialogBox(dialog).getRoot()); + } + } + } + + +} diff --git a/src/main/java/duke/ui/TrendingPane.java b/src/main/java/duke/ui/TrendingPane.java new file mode 100644 index 0000000000..865ce283c2 --- /dev/null +++ b/src/main/java/duke/ui/TrendingPane.java @@ -0,0 +1,13 @@ +package duke.ui; + +import javafx.scene.layout.AnchorPane; + +public class TrendingPane extends UiPart { + + private static final String FXML_FILE_NAME = "TrendingPane.fxml"; + + public TrendingPane() { + super(FXML_FILE_NAME, null); + } + +} diff --git a/src/main/java/duke/ui/Ui.java b/src/main/java/duke/ui/Ui.java new file mode 100644 index 0000000000..deb88e6966 --- /dev/null +++ b/src/main/java/duke/ui/Ui.java @@ -0,0 +1,8 @@ +package duke.ui; + +import javafx.stage.Stage; + +public interface Ui { + + public void start(Stage primaryStage); +} diff --git a/src/main/java/duke/ui/UiManager.java b/src/main/java/duke/ui/UiManager.java new file mode 100644 index 0000000000..3ffc55a137 --- /dev/null +++ b/src/main/java/duke/ui/UiManager.java @@ -0,0 +1,27 @@ +package duke.ui; + +import duke.commons.LogsCenter; +import duke.exception.DukeException; +import duke.logic.Logic; +import javafx.stage.Stage; + +import java.util.logging.Logger; + +public class UiManager implements Ui { + + private Logic logic; + private MainWindow mainWindow; + + private static final Logger logger = LogsCenter.getLogger(UiManager.class); + + public UiManager(Logic logic) { + this.logic = logic; + } + + @Override + public void start(Stage primaryStage) { + mainWindow = new MainWindow(primaryStage, logic); + mainWindow.show(); + logger.info("MainWindow are showed and filled in."); + } +} diff --git a/src/main/java/duke/ui/UiPart.java b/src/main/java/duke/ui/UiPart.java new file mode 100644 index 0000000000..6675e4329f --- /dev/null +++ b/src/main/java/duke/ui/UiPart.java @@ -0,0 +1,71 @@ +// Credit: Adopted from reference project addressbook-level3. + +package duke.ui; + +import static java.util.Objects.requireNonNull; + +import duke.Main; +import javafx.fxml.FXMLLoader; + +import java.io.IOException; +import java.net.URL; + +public abstract class UiPart { + + /** Resource folder where FXML files are stored. */ + public static final String FXML_FILE_FOLDER = "/view/"; + + private FXMLLoader fxmlLoader = new FXMLLoader(); + + /** + * Constructs a UiPart with the specified FXML file URL and root object. + * The FXML file must not specify the {@code fx:controller} attribute. + */ + public UiPart(URL fxmlFileUrl, T root) { + loadFxmlFile(fxmlFileUrl, root); + } + + /** + * Constructs a UiPart with the specified FXML file within {@link #FXML_FILE_FOLDER} and root object. + * @see #UiPart(URL, T) + */ + public UiPart(String fxmlFileName, T root) { + this(getFxmlFileUrl(fxmlFileName), root); + } + + + /** + * Returns the root object of the scene graph of this UiPart. + */ + public T getRoot() { + return fxmlLoader.getRoot(); + } + + /** + * Loads the object hierarchy from a FXML document. + * @param location Location of the FXML document. + * @param root Specifies the root of the object hierarchy. + */ + private void loadFxmlFile(URL location, T root) { + requireNonNull(location); + fxmlLoader.setLocation(location); + fxmlLoader.setController(this); + fxmlLoader.setRoot(root); + try { + fxmlLoader.load(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + /** + * Returns the FXML file URL for the specified FXML file name within {@link #FXML_FILE_FOLDER}. + */ + private static URL getFxmlFileUrl(String fxmlFileName) { + requireNonNull(fxmlFileName); + String fxmlFileNameWithFolder = FXML_FILE_FOLDER + fxmlFileName; + URL fxmlFileUrl = Main.class.getResource(fxmlFileNameWithFolder); + + return requireNonNull(fxmlFileUrl); + } +} diff --git a/src/main/java/duke/utils/DateCompare.java b/src/main/java/duke/utils/DateCompare.java new file mode 100644 index 0000000000..74931f8c9b --- /dev/null +++ b/src/main/java/duke/utils/DateCompare.java @@ -0,0 +1,53 @@ +package duke.utils; + +import java.util.Calendar; +import java.util.Date; + +public class DateCompare { + + /** + * Checks if day2 lies inside of day1. + * + * @param day1 the whole day from 00:00 to 23:59:59 + * @param day2 the day we want to check if it is in day 1 + * @return true if day 2 is in day 1 + */ + public static boolean isSameDay(Date day1, Date day2) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(day1); + calendar.add(Calendar.DATE, 1); + calendar.add(Calendar.SECOND, -1); + Date day1End = calendar.getTime(); + return day2.after(day1) && day2.before(day1End); + } + + /** + * Checks if 2 date ranges, A and B are overlapping each other. + * + * @param startDateA start of date A + * @param endDateA end of date A + * @param startDateB start of date B + * @param endDateB end of date B + * @return true if dates are overlapping else return false + */ + public static boolean isOverlapping(Date startDateA, Date endDateA, Date startDateB, Date endDateB) { + if (startDateA.after(startDateB) && startDateA.before(endDateB)) { + return true; + } else if (endDateA.after(startDateB) && endDateA.before(endDateB)) { + return true; + } else if (startDateA.after(startDateB) && endDateA.before(endDateB)) { + return true; + } else if (startDateB.after(startDateA) && startDateB.before(endDateA)) { + return true; + } else if (endDateB.after(startDateA) && endDateB.before(endDateB)) { + return true; + } else if (startDateB.after(startDateA) && endDateB.before(startDateB)) { + return true; + } else { + return startDateA.equals(startDateB) + || endDateA.equals(endDateB) + || startDateA.equals(endDateB) + || endDateA.equals(startDateB); + } + } +} diff --git a/src/main/resources/layout/BudgetPane.css b/src/main/resources/layout/BudgetPane.css new file mode 100644 index 0000000000..218c25a16f --- /dev/null +++ b/src/main/resources/layout/BudgetPane.css @@ -0,0 +1,10 @@ +#paneTitle { + -fx-font-size: 32px; + -fx-text-fill: GOLD; +} + +#totalIncomeLabel{ + -fx-font-size: 16px; + -fx-text-fill: GREENYELLOW; + +} diff --git a/src/main/resources/layout/ExpenseCard.css b/src/main/resources/layout/ExpenseCard.css new file mode 100644 index 0000000000..361da33e9c --- /dev/null +++ b/src/main/resources/layout/ExpenseCard.css @@ -0,0 +1,3 @@ +#amount { + -fx-font-size: 28px; +} \ No newline at end of file diff --git a/src/main/resources/layout/ExpensePane.css b/src/main/resources/layout/ExpensePane.css new file mode 100644 index 0000000000..232c0ae36a --- /dev/null +++ b/src/main/resources/layout/ExpensePane.css @@ -0,0 +1,16 @@ +#paneTitle { + -fx-font-size: 32px; + -fx-text-fill: GOLD; +} + +#totalLabel{ + -fx-font-size: 32px; +} + +#sortLabel, #viewLabel, #filterLabel { + -fx-text-fill: HONEYDEW; +} + +#totalLabel { + -fx-text-fill: GREENYELLOW; +} \ No newline at end of file diff --git a/src/main/resources/layout/IncomeCard.css b/src/main/resources/layout/IncomeCard.css new file mode 100644 index 0000000000..361da33e9c --- /dev/null +++ b/src/main/resources/layout/IncomeCard.css @@ -0,0 +1,3 @@ +#amount { + -fx-font-size: 28px; +} \ No newline at end of file diff --git a/src/main/resources/layout/MainWindow.css b/src/main/resources/layout/MainWindow.css new file mode 100644 index 0000000000..fb0e7ad11d --- /dev/null +++ b/src/main/resources/layout/MainWindow.css @@ -0,0 +1,29 @@ +BorderPane { +} + +Textfield { + -fx-alignment: bottom; + -fx-padding: 20px; +} + +#expenseListView, #budgetListView, #incomeListView{ + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 10, 0, 0, 0); +} + + +HBox { + -fx-min-width: 800px; +} + +VBox { + -fx-padding: 5 5 5 5; +} + +#inputField { + -fx-font-size: 1.5em; +} + +#MainContainer { + -fx-background-color: LIGHTSLATEGRAY +; +} \ No newline at end of file diff --git a/src/main/resources/layout/PlanPane.css b/src/main/resources/layout/PlanPane.css new file mode 100644 index 0000000000..f9dc632f44 --- /dev/null +++ b/src/main/resources/layout/PlanPane.css @@ -0,0 +1,21 @@ +.list-cell{ + -fx-background-color: WHITE + +; + +} + +.list-view{ + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.8), 10, 0, 0, 0); +} + +#paneTitle { + -fx-font-size: 36px; + -fx-font-weight: bold; + -fx-text-fill: GOLD; + +} + +#dialogListView { + -fx-font-size: 16px; +} diff --git a/src/main/resources/view/BudgetBar.fxml b/src/main/resources/view/BudgetBar.fxml new file mode 100644 index 0000000000..2af90ee86f --- /dev/null +++ b/src/main/resources/view/BudgetBar.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/view/BudgetPane.fxml b/src/main/resources/view/BudgetPane.fxml new file mode 100644 index 0000000000..a566053654 --- /dev/null +++ b/src/main/resources/view/BudgetPane.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..8342f048e1 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/main/resources/view/ExpenseCard.fxml b/src/main/resources/view/ExpenseCard.fxml new file mode 100644 index 0000000000..960d3dd415 --- /dev/null +++ b/src/main/resources/view/ExpenseCard.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/main/resources/view/ExpensePane.fxml b/src/main/resources/view/ExpensePane.fxml new file mode 100644 index 0000000000..da3a70f497 --- /dev/null +++ b/src/main/resources/view/ExpensePane.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/IncomeCard.fxml b/src/main/resources/view/IncomeCard.fxml new file mode 100644 index 0000000000..df904fb123 --- /dev/null +++ b/src/main/resources/view/IncomeCard.fxml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..81c6c77f77 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + +

+ +
+ + + + + + + + + + + + + diff --git a/src/main/resources/view/PaymentBox.fxml b/src/main/resources/view/PaymentBox.fxml new file mode 100644 index 0000000000..2054ede311 --- /dev/null +++ b/src/main/resources/view/PaymentBox.fxml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PaymentPane.fxml b/src/main/resources/view/PaymentPane.fxml new file mode 100644 index 0000000000..a418e7879d --- /dev/null +++ b/src/main/resources/view/PaymentPane.fxml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PlanPane.fxml b/src/main/resources/view/PlanPane.fxml new file mode 100644 index 0000000000..8be0bdbc57 --- /dev/null +++ b/src/main/resources/view/PlanPane.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/Tableview.fxml b/src/main/resources/view/Tableview.fxml new file mode 100644 index 0000000000..565017afb2 --- /dev/null +++ b/src/main/resources/view/Tableview.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/TrendingPane.fxml b/src/main/resources/view/TrendingPane.fxml new file mode 100644 index 0000000000..6d8ffacc5c --- /dev/null +++ b/src/main/resources/view/TrendingPane.fxml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/test/java/duke/logic/CommandParamsTest.java b/src/test/java/duke/logic/CommandParamsTest.java new file mode 100644 index 0000000000..91a16a2590 --- /dev/null +++ b/src/test/java/duke/logic/CommandParamsTest.java @@ -0,0 +1,86 @@ +package duke.logic; + +import duke.logic.CommandParams; +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class CommandParamsTest { + + + @Test + public void testCorrectParamValues() throws DukeException { + CommandParams testParams = new CommandParams("addExpense 2.12 /description hello /tag a b c"); + assertEquals(testParams.getCommand().getName(), "addExpense"); + assertEquals(testParams.getMainParam(), "2.12"); + assertEquals(testParams.getParam("description"), "hello"); + assertEquals(testParams.getParam("tag"), "a b c"); + assertTrue(testParams.containsParams("tag")); + assertFalse(testParams.containsParams("time")); + } + + + + @Test + public void testCorrectNullParamValues() throws DukeException { + CommandParams testParams = new CommandParams("addExpense /description /tag not null"); + assertNull(testParams.getMainParam()); + assertEquals(testParams.getParam("tag"), "not null"); + + try { + testParams.getParam("description"); + fail(); + } catch (DukeException e) { + assertEquals( + String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING_VALUE, "description"), e.getMessage()); + } + } + + + @Test + public void testParamNotFoundException() throws DukeException { + CommandParams testParams = new CommandParams("addExpense"); + try { + testParams.getParam("a"); + fail(); + } catch (DukeException e) { + assertEquals( + String.format(DukeException.MESSAGE_COMMAND_PARAM_MISSING_VALUE, "a"), e.getMessage()); + } + } + + @Test + public void testDuplicateParams() throws DukeException { + try { + CommandParams testParams = new CommandParams("addExpense /time /time"); + fail(); + } catch (DukeException e) { + assertEquals( + String.format(DukeException.MESSAGE_COMMAND_PARAM_DUPLICATE, "time"), e.getMessage()); + } + } + + /* + @Test + public void testAbbreviationFunctionality() throws DukeException { + try { + CommandParams testParams = new CommandParams("b"); + fail(); + } catch (DukeException e) { + assertEquals( + DukeException.MESSAGE_COMMAND_NAME_UNKNOWN, e.getMessage()); + } + + CommandParams testParams = new CommandParams("addE /d description"); + // assertEquals(testParams.getCommand().getName(), "addExpense"); + assertEquals(testParams.getParam("description"), "description"); + } + */ + +} diff --git a/src/test/java/duke/logic/util/AutoCompleterTest.java b/src/test/java/duke/logic/util/AutoCompleterTest.java new file mode 100644 index 0000000000..89faa2a261 --- /dev/null +++ b/src/test/java/duke/logic/util/AutoCompleterTest.java @@ -0,0 +1,105 @@ +package duke.logic.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class AutoCompleterTest { + + private static final String NULL_STRING = null; + private static final String NOT_NULL_STRING_INPUT = "dummy"; + + private static final String INPUT_WITH_ADD_PREFIX = "add"; + private static final String PREFIX_ADD_FIRST_OPTION = "addExpense"; + private static final String PREFIX_ADD_SECOND_OPTION = "addPayment"; + private static final String PREFIX_ADD_THIRD_OPTION = "addBudget"; + + private static final String PARAMETER_TO_COMPLETE = "addExpense /ti"; + private static final String PARAMETER_COMPLETED = "addExpense /time"; + + private static final String PARAMETER_TO_PRODUCE = "addExpense 10 "; + private static final String PARAMETER_PRODUCED_FIRST_OPTION = "addExpense 10 /recurring"; + private static final String PARAMETER_PRODUCED_SECOND_OPTION = "addExpense 10 /description"; + + private static final String NOT_IDENTIFIABLE_FRAGMENT = "arbitrary"; + private static final String DUMMY_BEFORE_PARAMETER_NAME = "addPayment 10 dummy/d"; + private static final String INCORRECT_COMMAND_NAME = "addDummy 10 "; + private static final String NO_SPACE_AT_END = "changePayment 60"; + private static final String EMPTY_STRING = ""; + + private static final String SPACES_AT_FIRST = " addE"; + private static final String SPACES_AT_FIRST_NO_PROBLEM = " addExpense"; + + @Test + public void receiveTest_nullStringInput_throwsNullPointerException() { + AutoCompleter autoCompleter = new AutoCompleter(); + assertThrows(NullPointerException.class, () -> autoCompleter.receiveText(NULL_STRING)); + } + + @Test + public void receiveTest_acceptableStringInput_success() { + AutoCompleter autoCompleter = new AutoCompleter(); + assertDoesNotThrow(() -> autoCompleter.receiveText(NOT_NULL_STRING_INPUT)); + } + + @Test + public void getFullComplement() { + AutoCompleter autoCompleter = new AutoCompleter(); + + // Completes "add" to "addExpense". + autoCompleter.receiveText(INPUT_WITH_ADD_PREFIX); + assertEquals(PREFIX_ADD_FIRST_OPTION, autoCompleter.getFullComplement()); + + // Iterates from "addExpense" to "addPayment". + autoCompleter.receiveText(PREFIX_ADD_FIRST_OPTION); + assertEquals(PREFIX_ADD_SECOND_OPTION, autoCompleter.getFullComplement()); + + // Iterates from "addPayment" to "addBudget". + autoCompleter.receiveText(PREFIX_ADD_SECOND_OPTION); + assertEquals(PREFIX_ADD_THIRD_OPTION, autoCompleter.getFullComplement()); + + // Iterates from "addBudget" to "addExpense". + autoCompleter.receiveText(PREFIX_ADD_THIRD_OPTION); + assertEquals(PREFIX_ADD_FIRST_OPTION, autoCompleter.getFullComplement()); + + // Completes the parameter name "/ti" to "/time". + autoCompleter.receiveText(PARAMETER_TO_COMPLETE); + assertEquals(PARAMETER_COMPLETED, autoCompleter.getFullComplement()); + + // Produces the first option "/recurring". + autoCompleter.receiveText(PARAMETER_TO_PRODUCE); + assertEquals(PARAMETER_PRODUCED_FIRST_OPTION, autoCompleter.getFullComplement()); + + // Produces the second option "/description". + autoCompleter.receiveText(PARAMETER_PRODUCED_FIRST_OPTION); + assertEquals(PARAMETER_PRODUCED_SECOND_OPTION, autoCompleter.getFullComplement()); + + // No complement is applied to not identifiable input. + autoCompleter.receiveText(NOT_IDENTIFIABLE_FRAGMENT); + assertEquals(NOT_IDENTIFIABLE_FRAGMENT, autoCompleter.getFullComplement()); + + // No complement is applied to parameter name if dummy exists before it. + autoCompleter.receiveText(DUMMY_BEFORE_PARAMETER_NAME); + assertEquals(DUMMY_BEFORE_PARAMETER_NAME, autoCompleter.getFullComplement()); + + // No parameter name is produced if the command name is incorrect. + autoCompleter.receiveText(INCORRECT_COMMAND_NAME); + assertEquals(INCORRECT_COMMAND_NAME, autoCompleter.getFullComplement()); + + // No parameter is produced if the input does not end with space. + autoCompleter.receiveText(NO_SPACE_AT_END); + assertEquals(NO_SPACE_AT_END, autoCompleter.getFullComplement()); + + // No change is applied if the input is empty. + autoCompleter.receiveText(EMPTY_STRING); + assertEquals(EMPTY_STRING, autoCompleter.getFullComplement()); + + // Spaces at the start of the input do not affect auto-complete. + autoCompleter.receiveText(SPACES_AT_FIRST); + assertEquals(SPACES_AT_FIRST_NO_PROBLEM, autoCompleter.getFullComplement()); + } + +} diff --git a/src/test/java/duke/logic/util/InputHistoryTest.java b/src/test/java/duke/logic/util/InputHistoryTest.java new file mode 100644 index 0000000000..e8ccf01449 --- /dev/null +++ b/src/test/java/duke/logic/util/InputHistoryTest.java @@ -0,0 +1,80 @@ +package duke.logic.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class InputHistoryTest { + + private static final String NULL_OBJECT = null; + private static final String NULL_STRING = null; + private static final String EMPTY_STRING = ""; + private static final String SPACE_STRING = " "; + + private static final String FIRST_HISTORY_COMMAND = "addExpense /description phone bill"; + private static final String SECOND_HISTORY_COMMAND = "deleteExpense 1"; + private static final String THIRD_HISTORY_COMMAND = "sortExpense amount"; + + @Test + public void add_nullStringInput_throwsNullPointerException() { + InputHistory inputHistory = new InputHistory(); + assertThrows(NullPointerException.class, () -> inputHistory.add(NULL_STRING)); + } + + @Test + public void add_blankStringInput_failure() { + InputHistory inputHistory = new InputHistory(); + inputHistory.add(EMPTY_STRING); + inputHistory.add(SPACE_STRING); + + // No history command has been added into inputHistory yet. + assertEquals(EMPTY_STRING, inputHistory.getLastInput()); + } + + @Test + public void add_acceptableCommandString_success() { + InputHistory inputHistory = new InputHistory(); + assertDoesNotThrow(() -> inputHistory.add(FIRST_HISTORY_COMMAND)); + } + + @Test + public void getLastInput() { + InputHistory inputHistory = new InputHistory(); + inputHistory.add(FIRST_HISTORY_COMMAND); + inputHistory.add(SECOND_HISTORY_COMMAND); + inputHistory.add(THIRD_HISTORY_COMMAND); + + // Navigates to three previous commands. + assertEquals(THIRD_HISTORY_COMMAND, inputHistory.getLastInput()); + assertEquals(SECOND_HISTORY_COMMAND, inputHistory.getLastInput()); + assertEquals(FIRST_HISTORY_COMMAND, inputHistory.getLastInput()); + + // Remains at the earliest history command if the head is reached. + assertEquals(FIRST_HISTORY_COMMAND, inputHistory.getLastInput()); + } + + @Test + public void getNextInput() { + InputHistory inputHistory = new InputHistory(); + inputHistory.add(FIRST_HISTORY_COMMAND); + inputHistory.add(SECOND_HISTORY_COMMAND); + inputHistory.add(THIRD_HISTORY_COMMAND); + + // Navigates to three previous commands. + inputHistory.getLastInput(); + inputHistory.getLastInput(); + inputHistory.getLastInput(); + + // Navigates back to two recent commands. + assertEquals(SECOND_HISTORY_COMMAND, inputHistory.getNextInput()); + assertEquals(THIRD_HISTORY_COMMAND, inputHistory.getNextInput()); + + // Clears the output if the tail is exceeded. + assertEquals(EMPTY_STRING, inputHistory.getNextInput()); + } +} diff --git a/src/test/java/duke/model/BudgetTest.java b/src/test/java/duke/model/BudgetTest.java new file mode 100644 index 0000000000..c01cad5e9b --- /dev/null +++ b/src/test/java/duke/model/BudgetTest.java @@ -0,0 +1,24 @@ +package duke.model; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; + +public class BudgetTest { + @TempDir + File userDirectory; + + @Test + public void testBasicOperations() throws IOException, DukeException { + Budget budget = new Budget(BigDecimal.ZERO, new HashMap<>()); + budget.setMonthlyBudget(BigDecimal.TEN); + Assertions.assertEquals("10", budget.getMonthlyBudgetString()); + } + +} diff --git a/src/test/java/duke/model/BudgetViewTest.java b/src/test/java/duke/model/BudgetViewTest.java new file mode 100644 index 0000000000..c4fa8e6007 --- /dev/null +++ b/src/test/java/duke/model/BudgetViewTest.java @@ -0,0 +1,22 @@ +package duke.model; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BudgetViewTest { + + private static final int TEST_VIEW = 1; + private static final String TEST_CATEGORY = "test category"; + + @Test + void testPositive() { + Map testBudgetViewCategory = new HashMap<>(); + BudgetView testBudgetView = new BudgetView(testBudgetViewCategory); + testBudgetView.setBudgetView(TEST_VIEW,TEST_CATEGORY); + assertEquals(testBudgetView.getBudgetViewCategory(), testBudgetViewCategory); + } +} diff --git a/src/test/java/duke/model/ExpenseListTest.java b/src/test/java/duke/model/ExpenseListTest.java new file mode 100644 index 0000000000..95b40e6a33 --- /dev/null +++ b/src/test/java/duke/model/ExpenseListTest.java @@ -0,0 +1,87 @@ +package duke.model; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class ExpenseListTest { + @TempDir + File userDirectory; + + private static final String STORAGE_DELIMITER = "\n\n"; + + + @Test + public void testBasicOperations() throws DukeException { + ExpenseList testExpenseList = new ExpenseList(new ArrayList<>()); + Expense testExpense = new Expense.Builder().build(); + testExpenseList.add(testExpense); + assertEquals(testExpenseList.get(1), testExpense); + assertEquals(testExpenseList.internalSize(), 1); + testExpenseList.remove(1); + assertEquals(testExpenseList.internalSize(), 0); + } + + @Test + public void testInvalidBasicOperations() throws DukeException { + ExpenseList testExpenseList = new ExpenseList(new ArrayList<>()); + Expense testExpense = new Expense.Builder().build(); + testExpenseList.add(testExpense); + try { + testExpenseList.get(2); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_NO_ITEM_AT_INDEX, "expense", 2), e.getMessage()); + } + + try { + testExpenseList.remove(2); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_NO_ITEM_AT_INDEX, "expense", 2), e.getMessage()); + } + } + /*TODO:This test is Currently broken due to regressions, please fix. + @Test + public void testUndoRedo() throws DukeException { + ExpenseList testExpenseList = new ExpenseList(new ArrayList<>()); + Expense testExpense = new Expense.Builder().build(); + testExpenseList.add(testExpense); + testExpenseList.remove(1); + assertEquals(testExpenseList.undo(1), 1); + assertEquals(testExpenseList.internalSize(), 1); + assertEquals(testExpenseList.undo(1), 1); + assertEquals(testExpenseList.internalSize(), 0); + assertEquals(testExpenseList.redo(1), 1); + assertEquals(testExpenseList.internalSize(), 1); + assertEquals(testExpenseList.redo(1), 1); + assertEquals(testExpenseList.internalSize(), 0); + assertEquals(testExpenseList.undo(2), 2); + assertEquals(testExpenseList.redo(2), 2); + } + + */ + @Test + public void testGetTotalAmount() throws DukeException { + ExpenseList testExpenseList = new ExpenseList(new ArrayList<>()); + Expense testExpenseOne = new Expense.Builder().build(); + Expense testExpenseTwo = new Expense.Builder().setAmount("12").build(); + Expense testExpenseThree = new Expense.Builder().setAmount("13").build(); + Expense testExpenseFour = new Expense.Builder().setAmount("12.4").build(); + Expense testExpenseFive = new Expense.Builder().setAmount("12.23").build(); + testExpenseList.add(testExpenseOne); + testExpenseList.add(testExpenseTwo); + testExpenseList.add(testExpenseThree); + testExpenseList.add(testExpenseFour); + testExpenseList.add(testExpenseFive); + assertEquals(testExpenseList.getTotalAmount(), new BigDecimal("49.63")); + } +} diff --git a/src/test/java/duke/model/ExpenseTest.java b/src/test/java/duke/model/ExpenseTest.java new file mode 100644 index 0000000000..64e99651ea --- /dev/null +++ b/src/test/java/duke/model/ExpenseTest.java @@ -0,0 +1,130 @@ +package duke.model; + +import duke.exception.DukeException; +import duke.logic.parser.Parser; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class ExpenseTest { + private static final BigDecimal DEFAULT_AMOUNT = BigDecimal.ZERO; + private static final String DEFAULT_DESCRIPTION = ""; + private static final boolean DEFAULT_TENTATIVE = false; + private static final String DEFAULT_TIME = Parser.formatTime(LocalDateTime.now()); + + private static final BigDecimal TEST_AMOUNT = new BigDecimal("1.23"); + private static final String TEST_DESCRIPTION = "test description"; + private static final boolean TEST_TENTATIVE = true; + private static final String TEST_TIME = "18:00 01/01/2000"; + private static final String TEST_TAG = "test tag"; + + private static final String INVALID_STORAGE_STRING = "tags:tag1 tag2 tag3\n" + + "amount:1.223\n" + + "d:1\n" + + "t:2"; + + private static final String ACTUAL_TO_STRING = "$1.23 " + + "test description " + + "18:00 01/01/2000 " + + "(tentative) " + + "tag1 tag2 tag 3 " + + "isRecurring:false"; + private static final String ACTUAL_TO_STORAGE_STRING = "tag:test tag\n" + + "amount:1.23\n" + + "description:test description\n" + + "time:18:00 01/01/2000\n" + + "isTentative:true\n" + + "isRecurring:false"; + + @Test + public void testDefaults() { + Expense testExpense = new Expense.Builder().build(); + assertEquals(testExpense.getAmount(), DEFAULT_AMOUNT); + assertEquals(testExpense.getDescription(), DEFAULT_DESCRIPTION); + assertEquals(testExpense.isTentative(), DEFAULT_TENTATIVE); + assertEquals(Parser.formatTime(testExpense.getTime()), DEFAULT_TIME); + assertTrue(testExpense.getTag().isEmpty()); + } + + @Test + public void testBuilderFromExpense() throws DukeException { + Expense testExpense = new Expense.Builder() + .setAmount(TEST_AMOUNT) + .setDescription(TEST_DESCRIPTION) + .setTentative(TEST_TENTATIVE) + .setTime(TEST_TIME) + .setTag(TEST_TAG) + .build(); + Expense testExpenseTwo = new Expense.Builder(testExpense).build(); + assertEquals(testExpense.getAmount(), testExpenseTwo.getAmount()); + assertEquals(testExpense.getDescription(), testExpenseTwo.getDescription()); + assertEquals(testExpense.getTag(), testExpenseTwo.getTag()); + assertEquals(testExpense.getTime(), testExpenseTwo.getTime()); + assertEquals(testExpense.getAmount(), testExpenseTwo.getAmount()); + } + + @Test + public void testAmount() throws DukeException { + Expense testExpense = new Expense.Builder().setAmount(TEST_AMOUNT).build(); + assertEquals(testExpense.getAmount(), TEST_AMOUNT); + } + + @Test + public void testDescription() { + Expense testExpense = new Expense.Builder().setDescription(TEST_DESCRIPTION).build(); + assertEquals(testExpense.getDescription(), TEST_DESCRIPTION); + } + + @Test + public void testIsTentative() { + Expense testExpense = new Expense.Builder().setTentative(TEST_TENTATIVE).build(); + assertEquals(testExpense.isTentative(), TEST_TENTATIVE); + } + + @Test + public void testTime() throws DukeException { + Expense testExpense = new Expense.Builder().setTime(TEST_TIME).build(); + assertEquals(Parser.formatTime(testExpense.getTime()), TEST_TIME); + } + + @Test + public void testTags() { + Expense testExpense = new Expense.Builder().setTag(TEST_TAG).build(); + assertEquals(testExpense.getTag(), TEST_TAG); + } + + + @Test + public void testToStorageString() throws DukeException { + String storageString = new Expense.Builder() + .setAmount(TEST_AMOUNT) + .setDescription(TEST_DESCRIPTION) + .setTentative(TEST_TENTATIVE) + .setTime(TEST_TIME) + .setTag(TEST_TAG) + .build() + .toStorageString(); + assertEquals(storageString, ACTUAL_TO_STORAGE_STRING); + Expense testExpense = new Expense.Builder(storageString).build(); + assertEquals(testExpense.getAmount(), TEST_AMOUNT); + assertEquals(testExpense.getDescription(), TEST_DESCRIPTION); + assertEquals(testExpense.isTentative(), TEST_TENTATIVE); + assertEquals(Parser.formatTime(testExpense.getTime()), TEST_TIME); + assertEquals(testExpense.getTag(), TEST_TAG); + } + + @Test + public void testInvalidStorageString() { + try { + new Expense.Builder(INVALID_STORAGE_STRING); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_EXPENSE_AMOUNT_INVALID, "1.223"), e.getMessage()); + } + } +} diff --git a/src/test/java/duke/model/IncomeListTest.java b/src/test/java/duke/model/IncomeListTest.java new file mode 100644 index 0000000000..3a10ffe5fb --- /dev/null +++ b/src/test/java/duke/model/IncomeListTest.java @@ -0,0 +1,91 @@ +package duke.model; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class IncomeListTest { + private static final BigDecimal TEST_AMOUNT = new BigDecimal("233.03"); + private static final String TEST_DESCRIPTION = "test description"; + private static final String ACTUAL_STORAGE_STRING = "amount:233.03\n" + "description:test description\n"; + private static final String ACTUAL_TOTAL_STRING = "Total Income: $460.13"; + + @Test + void testBasicOperations() throws DukeException { + IncomeList testIncomeList = new IncomeList(new ArrayList<>()); + Income testIncome = new Income.Builder().build(); + testIncomeList.add(testIncome); + assertEquals(testIncomeList.get(1), testIncome); + assertEquals(testIncomeList.internalSize(), 1); + testIncomeList.remove(1); + assertEquals(testIncomeList.internalSize(), 0); + testIncomeList.add(testIncome); + testIncomeList.add(testIncome); + testIncomeList.clear(); + assertEquals(testIncomeList.internalSize(), 0); + } + + @Test + void testInvalidBasicOperations() { + IncomeList testIncomeList = new IncomeList(new ArrayList<>()); + Income testIncome = new Income.Builder().build(); + testIncomeList.add(testIncome); + + try { + testIncomeList.get(2); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_NO_ITEM_AT_INDEX, "income", 2), e.getMessage()); + } + + try { + testIncomeList.remove(2); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_NO_ITEM_AT_INDEX, "income", 2), e.getMessage()); + } + } + + @Test + void testListOperations() { + IncomeList testIncomeList = new IncomeList(new ArrayList<>()); + Income testIncome = new Income.Builder().build(); + testIncomeList.add(testIncome); + Assertions.assertNotNull(testIncomeList.getInternalList()); + Assertions.assertFalse(testIncomeList.getInternalList().isEmpty()); + Assertions.assertNotNull(testIncomeList.getExternalList()); + Assertions.assertFalse(testIncomeList.getExternalList().isEmpty()); + } + + @Test + void testItemFromStringStorage() throws DukeException { + Income testIncome = IncomeList.itemFromStorageString(ACTUAL_STORAGE_STRING); + assertEquals(testIncome.getAmount(),TEST_AMOUNT); + assertEquals(testIncome.getDescription(),TEST_DESCRIPTION); + } + + @Test + void testGetTotalAmount() throws DukeException { + IncomeList testIncomeList = new IncomeList(new ArrayList<>()); + Income testIncomeOne = new Income.Builder().build(); + Income testIncomeTwo = new Income.Builder().setAmount("9").build(); + Income testIncomeThree = new Income.Builder().setAmount("300").build(); + Income testIncomeFour = new Income.Builder().setAmount("30.9").build(); + Income testIncomeFive = new Income.Builder().setAmount("120.23").build(); + testIncomeList.add(testIncomeOne); + testIncomeList.add(testIncomeTwo); + testIncomeList.add(testIncomeThree); + testIncomeList.add(testIncomeFour); + testIncomeList.add(testIncomeFive); + assertEquals(testIncomeList.getTotalExternalAmount(), new BigDecimal("460.13")); + String testTotalString = testIncomeList.getTotalString().get(); + assertEquals(testTotalString, ACTUAL_TOTAL_STRING); + } + +} diff --git a/src/test/java/duke/model/IncomeTest.java b/src/test/java/duke/model/IncomeTest.java new file mode 100644 index 0000000000..f43b114509 --- /dev/null +++ b/src/test/java/duke/model/IncomeTest.java @@ -0,0 +1,84 @@ +package duke.model; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class IncomeTest { + private static final BigDecimal DEFAULT_AMOUNT = BigDecimal.ZERO; + private static final String DEFAULT_DESCRIPTION = ""; + + private static final BigDecimal TEST_AMOUNT = new BigDecimal("100.23"); + private static final String TEST_DESCRIPTION = "test description"; + + private static final String INVALID_STORAGE_STRING = "amount:100.223\n" + "d:1\n"; + private static final String INVALID_AMOUNT = "test amount"; + + + private static final String ACTUAL_TO_STORAGE_STRING = "amount:100.23\n" + "description:test description"; + + @Test + void testDefaults() { + Income testIncome = new Income.Builder().build(); + assertEquals(testIncome.getAmount(), DEFAULT_AMOUNT); + assertEquals(testIncome.getDescription(), DEFAULT_DESCRIPTION); + } + + @Test + void testBuilderFromIncome() throws DukeException { + Income testIncome = new Income.Builder() + .setAmount(TEST_AMOUNT) + .setDescription(TEST_DESCRIPTION) + .build(); + Income testIncomeTwo = new Income.Builder(testIncome).build(); + assertEquals(testIncome.getAmount(), testIncomeTwo.getAmount()); + assertEquals(testIncome.getDescription(), testIncomeTwo.getDescription()); + assertEquals(testIncome.getAmount(), testIncomeTwo.getAmount()); + } + + @Test + void testAmount() throws DukeException { + Income testIncome1 = new Income.Builder().setAmount(TEST_AMOUNT).build(); + assertEquals(testIncome1.getAmount(), TEST_AMOUNT); + + try { + new Income.Builder().setAmount(INVALID_AMOUNT); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_INCOME_AMOUNT_INVALID, INVALID_AMOUNT), e.getMessage()); + } + } + + @Test + void testDescription() { + Income testIncome = new Income.Builder().setDescription(TEST_DESCRIPTION).build(); + assertEquals(testIncome.getDescription(), TEST_DESCRIPTION); + } + + @Test + void testToStorageString() throws DukeException { + String storageString = new Income.Builder() + .setAmount(TEST_AMOUNT) + .setDescription(TEST_DESCRIPTION) + .build() + .toStorageString(); + assertEquals(storageString, ACTUAL_TO_STORAGE_STRING); + Income testIncome = new Income.Builder(storageString).build(); + assertEquals(testIncome.getAmount(), TEST_AMOUNT); + assertEquals(testIncome.getDescription(), TEST_DESCRIPTION); + } + + @Test + void testInvalidStorageString() { + try { + new Income.Builder(INVALID_STORAGE_STRING); + fail(); + } catch (DukeException e) { + assertEquals(String.format(DukeException.MESSAGE_INCOME_AMOUNT_INVALID, "100.223"), e.getMessage()); + } + } +} diff --git a/src/test/java/duke/model/PlanBotTest.java b/src/test/java/duke/model/PlanBotTest.java new file mode 100644 index 0000000000..8e74e5c6d8 --- /dev/null +++ b/src/test/java/duke/model/PlanBotTest.java @@ -0,0 +1,42 @@ +package duke.model; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + + +public class PlanBotTest { + @Test + public void testPositive() { + Map knownAttributes = new HashMap<>(); + PlanBot planBot = PlanBot.getInstance(knownAttributes); + Assertions.assertNotNull(planBot.getDialogObservableList()); + Assertions.assertFalse(planBot.getDialogObservableList().isEmpty()); + int currSize = planBot.getDialogObservableList().size(); + planBot.processInput("yes"); + int newSize = planBot.getDialogObservableList().size(); + Assertions.assertTrue(newSize > currSize); + knownAttributes.put("NUS_STUDENT", "TRUE"); + Assertions.assertEquals(planBot.getPlanAttributes(), knownAttributes); + } + + @Test + public void testInvalidInput() { + Map knownAttributes = new HashMap<>(); + PlanBot planBot = PlanBot.getInstance(knownAttributes); + Assertions.assertNull(planBot.getPlanBudgetRecommendation()); + planBot.processInput("random string of inputs"); + PlanBot.PlanDialog planDialogExpected1 = new PlanBot.PlanDialog("Please enter a valid reply!", + PlanBot.Agent.BOT); + PlanBot.PlanDialog planDialogExpected2 = new PlanBot.PlanDialog( + "random string of inputs is not a valid amount!", PlanBot.Agent.BOT); + int size = planBot.getDialogObservableList().size(); + PlanBot.PlanDialog planDialog = planBot.getDialogObservableList().get(size - 1); + Assertions.assertTrue(planDialogExpected1.text.equals(planDialog.text) + || planDialogExpected2.text.equals(planDialog.text)); + } + + +} diff --git a/src/test/java/duke/model/PlanQuestionBankTest.java b/src/test/java/duke/model/PlanQuestionBankTest.java new file mode 100644 index 0000000000..24f2c63220 --- /dev/null +++ b/src/test/java/duke/model/PlanQuestionBankTest.java @@ -0,0 +1,87 @@ +package duke.model; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +public class PlanQuestionBankTest { + @Test + public void testPlanQuestionBank() { + try { + Map knownAttributes = new HashMap<>(); + PlanQuestionBank planQuestionBank = PlanQuestionBank.getInstance(); + + Assertions.assertFalse(planQuestionBank.getQuestions(knownAttributes).isEmpty()); + Queue questionQueue = new LinkedList<>(); + + questionQueue.addAll(planQuestionBank.getQuestions(knownAttributes)); + Assertions.assertFalse(questionQueue.isEmpty()); + + PlanQuestion firstQuestion = questionQueue.peek(); + questionQueue.remove(); + Assertions.assertEquals(firstQuestion.getQuestion(), "Are you a student from NUS? "); + + PlanQuestion.Reply reply = firstQuestion.getReply("yes", knownAttributes); + knownAttributes = reply.getAttributes(); + questionQueue.addAll(planQuestionBank.getQuestions(knownAttributes)); + Assertions.assertFalse(questionQueue.isEmpty()); + + /* + This goes through the entire questionBank with the first valid answer + and checks if we get a proper recommendation, i.e simulating a conversation. + */ + while (!questionQueue.isEmpty()) { + PlanQuestion currentQuestion = questionQueue.peek(); + questionQueue.remove(); + Field field = PlanQuestion.class.getDeclaredField("answersAttributesValue"); + field.setAccessible(true); + Map answerAttributeValue = (Map) field.get(currentQuestion); + String answer = answerAttributeValue.keySet().iterator().next(); + if (answer.equals("DOUBLE")) { + answer = "10.00"; + } + PlanQuestion.Reply currentQuestionReply = currentQuestion.getReply(answer, knownAttributes); + + Assertions.assertFalse(currentQuestionReply.getAttributes().isEmpty()); + + Assertions.assertEquals(currentQuestionReply.getText(), "Ok noted!"); + + knownAttributes = currentQuestionReply.getAttributes(); + questionQueue.addAll(planQuestionBank.getQuestions(knownAttributes)); + } + PlanQuestionBank.PlanRecommendation recommendation = planQuestionBank + .makeRecommendation(knownAttributes); + + Assertions.assertNotEquals(recommendation.getRecommendation(), "I can't make any recommendations for you" + + " :(. Something probably went wrong"); + + Assertions.assertFalse(recommendation.getPlanBudget().isEmpty()); + + Assertions.assertNotNull(recommendation.getRecommendationExpenseList()); + + } catch (DukeException | NoSuchFieldException | IllegalAccessException e) { + Assertions.fail(e.getMessage()); + } + } + + @Test + public void testNegative() { + try { + Map knownAttributes = new HashMap<>(); + PlanQuestionBank planQuestionBank = PlanQuestionBank.getInstance(); + Assertions.assertThrows(DukeException.class, () -> { + planQuestionBank.makeRecommendation(knownAttributes); + }); + } catch (DukeException e) { + Assertions.fail(e.getMessage()); + } + + } + +} diff --git a/src/test/java/duke/model/PlanQuestionTest.java b/src/test/java/duke/model/PlanQuestionTest.java new file mode 100644 index 0000000000..9d3606526b --- /dev/null +++ b/src/test/java/duke/model/PlanQuestionTest.java @@ -0,0 +1,73 @@ +package duke.model; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class PlanQuestionTest { + + private static final String[] BOOL_ANSWERS = {"YES", "NO"}; + private static final String[] BOOL_ATTRIBUTE_VALUES = {"TRUE", "FALSE"}; + private static final Integer[] NEIGHBOUR_ARR = {1, 2}; + private static final Set NEIGHBOUR_SET = new HashSet<>(Arrays.asList(NEIGHBOUR_ARR)); + private static final Map KNOWN_ATTRIBUTES = new HashMap<>(); + private static final Map UPDATED_ATTRIBUTES = new HashMap<>(); + + @Test + public void testConstruction() { + try { + PlanQuestion question = new PlanQuestion("Test Question", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "TEST"); + Assertions.assertEquals(question.getQuestion(), "Test Question"); + Assertions.assertEquals(question.getAttribute(), "TEST"); + } catch (DukeException e) { + Assertions.fail(); + } + } + + @Test + public void testPositive() { + try { + PlanQuestion question = new PlanQuestion("Test Question", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "TEST"); + PlanQuestion.Reply reply = question.getReply("yes", KNOWN_ATTRIBUTES); + Assertions.assertEquals(reply.getText(), "Ok noted!"); + UPDATED_ATTRIBUTES.put("TEST", "TRUE"); + question.addNeighbouring("TRUE", 1); + question.addNeighbouring("TRUE", 2); + Assertions.assertEquals(question.getNeighbouringQuestions("TRUE"), NEIGHBOUR_SET); + Assertions.assertEquals(reply.getAttributes(), UPDATED_ATTRIBUTES); + } catch (DukeException e) { + Assertions.fail(e.getMessage()); + } + } + + @Test + public void testNegative() { + try { + PlanQuestion question = new PlanQuestion("Test Question", + BOOL_ANSWERS, + BOOL_ATTRIBUTE_VALUES, + "TEST"); + Assertions.assertThrows(DukeException.class, () -> { + question.getReply("some random input", KNOWN_ATTRIBUTES); + }); + Assertions.assertTrue(question.getNeighbouringQuestions("SOME_RANDOM__ATTRIBUTE").isEmpty()); + Assertions.assertThrows(DukeException.class, () -> { + question.addNeighbouring("SOME_RANDOM__ATTRIBUTE", 1); + }); + } catch (DukeException e) { + Assertions.fail(e.getMessage()); + } + } +} diff --git a/src/test/java/duke/model/payment/PaymentInMonthPredicateTest.java b/src/test/java/duke/model/payment/PaymentInMonthPredicateTest.java new file mode 100644 index 0000000000..936078b01c --- /dev/null +++ b/src/test/java/duke/model/payment/PaymentInMonthPredicateTest.java @@ -0,0 +1,48 @@ +package duke.model.payment; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class PaymentInMonthPredicateTest { + + private static final LocalDate THIS_MONTH = LocalDate.now(); + private static final String THIS_MONTH_DUE = THIS_MONTH.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private static final LocalDate LAST_MONTH = LocalDate.now().minusMonths(1); + private static final String LAST_MONTH_DUE = LAST_MONTH.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private static final LocalDate NEXT_MONTH = LocalDate.now().plusMonths(1); + private static final String NEXT_MONTH_DUE = NEXT_MONTH.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private PaymentInMonthPredicate predicate = new PaymentInMonthPredicate(); + + @Test + public void test_nullObject_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> predicate.test(null)); + } + + @Test + public void test_paymentDueBefore_returnsFalse() throws DukeException { + assertFalse(predicate.test(new Payment.Builder().setDue(LAST_MONTH_DUE).build())); + } + + @Test + public void test_paymentDueInMonth_returnsTrue() throws DukeException { + assertTrue(predicate.test(new Payment.Builder().setDue(THIS_MONTH_DUE).build())); + } + + @Test + public void test_paymentDueAfterMonth_returnFalse() throws DukeException { + assertFalse(predicate.test(new Payment.Builder().setDue(NEXT_MONTH_DUE).build())); + } +} diff --git a/src/test/java/duke/model/payment/PaymentInWeekPredicateTest.java b/src/test/java/duke/model/payment/PaymentInWeekPredicateTest.java new file mode 100644 index 0000000000..a5d3eccb7f --- /dev/null +++ b/src/test/java/duke/model/payment/PaymentInWeekPredicateTest.java @@ -0,0 +1,48 @@ +package duke.model.payment; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class PaymentInWeekPredicateTest { + + private static final LocalDate THIS_WEEK = LocalDate.now(); + private static final String THIS_WEEK_DUE = THIS_WEEK.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private static final LocalDate LAST_WEEK = LocalDate.now().minusWeeks(1); + private static final String LAST_WEEK_DUE = LAST_WEEK.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private static final LocalDate NEXT_WEEK = LocalDate.now().plusWeeks(1); + private static final String NEXT_WEEK_DUE = NEXT_WEEK.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private PaymentInWeekPredicate predicate = new PaymentInWeekPredicate(); + + @Test + public void test_nullObject_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> predicate.test(null)); + } + + @Test + public void test_paymentDueBefore_returnsFalse() throws DukeException { + assertFalse(predicate.test(new Payment.Builder().setDue(LAST_WEEK_DUE).build())); + } + + @Test + public void test_paymentDueInWeek_returnsTrue() throws DukeException { + assertTrue(predicate.test(new Payment.Builder().setDue(THIS_WEEK_DUE).build())); + } + + @Test + public void test_paymentDueAfterWeek_returnFalse() throws DukeException { + assertFalse(predicate.test(new Payment.Builder().setDue(NEXT_WEEK_DUE).build())); + } +} diff --git a/src/test/java/duke/model/payment/PaymentListIntegrationTest.java b/src/test/java/duke/model/payment/PaymentListIntegrationTest.java new file mode 100644 index 0000000000..cd8b37aa24 --- /dev/null +++ b/src/test/java/duke/model/payment/PaymentListIntegrationTest.java @@ -0,0 +1,189 @@ +package duke.model.payment; + +import duke.exception.DukeException; +import duke.model.payment.Payment.Builder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PaymentListIntegrationTest { + + private static final String RELEVANT_KEYWORD = "john"; + private static final String IRRELEVANT_KEYWORD = "transportation"; + private static final String UNIDENTIFIABLE_SORTING_CRITERIA = "description"; + private static final String AMOUNT_SORTING_CRITERIA = "amount"; + private static final String PRIORITY_SORTING_CRITERIA = "priority"; + private static final int PAYMENTS_FULL_SIZE = 5; + + private static Payment STORAGE_FEE; + private static Payment ORIENTATION_FEE; + private static Payment HALL_MEAL; + private static Payment HOSTEL_FEE; + private static Payment RETURN_MONEY; + + private final PaymentList payments = new PaymentList(); + + /** + * Setup test. + */ + @BeforeEach + public void setup() { + assertDoesNotThrow(() -> { + STORAGE_FEE = new Builder().setDescription("In Room Storage Fee") + .setDue("09/09/2019") + .setAmount("150").setPriority("medium").build(); + + ORIENTATION_FEE = new Builder().setDescription("Orientation Fee") + .setDue("08/10/2019") + .setAmount("30").setPriority("low").build(); + + HALL_MEAL = new Builder().setDescription("Hall Meal Fee for Next semester") + .setDue("23/11/2019") + .setAmount("350").setPriority("low").build(); + + HOSTEL_FEE = new Builder().setDescription("Hostel_Preservation_Fee") + .setDue("15/10/2019") + .setAmount("200").setPriority("high").build(); + + RETURN_MONEY = new Builder().setDescription("Return Money to John") + .setDue("29/11/2019") + .setAmount("36").setPriority("medium").build(); + }); + } + + @Test + public void add_nullPayment_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> payments.add(null)); + } + + @Test + public void add_validPayment_success() { + assertDoesNotThrow(() -> payments.add(HALL_MEAL)); + } + + @Test + public void remove_indexOutOfBound_throwsDukeException() { + payments.add(ORIENTATION_FEE); + assertThrows(DukeException.class, () -> payments.remove(0)); // 1-based index + } + + @Test + public void remove_validIndex_success() { + payments.add(ORIENTATION_FEE); + assertDoesNotThrow(() -> payments.remove(1)); // 1-based index + assertTrue(payments.getInternalList().isEmpty()); + } + + @Test + public void setPayment_invalidIndex_throwsDukeException() { + payments.add(STORAGE_FEE); + assertThrows(DukeException.class, () -> payments.setPayment(0, HALL_MEAL)); // 1-based + } + + @Test + public void setPayment_nullEditedPayment_throwsNullPointerException() { + payments.add(STORAGE_FEE); + assertThrows(NullPointerException.class, () -> payments.setPayment(1, null)); + // 1-based index + } + + @Test + public void setPayment_editedPaymentIsSamePayment_success() { + payments.add(HOSTEL_FEE); + assertDoesNotThrow(() -> payments.setPayment(1, HOSTEL_FEE)); // 1-based index + + PaymentList expectedPayments = new PaymentList(); + expectedPayments.add(HOSTEL_FEE); + assertEquals(expectedPayments.getInternalList(), payments.getInternalList()); + } + + @Test + public void setPayment_editedPaymentIsDifferentPayment_success() { + payments.add(HOSTEL_FEE); + assertDoesNotThrow(() -> payments.setPayment(1, ORIENTATION_FEE)); // 1-based index + + PaymentList expectedPayments = new PaymentList(); + expectedPayments.add(ORIENTATION_FEE); + assertEquals(expectedPayments.getInternalList(), payments.getInternalList()); + } + + @Test + public void searchPredicate_nullKeyword_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> payments.setSearchPredicate(null)); + } + + @Test + public void searchPredicate_irrelevantKeyword_noResultsFound() { + fillFullPayments(); + assertDoesNotThrow(() -> payments.setSearchPredicate(IRRELEVANT_KEYWORD)); + assertTrue(payments.asUnmodifiableFilteredList().isEmpty()); + } + + @Test + public void searchPredicate_relevantKeyword_ResultsFound() { + fillFullPayments(); + assertDoesNotThrow(() -> payments.setSearchPredicate(RELEVANT_KEYWORD)); + + PaymentList expectedPayments = new PaymentList(); + expectedPayments.add(RETURN_MONEY); + assertEquals(expectedPayments.asUnmodifiableFilteredList(), + payments.asUnmodifiableFilteredList()); + } + + @Test + public void setSortingCriteria_nullSortingCriteria_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> payments.setSortingCriteria(null)); + } + + @Test + public void setSortingCriteria_unidentifiableSortingCriteria_throwsDukeException() { + assertThrows(DukeException.class, () -> payments.setSortingCriteria(UNIDENTIFIABLE_SORTING_CRITERIA)); + } + + @Test + public void setSortingCriteria_amount_paymentsSortedByAmount() throws DukeException { + fillFullPayments(); + assertDoesNotThrow(() -> payments.setSortingCriteria(AMOUNT_SORTING_CRITERIA)); + + for (int index = 0; index < PAYMENTS_FULL_SIZE - 1; index++) { // 0-based index + BigDecimal thisAmount = payments.asUnmodifiableFilteredList().get(index).getAmount(); + BigDecimal nextAmount = payments.asUnmodifiableFilteredList().get(index + 1).getAmount(); + assertTrue(thisAmount.compareTo(nextAmount) >= 0); + } + } + + @Test + public void setSortingCriteria_priority_paymentsSortedByPriority() throws DukeException { + fillFullPayments(); + assertDoesNotThrow(() -> payments.setSortingCriteria(PRIORITY_SORTING_CRITERIA)); + + for (int index = 0; index < PAYMENTS_FULL_SIZE - 1; index++) { // 0-based + int thisPriority = payments.asUnmodifiableFilteredList().get(index).getNumeratedPriority(); + int nextPriority = payments.asUnmodifiableFilteredList().get(index + 1).getNumeratedPriority(); + assertTrue(thisPriority >= nextPriority); + } + } + + @Test + public void asUnmodifiableFilteredList_modifyList_throwsUnsupportedOperationException() { + assertThrows(UnsupportedOperationException.class, () -> payments.asUnmodifiableFilteredList().remove(0)); + // 0-based + } + + /** + * Fills the {@code payments} with all five samples. + */ + private void fillFullPayments() { + payments.add(STORAGE_FEE); + payments.add(ORIENTATION_FEE); + payments.add(HALL_MEAL); + payments.add(HOSTEL_FEE); + payments.add(RETURN_MONEY); + } +} diff --git a/src/test/java/duke/model/payment/PaymentOverduePredicateTest.java b/src/test/java/duke/model/payment/PaymentOverduePredicateTest.java new file mode 100644 index 0000000000..c8530e923e --- /dev/null +++ b/src/test/java/duke/model/payment/PaymentOverduePredicateTest.java @@ -0,0 +1,41 @@ +package duke.model.payment; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class PaymentOverduePredicateTest { + + private static final LocalDate LAST_DAY = LocalDate.now().minusDays(1); + private static final String LAST_DAY_DUE = LAST_DAY.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + private static final LocalDate NEXT_DAY = LocalDate.now().plusDays(1); + private static final String NEXT_DAY_DUE = NEXT_DAY.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + + + private PaymentOverduePredicate predicate = new PaymentOverduePredicate(); + + @Test + public void test_nullObject_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> predicate.test(null)); + } + + @Test + public void test_paymentOverdue_returnsTrue() throws DukeException { + assertTrue(predicate.test(new Payment.Builder().setDue(LAST_DAY_DUE).build())); + } + + @Test + public void test_paymentNotOverdue_returnsFalse() throws DukeException { + assertFalse(predicate.test(new Payment.Builder().setDue(NEXT_DAY_DUE).build())); + } +} diff --git a/src/test/java/duke/model/payment/PaymentTest.java b/src/test/java/duke/model/payment/PaymentTest.java new file mode 100644 index 0000000000..3c7c9a5bcf --- /dev/null +++ b/src/test/java/duke/model/payment/PaymentTest.java @@ -0,0 +1,112 @@ +package duke.model.payment; + +import duke.exception.DukeException; +import org.junit.jupiter.api.Test; + +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PaymentTest { + + private static final Payment.Builder NULL_BUILDER = null; + private static final String NULL_STRING = null; + + private static final String DESCRIPTION_FOR_TEST = "Orientation Fee"; + + private static final String RECEIVER_FOR_TEST = "OSA"; + + private static final String DUE_FOR_TEST = "09/09/2019"; + private static final String INVALID_DUE_FOR_TEST = "9/9/2019"; + + private static final String TAG_FOR_TEST = "School Life"; + + private static final String AMOUNT_FOR_TEST = "30.5"; + private static final String INVALID_AMOUNT_FOR_TEST = "thirty.five"; + + private static final String PRIORITY_FOR_TEST = "Medium"; + private static final String INVALID_PRIORITY_FOR_TEST = "Very High"; + private static final int NUMERATED_PRIORITY_FOR_TEST = 2; + + private static final String KEYWORD_FOUND = "Orientation"; + private static final String KEYWORD_NOT_FOUND = "Transportation"; + + private Payment.Builder paymentBuilder = new Payment.Builder(); + + @Test + public void constructor_nullBuilder_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Payment(NULL_BUILDER)); + } + + @Test + public void getDescription() { + assertThrows(NullPointerException.class, () -> paymentBuilder.setDescription(NULL_STRING)); + + Payment payment = paymentBuilder.setDescription(DESCRIPTION_FOR_TEST).build(); + + assertEquals(DESCRIPTION_FOR_TEST, payment.getDescription()); + } + + @Test + public void getReceiver() { + assertThrows(NullPointerException.class, () -> paymentBuilder.setReceiver(NULL_STRING)); + + Payment payment = paymentBuilder.setReceiver(RECEIVER_FOR_TEST).build(); + + assertEquals(RECEIVER_FOR_TEST, payment.getReceiver()); + } + + @Test + public void getDue() throws DukeException { + assertThrows(NullPointerException.class, () -> paymentBuilder.setReceiver(NULL_STRING)); + assertThrows(DukeException.class, () -> paymentBuilder.setDue(INVALID_DUE_FOR_TEST)); + + Payment payment = paymentBuilder.setDue(DUE_FOR_TEST).build(); + + assertEquals(DUE_FOR_TEST, payment.getDue().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))); + } + + @Test + public void getTag() { + assertThrows(NullPointerException.class, () -> paymentBuilder.setTag(NULL_STRING)); + + Payment payment = paymentBuilder.setTag(TAG_FOR_TEST).build(); + + assertEquals(TAG_FOR_TEST.toUpperCase(), payment.getTag()); + } + + @Test + public void getAmount() throws DukeException { + assertThrows(NullPointerException.class, () -> paymentBuilder.setAmount(NULL_STRING)); + assertThrows(DukeException.class, () -> paymentBuilder.setAmount(INVALID_AMOUNT_FOR_TEST)); + + Payment payment = paymentBuilder.setAmount(AMOUNT_FOR_TEST).build(); + + assertEquals(AMOUNT_FOR_TEST, payment.getAmount().toString()); + } + + @Test + public void getPriority() throws DukeException { + assertThrows(NullPointerException.class, () -> paymentBuilder.setPriority(NULL_STRING)); + assertThrows(DukeException.class, () -> paymentBuilder.setPriority(INVALID_PRIORITY_FOR_TEST)); + + Payment payment = paymentBuilder.setPriority(PRIORITY_FOR_TEST).build(); + + assertEquals(PRIORITY_FOR_TEST, payment.getPriority().toString()); + // Tests the getNumerated() method by the way. + assertEquals(NUMERATED_PRIORITY_FOR_TEST, payment.getNumeratedPriority()); + } + + @Test + public void containsKeyword() throws DukeException { + Payment payment = paymentBuilder.setDescription(DESCRIPTION_FOR_TEST) + .setReceiver(RECEIVER_FOR_TEST) + .setTag(TAG_FOR_TEST).build(); + + assertTrue(payment.containsKeyword(KEYWORD_FOUND)); + assertFalse(payment.containsKeyword(KEYWORD_NOT_FOUND)); + } +} diff --git a/src/test/java/duke/model/payment/SearchKeywordPredicateTest.java b/src/test/java/duke/model/payment/SearchKeywordPredicateTest.java new file mode 100644 index 0000000000..d894c409f6 --- /dev/null +++ b/src/test/java/duke/model/payment/SearchKeywordPredicateTest.java @@ -0,0 +1,84 @@ +package duke.model.payment; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class SearchKeywordPredicateTest { + + private static final String DESCRIPTION = "Orientation Fee"; + private static final String DESCRIPTION_KEYWORD = "Orientation"; + private static final String MIXED_CASE_DESCRIPTION_KEYWORD = "oRIentaTion"; + + private static final String RECEIVER = "OSA"; + private static final String RECEIVER_KEYWORD = "OSA"; + private static final String MIXED_CASE_RECEIVER_KEYWORD = "osa"; + + private static final String TAG = "School Life"; + private static final String TAG_KEYWORD = "School"; + private static final String MIXED_CASE_TAG_KEYWORD = "school"; + + private static final String KEYWORD_NOT_FOUND = "Transportation"; + + @Test + public void test_nullObject_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new SearchKeywordPredicate(null)); + } + + @Test + public void test_descriptionContainsKeyword_returnsTrue() { + + Payment payment = new Payment.Builder().setDescription(DESCRIPTION).build(); + + // Searches in description + SearchKeywordPredicate predicate = new SearchKeywordPredicate(DESCRIPTION_KEYWORD); + assertTrue(predicate.test(payment)); + + // Mixed-case description keywords + predicate = new SearchKeywordPredicate(MIXED_CASE_DESCRIPTION_KEYWORD); + assertTrue(predicate.test(payment)); + } + + @Test + public void test_receiverContainsKeyword_returnsTrue() { + + Payment payment = new Payment.Builder().setReceiver(RECEIVER).build(); + + // Searches in receiver + SearchKeywordPredicate predicate = new SearchKeywordPredicate(RECEIVER_KEYWORD); + assertTrue(predicate.test(payment)); + + // Mixed-case receiver keywords + predicate = new SearchKeywordPredicate(MIXED_CASE_RECEIVER_KEYWORD); + assertTrue(predicate.test(payment)); + } + + @Test + public void test_tagContainsKeyword_returnsTrue() { + + Payment payment = new Payment.Builder().setTag(TAG).build(); + + // Searches in tag + SearchKeywordPredicate predicate = new SearchKeywordPredicate(TAG_KEYWORD); + assertTrue(predicate.test(payment)); + + // Mixed-case tag keywords + predicate = new SearchKeywordPredicate(MIXED_CASE_TAG_KEYWORD); + assertTrue(predicate.test(payment)); + } + + @Test + public void test_keywordNotFound_returnsFalse() { + Payment payment = new Payment.Builder().setDescription(DESCRIPTION) + .setReceiver(RECEIVER) + .setTag(TAG).build(); + + SearchKeywordPredicate predicate = new SearchKeywordPredicate(KEYWORD_NOT_FOUND); + assertFalse(predicate.test(payment)); + } +} diff --git a/text-ui-test/EXPECTED.txt b/text-ui-test/EXPECTED.txt new file mode 100644 index 0000000000..782d0796bc --- /dev/null +++ b/text-ui-test/EXPECTED.txt @@ -0,0 +1,31 @@ +Hello from + ____ _ +| _ \ _ _| | _____ +| | | | | | | |/ / _ \ +| |_| | |_| | < __/ +|____/ \__,_|_|\_\___| + +I am Duke. What can I do for you? +Ops, you haven't added any task! +Got it. I've added this task: +[T][✘] go to study +Now you have 1 expensesList in the list. +Got it. I've added this task: +[E][✘] attend the party (at: 02/02/2019 1800 - 02/03/2019 0300) +Now you have 2 expensesList in the list. +Got it. I've added this task: +[D][✘] return the book (by: 02/03/2019 1900) +Now you have 3 expensesList in the list. +Nice! I've marked this task as done: +[E][✓] attend the party (at: 02/02/2019 1800 - 02/03/2019 0300) +Here are the matching expensesList in your list: +1.[E][✓] attend the party (at: 02/02/2019 1800 - 02/03/2019 0300) +No results found. +Here are the expensesList in your list: +1.[T][✘] go to study +2.[E][✓] attend the party (at: 02/02/2019 1800 - 02/03/2019 0300) +3.[D][✘] return the book (by: 02/03/2019 1900) +Noted. I've removed this task: +[T][✘] go to study +Now you have 2 expensesList in the list. +Bye. Hope to see you again soon! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt new file mode 100644 index 0000000000..1fbf7f6315 --- /dev/null +++ b/text-ui-test/input.txt @@ -0,0 +1,12 @@ +list +todo go to study +event attend the party /at 02/02/2019 1800 - 02/03/2019 0300 +deadline return the book /by 02/03/2019 1900 +done 2 +find attend +find sdsds +list +delete 1 +bye + + diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh new file mode 100644 index 0000000000..240b58717c --- /dev/null +++ b/text-ui-test/runtest.sh @@ -0,0 +1,41 @@ + #!/usr/bin/env bash + + # create bin directory if it doesn't exist + if [ ! -d "../bin" ] + then + mkdir ../bin + fi + + # delete output from previous run + if [ -e "./ACTUAL.TXT" ] + then + rm ACTUAL.TXT + fi + + if [ -e "../data/ExpenseListStorage.txt" ] + then + rm ../data/ExpenseListStorage.txt + fi + + cd .. + + # compile the code into the bin folder, terminates if error occurred + if ! javac -cp src/main/java -Xlint:none -d bin src/main/java/Duke.java + then + echo "********** BUILD FAILURE **********" + exit 1 + fi + + # run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT + java -classpath bin Duke < text-ui-test/input.txt > text-ui-test/ACTUAL.TXT + + # compare the output to the expected output + diff text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED.TXT + if [ $? -eq 0 ] + then + echo "Test result: PASSED" + exit 0 + else + echo "Test result: FAILED" + exit 1 + fi diff --git a/tutorials/gradleTutorial.md b/tutorials/gradleTutorial.md index 08292b118d..4088c313aa 100644 --- a/tutorials/gradleTutorial.md +++ b/tutorials/gradleTutorial.md @@ -17,9 +17,9 @@ As a developer, you write a _build file_ that describes the project. A build fil * **Plugins** extend the functionality of Gradle. For example, the `java` plugin adds support for `Java` projects. -* **Tasks** are reusable blocks of logic. For example, the task `clean` simply deletes the project build directory. Tasks can be composed of other tasks or be dependent on another task. +* **Tasks** are reusable blocks of logic. For example, the task `clean` simply deletes the project build directory. Tasks can be composed of other expensesList or be dependent on another task. -* **Properties** change the behavior of tasks. For instance, `mainClassName` of the `application` plugin is a compulsory property which tells Gradle which class is the entrypoint to your application. +* **Properties** change the behavior of expensesList. For instance, `mainClassName` of the `application` plugin is a compulsory property which tells Gradle which class is the entrypoint to your application. As Gradle favors [_convention over configuration_](https://en.wikipedia.org/wiki/Convention_over_configuration), there is not much to you need to configure if you follow the recommended directory structure. ## Adding Gradle Support to Your Project @@ -39,10 +39,10 @@ As a developer, you write a _build file_ that describes the project. A build fil 1. To check if Gradle has been added to the project correctly, open a terminal window, navigate to the root directory of your project and run the command `gradlew run`. This should result in Gradle running the main method of your project. :bulb: Simply run the command `gradlew {taskName}` in the terminal and Gradle will run the task! Here are some example commands: -* `gradlew tasks` (or `gradlew tasks --all`): shows a list of tasks available +* `gradlew expensesList` (or `gradlew expensesList --all`): shows a list of expensesList available * `gradlew run`: runs the main class of your project -:bulb: Some plugins may add more helpful tasks so be sure to check their documentation! +:bulb: Some plugins may add more helpful expensesList so be sure to check their documentation! #### Using Gradle from within Intellij @@ -53,15 +53,15 @@ As a developer, you write a _build file_ that describes the project. A build fil 1. If the above didn't work either, close Intellij, delete the Intellij project files (i.e., `.idea` folder and `*.iml` files), and set up the project again, but instead of choosing `Create project from existing sources`, choose `Import project from external model` -> `Gradle`. -After this, IntelliJ IDEA will identify your project as a Gradle project and you will gain access to the `Gradle Toolbar`. Through the toolbar, you run Gradle tasks and view your project's dependencies. +After this, IntelliJ IDEA will identify your project as a Gradle project and you will gain access to the `Gradle Toolbar`. Through the toolbar, you run Gradle expensesList and view your project's dependencies. -You can click on the Gradle icon in the Gradle toolbar and create a new run configuration for running Gradle tasks without needing to type a `gradlew` command. +You can click on the Gradle icon in the Gradle toolbar and create a new run configuration for running Gradle expensesList without needing to type a `gradlew` command. ![Gradle icon](assets/GradleIcon.png) ## Adding Plugins -Gradle plugins are reusable units of build logic. Most common build tasks are provided as core plugins by Gradle. Given below are instructions on how to use some useful plugins: +Gradle plugins are reusable units of build logic. Most common build expensesList are provided as core plugins by Gradle. Given below are instructions on how to use some useful plugins: ### CheckStyle @@ -161,7 +161,7 @@ You can now write a test (e.g., `test/java/seedu/duke/DukeTest.java`) and run it ## Further Reading -Now that you have a general idea of how to accomplish basic tasks with Gradle, here's a list of material you can read to further your understanding. +Now that you have a general idea of how to accomplish basic expensesList with Gradle, here's a list of material you can read to further your understanding. * [Official Gradle Documentation](https://docs.gradle.org/current/userguide/userguide.html) diff --git a/tutorials/javaFxTutorialPart3.md b/tutorials/javaFxTutorialPart3.md index a9e1bdddd3..bd123fafbf 100644 --- a/tutorials/javaFxTutorialPart3.md +++ b/tutorials/javaFxTutorialPart3.md @@ -228,7 +228,7 @@ You have successfully implemented a fully functional GUI for Duke! ## Exercises -1. While the GUI looks similar to the mockup, there are still parts that need to be refined. Try your hand at some of these tasks: +1. While the GUI looks similar to the mockup, there are still parts that need to be refined. Try your hand at some of these expensesList: * Add padding between each DialogBox * Add padding between each ImageView and its Label * Clip the ImageView into a circle diff --git a/tutorials/javaFxTutorialPart4.md b/tutorials/javaFxTutorialPart4.md index 0e0ab280c4..1a6e5bc412 100644 --- a/tutorials/javaFxTutorialPart4.md +++ b/tutorials/javaFxTutorialPart4.md @@ -48,15 +48,15 @@ Create the following files in `src/main/resources/view`: - - -