From 9fc90b81752b272739eef28a74bf65fc08b609f0 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sat, 14 Jan 2023 17:52:47 +0100
Subject: [PATCH] #724 - parse date from csv

---
 .../TransactionImportController.java          | 52 ++++++++++++-------
 .../csvimport/CsvTransaction.java             |  7 +--
 .../CsvTransactionParseException.java         |  9 ++++
 .../transactions/csvimport/DateParser.java    | 32 ++++++++++++
 .../resources/languages/base_de.properties    |  1 +
 .../resources/languages/base_en.properties    |  1 +
 .../transaction/csvimport/DateParserTest.java | 41 +++++++++++++++
 7 files changed, 122 insertions(+), 21 deletions(-)
 create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransactionParseException.java
 create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/DateParser.java
 create mode 100644 BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/transaction/csvimport/DateParserTest.java

diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java
index 95ef20d1d..5677e13ca 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionImportController.java
@@ -4,7 +4,9 @@ import de.deadlocker8.budgetmaster.accounts.AccountService;
 import de.deadlocker8.budgetmaster.categories.CategoryService;
 import de.deadlocker8.budgetmaster.categories.CategoryType;
 import de.deadlocker8.budgetmaster.controller.BaseController;
+import de.deadlocker8.budgetmaster.services.DateFormatStyle;
 import de.deadlocker8.budgetmaster.services.HelpersService;
+import de.deadlocker8.budgetmaster.settings.SettingsService;
 import de.deadlocker8.budgetmaster.transactions.csvimport.*;
 import de.deadlocker8.budgetmaster.utils.Mappings;
 import de.thecodelabs.utils.util.Localization;
@@ -60,14 +62,16 @@ public class TransactionImportController extends BaseController
 	private final HelpersService helpers;
 	private final CategoryService categoryService;
 	private final AccountService accountService;
+	private final SettingsService settingsService;
 
 	@Autowired
-	public TransactionImportController(TransactionService transactionService, HelpersService helpers, CategoryService categoryService, AccountService accountService)
+	public TransactionImportController(TransactionService transactionService, HelpersService helpers, CategoryService categoryService, AccountService accountService, SettingsService settingsService)
 	{
 		this.transactionService = transactionService;
 		this.helpers = helpers;
 		this.categoryService = categoryService;
 		this.accountService = accountService;
+		this.settingsService = settingsService;
 	}
 
 	@GetMapping
@@ -159,25 +163,17 @@ public class TransactionImportController extends BaseController
 			final CsvRow csvRow = csvRows.get(i);
 			try
 			{
-				final String date = csvRow.getColumns().get(csvColumnSettings.columnDate() - 1);
-				final String name = csvRow.getColumns().get(csvColumnSettings.columnName() - 1);
-				final String description = csvRow.getColumns().get(csvColumnSettings.columnDescription() - 1);
-
-				final String amount = csvRow.getColumns().get(csvColumnSettings.columnAmount() - 1);
-				final Optional<Integer> parsedAmountOptional = AmountParser.parse(amount);
-				if(parsedAmountOptional.isEmpty())
-				{
-					errors.add(Localization.getString("transactions.import.error.parse.amount", i, csvRow));
-					continue;
-				}
-
-				csvTransactions.add(new CsvTransaction(date, name, parsedAmountOptional.get(), description, CsvTransactionStatus.PENDING));
+				csvTransactions.add(createCsvTransactionFromCsvRow(csvRow, csvColumnSettings, i));
 			}
 			catch(IndexOutOfBoundsException e)
 			{
 				LOGGER.error("Invalid access to column", e);
 				errors.add(Localization.getString("transactions.import.error.column", i, csvRow));
 			}
+			catch(CsvTransactionParseException e)
+			{
+				errors.add(e.getMessage());
+			}
 		}
 
 		request.setAttribute(RequestAttributeNames.ERRORS_COLUMN_SETTINGS, errors, RequestAttributes.SCOPE_SESSION);
@@ -186,6 +182,28 @@ public class TransactionImportController extends BaseController
 		return ReturnValues.REDIRECT_IMPORT;
 	}
 
+	private CsvTransaction createCsvTransactionFromCsvRow(CsvRow csvRow, CsvColumnSettings csvColumnSettings, Integer index) throws CsvTransactionParseException
+	{
+		final String date = csvRow.getColumns().get(csvColumnSettings.columnDate() - 1);
+		final Optional<LocalDate> parsedDateOptional = DateParser.parse(date, DateFormatStyle.LONG.getKey(), settingsService.getSettings().getLanguage().getLocale());
+		if(parsedDateOptional.isEmpty())
+		{
+			throw new CsvTransactionParseException(Localization.getString("transactions.import.error.parse.date", index, csvRow));
+		}
+
+		final String name = csvRow.getColumns().get(csvColumnSettings.columnName() - 1);
+		final String description = csvRow.getColumns().get(csvColumnSettings.columnDescription() - 1);
+
+		final String amount = csvRow.getColumns().get(csvColumnSettings.columnAmount() - 1);
+		final Optional<Integer> parsedAmountOptional = AmountParser.parse(amount);
+		if(parsedAmountOptional.isEmpty())
+		{
+			throw new CsvTransactionParseException(Localization.getString("transactions.import.error.parse.amount", index, csvRow));
+		}
+
+		return new CsvTransaction(parsedDateOptional.get(), name, parsedAmountOptional.get(), description, CsvTransactionStatus.PENDING);
+	}
+
 	@GetMapping("/cancel")
 	public String cancel(WebRequest request)
 	{
@@ -223,8 +241,7 @@ public class TransactionImportController extends BaseController
 
 		final Transaction newTransaction = createTransactionFromCsvTransaction(csvTransaction);
 
-		// TODO use csvTransaction.getDate() instead of debug date
-		transactionService.prepareModelNewOrEdit(model, false, LocalDate.now(), false, newTransaction, accountService.getAllActivatedAccountsAsc());
+		transactionService.prepareModelNewOrEdit(model, false, csvTransaction.getDate(), false, newTransaction, accountService.getAllActivatedAccountsAsc());
 
 		if(type.equals("transfer"))
 		{
@@ -260,8 +277,7 @@ public class TransactionImportController extends BaseController
 	private Transaction createTransactionFromCsvTransaction(CsvTransaction csvTransaction)
 	{
 		final Transaction newTransaction = new Transaction();
-		// TODO parse first
-//		newTransaction.setDate(csvTransaction.getDate());
+		newTransaction.setDate(csvTransaction.getDate());
 		newTransaction.setName(csvTransaction.getName());
 		newTransaction.setDescription(csvTransaction.getDescription());
 		newTransaction.setAmount(csvTransaction.getAmount());
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransaction.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransaction.java
index 82b4a7058..70c46064a 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransaction.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransaction.java
@@ -1,16 +1,17 @@
 package de.deadlocker8.budgetmaster.transactions.csvimport;
 
+import java.time.LocalDate;
 import java.util.Objects;
 
 public final class CsvTransaction
 {
-	private final String date;
+	private final LocalDate date;
 	private String name;
 	private final Integer amount;
 	private String description;
 	private CsvTransactionStatus status;
 
-	public CsvTransaction(String date, String name, Integer amount, String description, CsvTransactionStatus status)
+	public CsvTransaction(LocalDate date, String name, Integer amount, String description, CsvTransactionStatus status)
 	{
 		this.date = date;
 		this.name = name;
@@ -19,7 +20,7 @@ public final class CsvTransaction
 		this.status = status;
 	}
 
-	public String getDate()
+	public LocalDate getDate()
 	{
 		return date;
 	}
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransactionParseException.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransactionParseException.java
new file mode 100644
index 000000000..9e36a3bf9
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/CsvTransactionParseException.java
@@ -0,0 +1,9 @@
+package de.deadlocker8.budgetmaster.transactions.csvimport;
+
+public class CsvTransactionParseException extends Exception
+{
+	public CsvTransactionParseException(String message)
+	{
+		super(message);
+	}
+}
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/DateParser.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/DateParser.java
new file mode 100644
index 000000000..80afaa8f4
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/transactions/csvimport/DateParser.java
@@ -0,0 +1,32 @@
+package de.deadlocker8.budgetmaster.transactions.csvimport;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Locale;
+import java.util.Optional;
+
+public class DateParser
+{
+
+	private DateParser()
+	{
+	}
+
+	public static Optional<LocalDate> parse(String dateString, String pattern, Locale locale)
+	{
+		if(dateString == null || pattern == null || locale == null)
+		{
+			return Optional.empty();
+		}
+
+		try
+		{
+			return Optional.of(LocalDate.parse(dateString, DateTimeFormatter.ofPattern(pattern).withLocale(locale)));
+		}
+		catch(DateTimeParseException e)
+		{
+			return Optional.empty();
+		}
+	}
+}
diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties
index 19640400c..9de718137 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_de.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties
@@ -382,6 +382,7 @@ transactions.import.status.skipped=übersprungen
 transactions.import.actions=Aktionen
 transactions.import.error.column=Zugeordnete Spalten in Zeile {0} (Zählung beginnt relativ zu Anzahl übersprungener Zeilen) nicht gefunden: {1}
 transactions.import.error.parse.amount=Fehler beim Parsen des Betrags in Zeile {0} (Zählung beginnt relativ zu Anzahl übersprungener Zeilen)
+transactions.import.error.parse.date=Fehler beim Parsen des Datums in Zeile {0} (Zählung beginnt relativ zu Anzahl übersprungener Zeilen)
 
 repeating.button.add=Wiederholung hinzufügen
 repeating.button.remove=Wiederholung entfernen
diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties
index 58d90b49a..f54cefd95 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_en.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties
@@ -381,6 +381,7 @@ transactions.import.status.skipped=skipped
 transactions.import.actions=Actions
 transactions.import.error.column=Associated columns not found in row {0} (counting starts relative to the number of skipped rows): {1}
 transactions.import.error.parse.amount=Error parsing the amount in line {0} (counting starts relative to number of skipped lines)
+transactions.import.error.parse.date=Error parsing the date in line {0} (counting starts relative to number of skipped lines)
 
 repeating.button.add=Add repetition
 repeating.button.remove=Remove repetition
diff --git a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/transaction/csvimport/DateParserTest.java b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/transaction/csvimport/DateParserTest.java
new file mode 100644
index 000000000..8ba22b79f
--- /dev/null
+++ b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/transaction/csvimport/DateParserTest.java
@@ -0,0 +1,41 @@
+package de.deadlocker8.budgetmaster.unit.transaction.csvimport;
+
+import de.deadlocker8.budgetmaster.transactions.csvimport.DateParser;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.util.Locale;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DateParserTest
+{
+	@Test
+	void test_nullText()
+	{
+		assertThat(DateParser.parse(null, "dd.MM", Locale.ENGLISH))
+				.isEmpty();
+	}
+
+	@Test
+	void test_nullPattern()
+	{
+		assertThat(DateParser.parse("14.01.23", null, Locale.ENGLISH))
+				.isEmpty();
+	}
+
+	@Test
+	void test_textNotMatchingPattern()
+	{
+		assertThat(DateParser.parse("14.01.23", "dd.MM", Locale.ENGLISH))
+				.isEmpty();
+	}
+
+	@Test
+	void test_matchingPattern()
+	{
+		assertThat(DateParser.parse("14.01.23", "dd.MM.yy", Locale.ENGLISH))
+				.isPresent()
+				.get().isEqualTo(LocalDate.of(2023, 1, 14));
+	}
+}
-- 
GitLab