]> git.ktnx.net Git - mobile-ledger.git/blobdiff - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
whitespace
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionModel.java
index f6ecb443f49fe775413d50b9df536fe0dedd352f..a2f356c68e6b44c42d7d1286c69812047648042f 100644 (file)
 
 package net.ktnx.mobileledger.ui.activity;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModel;
 
+import net.ktnx.mobileledger.BuildConfig;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.Misc;
 
 import org.jetbrains.annotations.NotNull;
@@ -31,12 +36,12 @@ import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.GregorianCalendar;
+import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
-import static net.ktnx.mobileledger.utils.Misc.isZero;
 
 public class NewTransactionModel extends ViewModel {
     static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
@@ -46,7 +51,7 @@ public class NewTransactionModel extends ViewModel {
     private final Item trailer = new Item(this);
     private final ArrayList<Item> items = new ArrayList<>();
     private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
-    private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(null);
+    private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
     private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
     public int getAccountCount() {
         return items.size();
@@ -85,6 +90,7 @@ public class NewTransactionModel extends ViewModel {
             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
         this.accountCount.removeObserver(observer);
     }
+    public int getFocusedItem() { return focusedItem.getValue(); }
     public void setFocusedItem(int position) {
         focusedItem.setValue(position);
     }
@@ -96,10 +102,12 @@ public class NewTransactionModel extends ViewModel {
     boolean accountsInInitialState() {
         for (Item item : items) {
             LedgerTransactionAccount acc = item.getAccount();
-            if (acc.isAmountSet()) return false;
+            if (acc.isAmountSet())
+                return false;
             if (!acc.getAccountName()
                     .trim()
-                    .isEmpty()) return false;
+                    .isEmpty())
+                return false;
         }
 
         return true;
@@ -112,90 +120,139 @@ public class NewTransactionModel extends ViewModel {
         if (index == 0) {
             return header;
         }
-        else if (index <= items.size()) return items.get(index - 1);
-        else return trailer;
+
+        if (index <= items.size())
+            return items.get(index - 1);
+
+        return trailer;
     }
-    // rules:
-    // 1) at least two account names
-    // 2) each amount must have account name
-    // 3) amounts must balance to 0, or
-    // 3a) there must be exactly one empty amount
-    // 4) empty accounts with empty amounts are ignored
-    // 5) a row with an empty account name or empty amount is guaranteed to exist
+    /*
+     A transaction is submittable if:
+     0) has description
+     1) has at least two account names
+     2) each amount has account name
+     3) amounts must balance to 0, or
+     3a) there must be exactly one empty amount (with account)
+     4) empty accounts with empty amounts are ignored
+     5) a row with an empty account name or empty amount is guaranteed to exist
+    */
+    @SuppressLint("DefaultLocale")
     public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
         int accounts = 0;
-        int accounts_with_values = 0;
         int amounts = 0;
-        int amounts_with_accounts = 0;
         int empty_rows = 0;
-        Item empty_amount = null;
-        boolean single_empty_amount = false;
-        boolean single_empty_amount_has_account = false;
-        float running_total = 0f;
+        float balance = 0f;
         final String descriptionText = getDescription();
-        final boolean have_description = ((descriptionText != null) && !descriptionText.isEmpty());
+        boolean submittable = true;
+        List<Item> itemsWithEmptyAmount = new ArrayList<>();
+        List<Item> itemsWithAccountAndEmptyAmount = new ArrayList<>();
 
         try {
+            if ((descriptionText == null) || descriptionText.trim()
+                                                            .isEmpty())
+            {
+                Logger.debug("submittable", "Transaction not submittable: missing description");
+                submittable = false;
+            }
+
             for (int i = 0; i < this.items.size(); i++) {
                 Item item = this.items.get(i);
 
                 LedgerTransactionAccount acc = item.getAccount();
                 String acc_name = acc.getAccountName()
                                      .trim();
-                if (!acc_name.isEmpty()) {
-                    accounts++;
+                if (acc_name.isEmpty()) {
+                    empty_rows++;
 
                     if (acc.isAmountSet()) {
-                        accounts_with_values++;
+                        // 2) each amount has account name
+                        Logger.debug("submittable", String.format(
+                                "Transaction not submittable: row %d has no account name, but has" +
+                                " amount %1.2f", i + 1, acc.getAmount()));
+                        submittable = false;
                     }
                 }
-                else empty_rows++;
+                else {
+                    accounts++;
+                }
 
-                if (!acc.isAmountSet()) {
-                    if (empty_amount == null) {
-                        empty_amount = item;
-                        single_empty_amount = true;
-                        single_empty_amount_has_account = !acc_name.isEmpty();
-                    }
-                    else if (!acc_name.isEmpty()) single_empty_amount = false;
+                if (acc.isAmountSet()) {
+                    amounts++;
+                    balance += acc.getAmount();
                 }
                 else {
-                    amounts++;
-                    if (!acc_name.isEmpty()) amounts_with_accounts++;
-                    running_total += acc.getAmount();
+                    itemsWithEmptyAmount.add(item);
+
+                    if (!acc_name.isEmpty()) {
+                        itemsWithAccountAndEmptyAmount.add(item);
+                    }
+                }
+            }
+
+            // 1) has at least two account names
+            if (accounts < 2) {
+                Logger.debug("submittable",
+                        String.format("Transaction not submittable: only %d account names",
+                                accounts));
+                submittable = false;
+            }
+
+            // 3) amount must balance to 0, or
+            // 3a) there must be exactly one empty amount (with account)
+            if (Misc.isZero(balance)) {
+                for (Item item : items) {
+                    item.setAmountHint(null);
+                }
+            }
+            else {
+                int balanceReceiversCount = itemsWithAccountAndEmptyAmount.size();
+                if (balanceReceiversCount != 1) {
+                    Logger.debug("submittable", (balanceReceiversCount == 0) ?
+                                                "Transaction not submittable: non-zero balance " +
+                                                "with no empty amounts with accounts" :
+                                                "Transaction not submittable: non-zero balance " +
+                                                "with multiple empty amounts with accounts");
+                    submittable = false;
+                }
+
+                // suggest off-balance amount to a row and remove hints on other rows
+                Item receiver = null;
+                if (!itemsWithAccountAndEmptyAmount.isEmpty())
+                    receiver = itemsWithAccountAndEmptyAmount.get(0);
+                else if (!itemsWithEmptyAmount.isEmpty())
+                    receiver = itemsWithEmptyAmount.get(0);
+
+                for (Item item : items) {
+                    if (item.equals(receiver)) {
+                        Logger.debug("submittable",
+                                String.format("Setting amount hint to %1.2f", -balance));
+                        item.setAmountHint(String.format("%1.2f", -balance));
+                    }
+                    else
+                        item.setAmountHint(null);
                 }
             }
 
+            // 5) a row with an empty account name or empty amount is guaranteed to exist
             if ((empty_rows == 0) &&
                 ((this.items.size() == accounts) || (this.items.size() == amounts)))
             {
                 adapter.addRow();
             }
 
-            if (single_empty_amount) {
-                empty_amount.setAmountHint(String.format(Locale.US, "%1.2f",
-                        Misc.isZero(running_total) ? 0f : -running_total));
-            }
 
-            debug("submittable", String.format(Locale.US,
-                    "%s, accounts=%d, accounts_with_values=%s, " +
-                    "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " +
-                    "single_empty_with_acc=%s", have_description ? "description" : "NO description",
-                    accounts, accounts_with_values, amounts_with_accounts, amounts, running_total,
-                    (single_empty_amount && single_empty_amount_has_account) ? "true" : "false"));
+            debug("submittable", submittable ? "YES" : "NO");
+            isSubmittable.setValue(submittable);
 
-            if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) &&
-                (amounts_with_accounts == amounts) &&
-                (single_empty_amount && single_empty_amount_has_account || isZero(running_total)))
-            {
-                debug("submittable", "YES");
-                isSubmittable.setValue(true);
-            }
-            else {
-                debug("submittable", "NO");
-                isSubmittable.setValue(false);
+            if (BuildConfig.DEBUG) {
+                debug("submittable", "== Dump of all items");
+                for (int i = 0; i < items.size(); i++) {
+                    Item item = items.get(i);
+                    LedgerTransactionAccount acc = item.getAccount();
+                    debug("submittable", String.format("Item %2d: [%4.2f] %s", i,
+                            acc.isAmountSet() ? acc.getAmount() : 0, acc.getAccountName()));
+                }
             }
-
         }
         catch (NumberFormatException e) {
             debug("submittable", "NO (because of NumberFormatException)");
@@ -214,41 +271,65 @@ public class NewTransactionModel extends ViewModel {
     public void sendCountNotifications() {
         accountCount.setValue(getAccountCount());
     }
+    public void sendFocusedNotification() {
+        focusedItem.setValue(focusedItem.getValue());
+    }
+    public void updateFocusedItem(int position) {
+        focusedItem.setValue(position);
+    }
+    public void noteFocusIsOnAccount(int position) {
+        getItem(position).setFocusIsOnAmount(false);
+    }
+    public void noteFocusIsOnAmount(int position) {
+        getItem(position).setFocusIsOnAmount(true);
+    }
     enum ItemType {generalData, transactionRow, bottomFiller}
 
+    //==========================================================================================
+
     class Item extends Object {
         private ItemType type;
         private MutableLiveData<Date> date = new MutableLiveData<>();
         private MutableLiveData<String> description = new MutableLiveData<>();
         private LedgerTransactionAccount account;
-        private MutableLiveData<String> amountHint = new MutableLiveData<>();
+        private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
         private NewTransactionModel model;
-        private boolean editable = true;
+        private MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
+        private boolean focusIsOnAmount = false;
         public Item(NewTransactionModel model) {
             this.model = model;
             type = ItemType.bottomFiller;
+            editable.setValue(false);
         }
         public Item(NewTransactionModel model, Date date, String description) {
             this.model = model;
             this.type = ItemType.generalData;
             this.date.setValue(date);
             this.description.setValue(description);
+            this.editable.setValue(true);
         }
         public Item(NewTransactionModel model, LedgerTransactionAccount account) {
             this.model = model;
             this.type = ItemType.transactionRow;
             this.account = account;
+            this.editable.setValue(true);
+        }
+        public boolean focusIsOnAmount() {
+            return focusIsOnAmount;
         }
         public NewTransactionModel getModel() {
             return model;
         }
-        public boolean isEditable() {
-            ensureType(ItemType.transactionRow);
-            return editable;
-        }
         public void setEditable(boolean editable) {
-            ensureType(ItemType.transactionRow);
-            this.editable = editable;
+            ensureType(ItemType.generalData, ItemType.transactionRow);
+            this.editable.setValue(editable);
+        }
+        private void ensureType(ItemType type1, ItemType type2) {
+            if ((type != type1) && (type != type2)) {
+                throw new RuntimeException(
+                        String.format("Actual type (%s) differs from wanted (%s or %s)", type,
+                                type1, type2));
+            }
         }
         public String getAmountHint() {
             ensureType(ItemType.transactionRow);
@@ -256,6 +337,17 @@ public class NewTransactionModel extends ViewModel {
         }
         public void setAmountHint(String amountHint) {
             ensureType(ItemType.transactionRow);
+
+            // avoid unnecessary triggers
+            if (amountHint == null) {
+                if (this.amountHint.getValue() == null)
+                    return;
+            }
+            else {
+                if (amountHint.equals(this.amountHint.getValue()))
+                    return;
+            }
+
             this.amountHint.setValue(amountHint);
         }
         public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
@@ -273,7 +365,7 @@ public class NewTransactionModel extends ViewModel {
         public void ensureType(ItemType wantedType) {
             if (type != wantedType) {
                 throw new RuntimeException(
-                        String.format("Actual type (%d) differs from wanted (%s)", type,
+                        String.format("Actual type (%s) differs from wanted (%s)", type,
                                 wantedType));
             }
         }
@@ -354,9 +446,11 @@ public class NewTransactionModel extends ViewModel {
          * @return nicely formatted, shortest available date representation
          */
         public String getFormattedDate() {
-            if (date == null) return null;
+            if (date == null)
+                return null;
             Date time = date.getValue();
-            if (time == null) return null;
+            if (time == null)
+                return null;
 
             Calendar c = GregorianCalendar.getInstance();
             c.setTime(time);
@@ -367,14 +461,24 @@ public class NewTransactionModel extends ViewModel {
             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, myDay);
+                return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay);
             }
 
             if (today.get(Calendar.MONTH) != myMonth) {
-                return String.format(Locale.US, "%d/%02d", myMonth, myDay);
+                return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay);
             }
 
             return String.valueOf(myDay);
         }
+        public void observeEditableFlag(NewTransactionActivity activity,
+                                        Observer<Boolean> observer) {
+            editable.observe(activity, observer);
+        }
+        public void stopObservingEditableFlag(Observer<Boolean> observer) {
+            editable.removeObserver(observer);
+        }
+        public void setFocusIsOnAmount(boolean flag) {
+            focusIsOnAmount = flag;
+        }
     }
 }