rework transaction date handling
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Tue, 16 Jun 2020 05:16:39 +0000 (05:16 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Wed, 17 Jun 2020 06:04:07 +0000 (06:04 +0000)
the surface problem is that storing dates as strings in hledger format
(yyyy/mm/dd) doesn't guarantee sort order. An year of 12020 is a typo,
but goes unnoticed and sorts earlier than 2020. Splitting the date into
separate numeric columns for year, month and day makes sorting work as needed.

While there, replace in-memory storage from java's Date to a SimpleDate
object which just holds the year, month and day. Simplifies exchanging
dates with the database and fulfils the task of holding a date just
fine. The extra flexibility of adding intervals to dates or holding
also time components is not needed anyway.

17 files changed:
app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java
app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedLedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedLedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java
app/src/main/java/net/ktnx/mobileledger/model/TransactionListItem.java
app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java
app/src/main/java/net/ktnx/mobileledger/utils/Globals.java
app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java
app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java [new file with mode: 0644]
app/src/main/res/raw/create_db.sql
app/src/main/res/raw/sql_34.sql [new file with mode: 0644]

index 847e8c2..0fd4a1b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -32,6 +32,7 @@ import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.utils.Globals;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.NetworkUtil;
+import net.ktnx.mobileledger.utils.SimpleDate;
 import net.ktnx.mobileledger.utils.UrlEncodedFormData;
 
 import java.io.BufferedReader;
@@ -41,8 +42,6 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -176,9 +175,9 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
         if (token != null)
             params.addPair("_token", token);
 
-        Date transactionDate = ltr.getDate();
+        SimpleDate transactionDate = ltr.getDate();
         if (transactionDate == null) {
-            transactionDate = new GregorianCalendar().getTime();
+            transactionDate = SimpleDate.today();
         }
 
         params.addPair("date", Globals.formatLedgerDate(transactionDate));
index 1152cde..bb3d3dd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -26,12 +26,9 @@ import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
-import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
@@ -50,46 +47,44 @@ public class UpdateTransactionsTask extends AsyncTask<String, Void, String> {
             String[] params;
 
             if (filterAccName[0] == null) {
-                sql = "SELECT id, date FROM transactions WHERE profile=? ORDER BY date desc, id " +
-                      "desc";
+                sql = "SELECT id, year, month, day FROM transactions WHERE profile=? ORDER BY " +
+                      "year desc, month desc, day desc, id desc";
                 params = new String[]{profile_uuid};
 
             }
             else {
-                sql = "SELECT distinct tr.id, tr.date from transactions tr JOIN " +
-                      "transaction_accounts ta " +
+                sql = "SELECT distinct tr.id, tr.year, tr.month, tr.day from transactions tr " +
+                      "JOIN " + "transaction_accounts ta " +
                       "ON ta.transaction_id=tr.id AND ta.profile=tr.profile WHERE tr.profile=? " +
                       "and ta.account_name LIKE ?||'%' AND ta" +
-                      ".amount <> 0 ORDER BY tr.date desc, tr.id desc";
+                      ".amount <> 0 ORDER BY tr.year desc, tr.month desc, tr.day desc, tr.id " +
+                      "desc";
                 params = new String[]{profile_uuid, filterAccName[0]};
             }
 
             debug("UTT", sql);
             SQLiteDatabase db = App.getDatabase();
-            String lastDateString = Globals.formatLedgerDate(new Date());
-            Calendar lastDate = Globals.parseLedgerDateAsCalendar(lastDateString);
             boolean odd = true;
+            SimpleDate lastDate = SimpleDate.today();
             try (Cursor cursor = db.rawQuery(sql, params)) {
                 while (cursor.moveToNext()) {
                     if (isCancelled())
                         return null;
 
                     int transaction_id = cursor.getInt(0);
-                    String dateString = cursor.getString(1);
-                    Calendar date = Globals.parseLedgerDateAsCalendar(dateString);
+                    SimpleDate date =
+                            new SimpleDate(cursor.getInt(1), cursor.getInt(2), cursor.getInt(3));
 
-                    if (!lastDateString.equals(dateString)) {
+                    if (!date.equals(lastDate)) {
                         boolean showMonth =
-                                (date.get(Calendar.MONTH) != lastDate.get(Calendar.MONTH)) ||
-                                (date.get(Calendar.YEAR) != lastDate.get(Calendar.YEAR));
-                        newList.add(new TransactionListItem(date.getTime(), showMonth));
+                                (date.month != lastDate.month) || (date.year != lastDate.year);
+                        newList.add(new TransactionListItem(date, showMonth));
                     }
                     newList.add(
                             new TransactionListItem(new LedgerTransaction(transaction_id), odd));
 //                    debug("UTT", String.format("got transaction %d", transaction_id));
 
                     lastDate = date;
-                    lastDateString = dateString;
                     odd = !odd;
                 }
                 Data.transactions.setList(newList);
@@ -98,9 +93,6 @@ public class UpdateTransactionsTask extends AsyncTask<String, Void, String> {
 
             return null;
         }
-        catch (ParseException e) {
-            return String.format("Error parsing stored date '%s'", e.getMessage());
-        }
         finally {
             Data.backgroundTaskFinished();
         }
index 926717c..5b9b399 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -23,11 +23,10 @@ import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.utils.Globals;
 import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -60,9 +59,9 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         }
 
         result.setTpostings(postings);
-        Date transactionDate = tr.getDate();
+        SimpleDate transactionDate = tr.getDate();
         if (transactionDate == null) {
-            transactionDate = new GregorianCalendar().getTime();
+            transactionDate = SimpleDate.today();
         }
         result.setTdate(Globals.formatIsoDate(transactionDate));
         result.setTdate2(null);
@@ -145,7 +144,7 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         tpostings.add(posting);
     }
     public LedgerTransaction asLedgerTransaction() throws ParseException {
-        Date date = Globals.parseIsoDate(tdate);
+        SimpleDate date = Globals.parseIsoDate(tdate);
         LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
         tr.setComment(Misc.trim(Misc.emptyIsNull(tcomment)));
 
index 305a2ad..225f162 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -23,11 +23,10 @@ import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.utils.Globals;
 import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -58,9 +57,9 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         }
 
         result.setTpostings(postings);
-        Date transactionDate = tr.getDate();
+        SimpleDate transactionDate = tr.getDate();
         if (transactionDate == null) {
-            transactionDate = new GregorianCalendar().getTime();
+            transactionDate = SimpleDate.today();
         }
         result.setTdate(Globals.formatIsoDate(transactionDate));
         result.setTdate2(null);
@@ -143,7 +142,7 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         tpostings.add(posting);
     }
     public LedgerTransaction asLedgerTransaction() throws ParseException {
-        Date date = Globals.parseIsoDate(tdate);
+        SimpleDate date = Globals.parseIsoDate(tdate);
         LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
         tr.setComment(Misc.trim(Misc.emptyIsNull(tcomment)));
 
index 8417eae..9b73d2c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -22,13 +22,13 @@ import android.database.sqlite.SQLiteDatabase;
 
 import net.ktnx.mobileledger.utils.Digest;
 import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import java.nio.charset.StandardCharsets;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.Date;
 
 public class LedgerTransaction {
     private static final String DIGEST_TYPE = "SHA-256";
@@ -53,7 +53,7 @@ public class LedgerTransaction {
             };
     private String profile;
     private Integer id;
-    private Date date;
+    private SimpleDate date;
     private String description;
     private String comment;
     private ArrayList<LedgerTransactionAccount> accounts;
@@ -63,7 +63,7 @@ public class LedgerTransaction {
             throws ParseException {
         this(id, Globals.parseLedgerDate(dateString), description);
     }
-    public LedgerTransaction(Integer id, Date date, String description,
+    public LedgerTransaction(Integer id, SimpleDate date, String description,
                              MobileLedgerProfile profile) {
         this.profile = profile.getUuid();
         this.id = id;
@@ -73,14 +73,14 @@ public class LedgerTransaction {
         this.dataHash = null;
         dataLoaded = false;
     }
-    public LedgerTransaction(Integer id, Date date, String description) {
+    public LedgerTransaction(Integer id, SimpleDate date, String description) {
         this(id, date, description, Data.profile.getValue());
     }
-    public LedgerTransaction(Date date, String description) {
+    public LedgerTransaction(SimpleDate date, String description) {
         this(null, date, description);
     }
     public LedgerTransaction(int id) {
-        this(id, (Date) null, null);
+        this(id, (SimpleDate) null, null);
     }
     public LedgerTransaction(int id, String profileUUID) {
         this.profile = profileUUID;
@@ -98,10 +98,10 @@ public class LedgerTransaction {
         accounts.add(item);
         dataHash = null;
     }
-    public Date getDate() {
+    public SimpleDate getDate() {
         return date;
     }
-    public void setDate(Date date) {
+    public void setDate(SimpleDate date) {
         this.date = date;
         dataHash = null;
     }
@@ -171,22 +171,13 @@ public class LedgerTransaction {
             return;
 
         try (Cursor cTr = db.rawQuery(
-                "SELECT date, description, comment from transactions WHERE profile=? AND id=?",
-                new String[]{profile, String.valueOf(id)}))
+                "SELECT year, month, day, description, comment from transactions WHERE profile=? " +
+                "AND id=?", new String[]{profile, String.valueOf(id)}))
         {
             if (cTr.moveToFirst()) {
-                String dateString = cTr.getString(0);
-                try {
-                    date = Globals.parseLedgerDate(dateString);
-                }
-                catch (ParseException e) {
-                    e.printStackTrace();
-                    throw new RuntimeException(
-                            String.format("Error parsing date '%s' from " + "transaction %d",
-                                    dateString, id));
-                }
-                description = cTr.getString(1);
-                comment = cTr.getString(2);
+                date = new SimpleDate(cTr.getInt(0), cTr.getInt(1), cTr.getInt(2));
+                description = cTr.getString(3);
+                comment = cTr.getString(4);
 
                 accounts.clear();
 
index 9d41795..925e05e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -293,11 +293,12 @@ public final class MobileLedgerProfile {
                 new Object[]{uuid, tr.getId()});
 
         db.execSQL(
-                "INSERT INTO transactions(profile, id, date, description, comment, data_hash, " +
-                "keep) " +
-                "values(?,?,?,?,?,?,1)",
-                new Object[]{uuid, tr.getId(), Globals.formatLedgerDate(tr.getDate()),
-                             tr.getDescription(), tr.getComment(), tr.getDataHash()
+                "INSERT INTO transactions(profile, id, year, month, day, description, "+
+                "comment, data_hash, keep) " +
+                "values(?,?,?,?,?,?,?,?,1)",
+                new Object[]{uuid, tr.getId(), tr.getDate().year, tr.getDate().month,
+                             tr.getDate().day, tr.getDescription(),
+                             tr.getComment(), tr.getDataHash()
                 });
 
         for (LedgerTransactionAccount item : tr.getAccounts()) {
index e24647f..ab8bb7a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
 
 package net.ktnx.mobileledger.model;
 
-import java.util.Date;
-
 import androidx.annotation.NonNull;
 
+import net.ktnx.mobileledger.utils.SimpleDate;
+
 public class TransactionListItem {
     private Type type;
-    private Date date;
+    private SimpleDate date;
     private boolean monthShown;
     private LedgerTransaction transaction;
     private boolean odd;
-    public TransactionListItem(Date date, boolean monthShown) {
+    public TransactionListItem(SimpleDate date, boolean monthShown) {
         this.type = Type.DELIMITER;
         this.date = date;
         this.monthShown = monthShown;
@@ -41,7 +41,7 @@ public class TransactionListItem {
     public Type getType() {
         return type;
     }
-    public Date getDate() {
+    public SimpleDate getDate() {
         return date;
     }
     public boolean isMonthShown() {
index 3115d47..80c69e5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -22,14 +22,15 @@ import android.os.Bundle;
 import android.widget.CalendarView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatDialogFragment;
 
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import java.util.Calendar;
 import java.util.GregorianCalendar;
-import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -40,12 +41,54 @@ public class DatePickerFragment extends AppCompatDialogFragment
     static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
     private Calendar presentDate = GregorianCalendar.getInstance();
     private DatePickedListener onDatePickedListener;
-    private MobileLedgerProfile.FutureDates futureDates = MobileLedgerProfile.FutureDates.None;
-    public MobileLedgerProfile.FutureDates getFutureDates() {
-        return futureDates;
+    private long minDate = 0;
+    private long maxDate = Long.MAX_VALUE;
+    public void setDateRange(@Nullable SimpleDate minDate, @Nullable SimpleDate maxDate) {
+        if (minDate == null)
+            this.minDate = 0;
+        else
+            this.minDate = minDate.toDate().getTime();
+
+        if (maxDate == null)
+            this.maxDate = Long.MAX_VALUE;
+        else
+            this.maxDate = maxDate.toDate().getTime();
     }
     public void setFutureDates(MobileLedgerProfile.FutureDates futureDates) {
-        this.futureDates = futureDates;
+        if (futureDates == MobileLedgerProfile.FutureDates.All) {
+            maxDate = Long.MAX_VALUE;
+        }
+        else {
+            final Calendar dateLimit = GregorianCalendar.getInstance();
+            switch (futureDates) {
+                case None:
+                    // already there
+                    break;
+                case OneWeek:
+                    dateLimit.add(Calendar.DAY_OF_MONTH, 7);
+                    break;
+                case TwoWeeks:
+                    dateLimit.add(Calendar.DAY_OF_MONTH, 14);
+                    break;
+                case OneMonth:
+                    dateLimit.add(Calendar.MONTH, 1);
+                    break;
+                case TwoMonths:
+                    dateLimit.add(Calendar.MONTH, 2);
+                    break;
+                case ThreeMonths:
+                    dateLimit.add(Calendar.MONTH, 3);
+                    break;
+                case SixMonths:
+                    dateLimit.add(Calendar.MONTH, 6);
+                    break;
+                case OneYear:
+                    dateLimit.add(Calendar.YEAR, 1);
+                    break;
+            }
+            maxDate = dateLimit.getTime()
+                               .getTime();
+        }
     }
     public void setCurrentDateFromText(CharSequence present) {
         final Calendar now = GregorianCalendar.getInstance();
@@ -85,40 +128,8 @@ public class DatePickerFragment extends AppCompatDialogFragment
         cv.setDate(presentDate.getTime()
                               .getTime());
 
-        if (futureDates == MobileLedgerProfile.FutureDates.All) {
-            cv.setMaxDate(Long.MAX_VALUE);
-        }
-        else {
-            final Calendar dateLimit = GregorianCalendar.getInstance();
-            switch (futureDates) {
-                case None:
-                    // already there
-                    break;
-                case OneWeek:
-                    dateLimit.add(Calendar.DAY_OF_MONTH, 7);
-                    break;
-                case TwoWeeks:
-                    dateLimit.add(Calendar.DAY_OF_MONTH, 14);
-                    break;
-                case OneMonth:
-                    dateLimit.add(Calendar.MONTH, 1);
-                    break;
-                case TwoMonths:
-                    dateLimit.add(Calendar.MONTH, 2);
-                    break;
-                case ThreeMonths:
-                    dateLimit.add(Calendar.MONTH, 3);
-                    break;
-                case SixMonths:
-                    dateLimit.add(Calendar.MONTH, 6);
-                    break;
-                case OneYear:
-                    dateLimit.add(Calendar.YEAR, 1);
-                    break;
-            }
-            cv.setMaxDate(dateLimit.getTime()
-                                   .getTime());
-        }
+        cv.setMinDate(minDate);
+        cv.setMaxDate(maxDate);
 
         cv.setOnDateChangeListener(this);
 
index 2b3adc9..e4ac779 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -46,11 +46,10 @@ import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import org.jetbrains.annotations.NotNull;
 
-import java.util.Date;
-
 /**
  * A simple {@link Fragment} subclass.
  * Activities that contain this fragment must implement the
@@ -197,7 +196,7 @@ public class NewTransactionFragment extends Fragment {
         fab.setEnabled(false);
         Misc.hideSoftKeyboard(this);
         if (mListener != null) {
-            Date date = viewModel.getDate();
+            SimpleDate date = viewModel.getDate();
             LedgerTransaction tr =
                     new LedgerTransaction(null, date, viewModel.getDescription(), mProfile);
 
index 50c321f..9af8bac 100644 (file)
@@ -50,11 +50,11 @@ import net.ktnx.mobileledger.utils.DimensionUtils;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.MLDB;
 import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import java.text.DecimalFormatSymbols;
-import java.util.Calendar;
+import java.text.ParseException;
 import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.Locale;
 
 import static net.ktnx.mobileledger.ui.activity.NewTransactionModel.ItemType;
@@ -77,7 +77,7 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
     private FrameLayout lPadding;
     private MobileLedgerProfile mProfile;
     private Date date;
-    private Observer<Date> dateObserver;
+    private Observer<SimpleDate> dateObserver;
     private Observer<String> descriptionObserver;
     private Observer<String> transactionCommentObserver;
     private Observer<String> hintObserver;
@@ -590,6 +590,9 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
 
             return true;
         }
+        catch (ParseException e) {
+            throw new RuntimeException("Should not happen", e);
+        }
         finally {
             syncingData = false;
         }
@@ -707,9 +710,7 @@ class NewTransactionItemHolder extends RecyclerView.ViewHolder
     }
     @Override
     public void onDatePicked(int year, int month, int day) {
-        final Calendar c = GregorianCalendar.getInstance();
-        c.set(year, month, day);
-        item.setDate(c.getTime());
+        item.setDate(new SimpleDate(year, month+1, day));
         boolean focused = tvDescription.requestFocus();
         if (focused)
             Misc.showSoftKeyboard((NewTransactionActivity) tvAccount.getContext());
index 83ffbb8..3350664 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -28,24 +28,20 @@ import net.ktnx.mobileledger.model.Currency;
 import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import org.jetbrains.annotations.NotNull;
 
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
-import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.Locale;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 public class NewTransactionModel extends ViewModel {
-    private static final Pattern reYMD =
-            Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-    private static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-    private static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
     final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
     final ArrayList<Item> items = new ArrayList<>();
     final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
@@ -89,7 +85,7 @@ public class NewTransactionModel extends ViewModel {
     int getAccountCount() {
         return items.size();
     }
-    public Date getDate() {
+    public SimpleDate getDate() {
         return header.date.getValue();
     }
     public String getDescription() {
@@ -232,7 +228,7 @@ public class NewTransactionModel extends ViewModel {
 
     static class Item {
         private ItemType type;
-        private MutableLiveData<Date> date = new MutableLiveData<>();
+        private MutableLiveData<SimpleDate> date = new MutableLiveData<>();
         private MutableLiveData<String> description = new MutableLiveData<>();
         private LedgerTransactionAccount account;
         private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
@@ -248,7 +244,7 @@ public class NewTransactionModel extends ViewModel {
             type = ItemType.bottomFiller;
             editable.setValue(false);
         }
-        Item(NewTransactionModel model, Date date, String description) {
+        Item(NewTransactionModel model, SimpleDate date, String description) {
             this.model = model;
             this.type = ItemType.generalData;
             this.date.setValue(date);
@@ -325,58 +321,30 @@ public class NewTransactionModel extends ViewModel {
                                 wantedType));
             }
         }
-        public Date getDate() {
+        public SimpleDate getDate() {
             ensureType(ItemType.generalData);
             return date.getValue();
         }
-        public void setDate(Date date) {
+        public void setDate(SimpleDate date) {
             ensureType(ItemType.generalData);
             this.date.setValue(date);
         }
-        public void setDate(String text) {
+        public void setDate(String text) throws ParseException {
             if ((text == null) || text.trim()
                                       .isEmpty())
             {
-                setDate((Date) null);
+                setDate((SimpleDate) null);
                 return;
             }
 
-            int year, month, day;
-            final Calendar c = GregorianCalendar.getInstance();
-            Matcher m = reYMD.matcher(text);
-            if (m.matches()) {
-                year = Integer.parseInt(m.group(1));
-                month = Integer.parseInt(m.group(2)) - 1;   // month is 0-based
-                day = Integer.parseInt(m.group(3));
-            }
-            else {
-                year = c.get(Calendar.YEAR);
-                m = reMD.matcher(text);
-                if (m.matches()) {
-                    month = Integer.parseInt(m.group(1)) - 1;
-                    day = Integer.parseInt(m.group(2));
-                }
-                else {
-                    month = c.get(Calendar.MONTH);
-                    m = reD.matcher(text);
-                    if (m.matches()) {
-                        day = Integer.parseInt(m.group(1));
-                    }
-                    else {
-                        day = c.get(Calendar.DAY_OF_MONTH);
-                    }
-                }
-            }
-
-            c.set(year, month, day);
-
-            this.setDate(c.getTime());
+            SimpleDate date = Globals.parseLedgerDate(text);
+            this.setDate(date);
         }
         void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                         @NonNull androidx.lifecycle.Observer<? super Date> observer) {
+                         @NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
             this.date.observe(owner, observer);
         }
-        void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
+        void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
             this.date.removeObserver(observer);
         }
         public String getDescription() {
@@ -426,27 +394,21 @@ public class NewTransactionModel extends ViewModel {
         String getFormattedDate() {
             if (date == null)
                 return null;
-            Date time = date.getValue();
-            if (time == null)
+            SimpleDate d = date.getValue();
+            if (d == null)
                 return null;
 
-            Calendar c = GregorianCalendar.getInstance();
-            c.setTime(time);
             Calendar today = GregorianCalendar.getInstance();
 
-            final int myYear = c.get(Calendar.YEAR);
-            final int myMonth = c.get(Calendar.MONTH);
-            final int myDay = c.get(Calendar.DAY_OF_MONTH);
-
-            if (today.get(Calendar.YEAR) != myYear) {
-                return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay);
+            if (today.get(Calendar.YEAR) != d.year) {
+                return String.format(Locale.US, "%d/%02d/%02d", d.year, d.month, d.day);
             }
 
-            if (today.get(Calendar.MONTH) != myMonth) {
-                return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay);
+            if (today.get(Calendar.MONTH) != d.month - 1) {
+                return String.format(Locale.US, "%d/%02d", d.month, d.day);
             }
 
-            return String.valueOf(myDay);
+            return String.valueOf(d.day);
         }
         void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
             editable.observe(activity, observer);
index 35e68e5..f8b8707 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -43,9 +43,9 @@ import net.ktnx.mobileledger.model.TransactionListItem;
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.Globals;
 import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import java.text.DateFormat;
-import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.TimeZone;
 
@@ -84,14 +84,14 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
                         View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                 break;
             case DELIMITER:
-                Date date = item.getDate();
+                SimpleDate date = item.getDate();
                 holder.vTransaction.setVisibility(View.GONE);
                 holder.vDelimiter.setVisibility(View.VISIBLE);
                 holder.tvDelimiterDate.setText(DateFormat.getDateInstance()
-                                                         .format(date));
+                                                         .format(date.toDate()));
                 if (item.isMonthShown()) {
                     GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
-                    cal.setTime(date);
+                    cal.setTime(date.toDate());
                     holder.tvDelimiterMonth.setText(
                             Globals.monthNames[cal.get(GregorianCalendar.MONTH)]);
                     holder.tvDelimiterMonth.setVisibility(View.VISIBLE);
index fe0cbe0..e1ce8ee 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import android.view.inputmethod.InputMethodManager;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
-import java.util.Date;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -58,43 +57,48 @@ public final class Globals {
             imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
         }
     }
-    public static Date parseLedgerDate(String dateString) throws ParseException {
+    public static SimpleDate parseLedgerDate(String dateString) throws ParseException {
         Matcher m = reLedgerDate.matcher(dateString);
         if (!m.matches()) throw new ParseException(
                 String.format("'%s' does not match expected pattern '%s'", dateString,
                         reLedgerDate.toString()), 0);
 
-        String year = m.group(1);
-        String month = m.group(2);
-        String day = m.group(3);
+        String yearStr = m.group(1);
+        String monthStr = m.group(2);
+        String dayStr = m.group(3);
+
+        int year, month, day;
 
         String toParse;
-        if (year == null) {
-            Calendar now = Calendar.getInstance();
-            int thisYear = now.get(Calendar.YEAR);
-            if (month == null) {
-                int thisMonth = now.get(Calendar.MONTH) + 1;
-                toParse = String.format(Locale.US, "%04d/%02d/%s", thisYear, thisMonth, dateString);
+        if (yearStr == null) {
+            SimpleDate today = SimpleDate.today();
+            year = today.year;
+            if (monthStr == null) {
+                month = today.month;
             }
-            else toParse = String.format(Locale.US, "%04d/%s", thisYear, dateString);
+            else month = Integer.parseInt(monthStr);
+        }
+        else {
+            year = Integer.parseInt(yearStr);
+            assert monthStr != null;
+            month = Integer.parseInt(monthStr);
         }
-        else toParse = dateString;
 
-        return dateFormatter.get().parse(toParse);
+        assert dayStr != null;
+        day = Integer.parseInt(dayStr);
+
+        return new SimpleDate(year, month, day);
     }
     public static Calendar parseLedgerDateAsCalendar(String dateString) throws ParseException {
-        Date date = parseLedgerDate(dateString);
-        Calendar calendar = Calendar.getInstance();
-        calendar.setTime(date);
-        return calendar;
+        return parseLedgerDate(dateString).toCalendar();
     }
-    public static Date parseIsoDate(String dateString) throws ParseException {
-        return isoDateFormatter.get().parse(dateString);
+    public static SimpleDate parseIsoDate(String dateString) throws ParseException {
+        return SimpleDate.fromDate(isoDateFormatter.get().parse(dateString));
     }
-    public static String formatLedgerDate(Date date) {
-        return dateFormatter.get().format(date);
+    public static String formatLedgerDate(SimpleDate date) {
+        return dateFormatter.get().format(date.toDate());
     }
-    public static String formatIsoDate(Date date) {
-        return isoDateFormatter.get().format(date);
+    public static String formatIsoDate(SimpleDate date) {
+        return isoDateFormatter.get().format(date.toDate());
     }
 }
\ No newline at end of file
index eeb6873..ffdeb9e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -34,7 +34,7 @@ import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class MobileLedgerDatabase extends SQLiteOpenHelper {
     private static final String DB_NAME = "MoLe.db";
-    private static final int LATEST_REVISION = 33;
+    private static final int LATEST_REVISION = 34;
     private static final String CREATE_DB_SQL = "create_db";
 
     private final Application mContext;
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java b/app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java
new file mode 100644 (file)
index 0000000..ff6c0e5
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Calendar;
+import java.util.Date;
+
+public class SimpleDate {
+    public int year;
+    public int month;
+    public int day;
+    public SimpleDate(int y, int m, int d) {
+        year = y;
+        month = m;
+        day = d;
+    }
+    public static SimpleDate fromDate(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        return fromCalendar(calendar);
+    }
+    public static SimpleDate fromCalendar(Calendar calendar) {
+        return new SimpleDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1,
+                calendar.get(Calendar.DATE));
+    }
+    public static SimpleDate today() {
+        return fromCalendar(Calendar.getInstance());
+    }
+    public Calendar toCalendar() {
+        Calendar result = Calendar.getInstance();
+        result.set(year, month - 1, day);
+        return result;
+    }
+    public Date toDate() {
+        return toCalendar().getTime();
+    }
+    public boolean equals(@Nullable SimpleDate date) {
+        if (date == null)
+            return false;
+
+        if (year != date.year)
+            return false;
+        if (month != date.month)
+            return false;
+        if (day != date.day)
+            return false;
+
+        return true;
+    }
+    public boolean earlierThan(@NonNull SimpleDate date) {
+        if (year < date.year)
+            return true;
+        if (year > date.year)
+            return false;
+        if (month < date.month)
+            return true;
+        if (month > date.month)
+            return false;
+        return (day < date.day);
+    }
+    public boolean laterThan(@NonNull SimpleDate date) {
+        if (year > date.year)
+            return true;
+        if (year < date.year)
+            return false;
+        if (month > date.month)
+            return true;
+        if (month < date.month)
+            return false;
+        return (day > date.day);
+    }
+}
index 175d657..da3e8d8 100644 (file)
@@ -1,3 +1,17 @@
+-- Copyright © 2020 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
 create table accounts(profile varchar not null, name varchar not null, name_upper varchar not null, hidden boolean not null default 0, keep boolean not null default 0, level integer not null, parent_name varchar, expanded default 1, amounts_expanded boolean default 0);
 create unique index un_accounts on accounts(profile, name);
 create table options(profile varchar not null, name varchar not null, value varchar);
@@ -6,10 +20,10 @@ create table account_values(profile varchar not null, account varchar not null,
 create unique index un_account_values on account_values(profile,account,currency);
 create table description_history(description varchar not null primary key, keep boolean, description_upper varchar);
 create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1);
-create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, date varchar not null, description varchar not null, comment varchar, keep boolean not null default 0);
+create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, keep boolean not null default 0);
 create unique index un_transactions_id on transactions(profile,id);
 create unique index un_transactions_data_hash on transactions(profile,data_hash);
 create index idx_transaction_description on transactions(description);
 create table transaction_accounts(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,account_name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id));
 create table currencies(id integer not null primary key, name varchar not null, position varchar not null, has_gap boolean not null);
--- updated to revision 33
\ No newline at end of file
+-- updated to revision 34
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_34.sql b/app/src/main/res/raw/sql_34.sql
new file mode 100644 (file)
index 0000000..e03541d
--- /dev/null
@@ -0,0 +1,27 @@
+-- Copyright © 2020 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+alter table transactions add year integer not null default 0;
+alter table transactions add month integer not null default 0;
+alter table transactions add day integer not null default 0;
+alter table transactions add tmp_md varchar;
+update transactions set year= cast(substr(date,  1,instr(date,  '/')-1) as integer);
+update transactions set tmp_md=    substr(date,    instr(date,  '/')+1);
+update transactions set month=cast(substr(tmp_md,1,instr(tmp_md,'/')-1) as integer);
+update transactions set day=  cast(substr(tmp_md,  instr(tmp_md,'/')+1) as integer);
+-- alter table transactions drop date
+create table transactions_2(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, keep boolean not null default 0);
+insert into transactions_2(profile, id, data_hash, year, month, day, description, comment, keep) select profile, id, data_hash, year, month, day, description, comment, keep from transactions;
+drop table transactions;
+alter table transactions_2 rename to transactions;