]> git.ktnx.net Git - mobile-ledger.git/commitdiff
add support for filling new transaction data from matched pattern(s)
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 30 Jan 2021 21:08:36 +0000 (21:08 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 30 Jan 2021 21:08:36 +0000 (21:08 +0000)
still not ideal, but works in the simplest case

app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java
app/src/main/res/values/strings.xml

index 4932f9de12f2d31e4c9a3305f25ac1ee013db26e..9a2226f3acb5c776af9f2a99a4738bbd26b9871b 100644 (file)
 package net.ktnx.mobileledger.ui.new_transaction;
 
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Resources;
+import android.database.AbstractCursor;
 import android.os.Bundle;
+import android.os.ParcelFormatException;
 import android.renderscript.RSInvalidStateException;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -34,26 +37,36 @@ import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.BaseTransientBottomBar;
 import com.google.android.material.snackbar.Snackbar;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.PatternAccount;
+import net.ktnx.mobileledger.db.PatternHeader;
 import net.ktnx.mobileledger.json.API;
 import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.ui.QRScanAbleFragment;
+import net.ktnx.mobileledger.ui.patterns.PatternsActivity;
 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.ArrayList;
+import java.util.List;
+import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -76,55 +89,279 @@ public class NewTransactionFragment extends QRScanAbleFragment {
         // Required empty public constructor
         setHasOptionsMenu(true);
     }
+    private void startNewPatternActivity(String scanned) {
+        Intent intent = new Intent(requireContext(), PatternsActivity.class);
+        Bundle args = new Bundle();
+        args.putString(PatternsActivity.ARG_ADD_PATTERN, scanned);
+        requireContext().startActivity(intent, args);
+    }
+    private void alertNoPatternMatch(String scanned) {
+        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
+        builder.setCancelable(true)
+               .setMessage(R.string.no_pattern_matches)
+               .setPositiveButton(R.string.add_button,
+                       (dialog, which) -> startNewPatternActivity(scanned))
+               .create()
+               .show();
+    }
     protected void onQrScanned(String text) {
         Logger.debug("qr", String.format("Got QR scan result [%s]", text));
-        Pattern p =
-                Pattern.compile("^(\\d+)\\*(\\d+)\\*(\\d+)-(\\d+)-(\\d+)\\*([:\\d]+)\\*([\\d.]+)$");
-        Matcher m = p.matcher(text);
-        if (m.matches()) {
-            float amount = Float.parseFloat(m.group(7));
-            viewModel.setDate(
-                    new SimpleDate(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
-                            Integer.parseInt(m.group(5))));
-
-            if (viewModel.accountsInInitialState()) {
-                {
-                    NewTransactionModel.Item firstItem = viewModel.getItem(1);
-                    if (firstItem == null) {
-                        viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
-                        listAdapter.notifyItemInserted(viewModel.items.size() - 1);
-                    }
-                    else {
-                        firstItem.setAccountName("разход:пазар");
-                        firstItem.getAccount()
-                                 .resetAmount();
-                        listAdapter.notifyItemChanged(1);
-                    }
+
+        if (Misc.emptyIsNull(text) == null)
+            return;
+
+        LiveData<List<PatternHeader>> allPatterns = DB.get()
+                                                      .getPatternDAO()
+                                                      .getPatterns();
+        allPatterns.observe(getViewLifecycleOwner(), patternHeaders -> {
+            ArrayList<PatternHeader> matchingPatterns = new ArrayList<>();
+
+            for (PatternHeader ph : patternHeaders) {
+                String patternSource = ph.getRegularExpression();
+                if (Misc.emptyIsNull(patternSource) == null)
+                    continue;
+                try {
+                    Pattern pattern = Pattern.compile(patternSource);
+                    Matcher matcher = pattern.matcher(text);
+                    if (!matcher.matches())
+                        continue;
+
+                    Logger.debug("pattern",
+                            String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
+                                    patternSource, text));
+                    matchingPatterns.add(ph);
                 }
-                {
-                    NewTransactionModel.Item secondItem = viewModel.getItem(2);
-                    if (secondItem == null) {
-                        viewModel.addAccount(
-                                new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
-                        listAdapter.notifyItemInserted(viewModel.items.size() - 1);
-                    }
-                    else {
-                        secondItem.setAccountName("актив:кеш:дам");
-                        secondItem.getAccount()
-                                  .setAmount(-amount);
-                        listAdapter.notifyItemChanged(2);
-                    }
+                catch (ParcelFormatException e) {
+                    // ignored
+                    Logger.debug("pattern",
+                            String.format("Error compiling regular expression '%s'", patternSource),
+                            e);
                 }
             }
-            else {
-                viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
-                viewModel.addAccount(
-                        new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
-                listAdapter.notifyItemRangeInserted(viewModel.items.size() - 1, 2);
+
+            if (matchingPatterns.isEmpty())
+                alertNoPatternMatch(text);
+            else if (matchingPatterns.size() == 1)
+                applyPattern(matchingPatterns.get(0), text);
+            else
+                choosePattern(matchingPatterns, text);
+        });
+    }
+    private void choosePattern(ArrayList<PatternHeader> matchingPatterns, String matchedText) {
+        final String patternNameColumn = getString(R.string.pattern_name);
+        AbstractCursor cursor = new AbstractCursor() {
+            @Override
+            public int getCount() {
+                return matchingPatterns.size();
+            }
+            @Override
+            public String[] getColumnNames() {
+                return new String[]{patternNameColumn};
+            }
+            @Override
+            public String getString(int column) {
+                return matchingPatterns.get(getPosition())
+                                       .getName();
+            }
+            @Override
+            public short getShort(int column) {
+                return -1;
+            }
+            @Override
+            public int getInt(int column) {
+                return -1;
+            }
+            @Override
+            public long getLong(int column) {
+                return -1;
+            }
+            @Override
+            public float getFloat(int column) {
+                return -1;
             }
+            @Override
+            public double getDouble(int column) {
+                return -1;
+            }
+            @Override
+            public boolean isNull(int column) {
+                return false;
+            }
+        };
+
+        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
+        builder.setCancelable(true)
+               .setTitle(R.string.choose_pattern_to_apply)
+               .setSingleChoiceItems(cursor, -1, patternNameColumn,
+                       (dialog, which) -> applyPattern(matchingPatterns.get(which), matchedText))
+               .create()
+               .show();
+    }
+    private void applyPattern(PatternHeader patternHeader, String text) {
+        Pattern pattern = Pattern.compile(patternHeader.getRegularExpression());
+
+        Matcher m = pattern.matcher(text);
+
+        if (!m.matches()) {
+            Snackbar.make(requireView(), R.string.pattern_does_not_match,
+                    BaseTransientBottomBar.LENGTH_INDEFINITE)
+                    .show();
+            return;
+        }
+
+        SimpleDate transactionDate;
+        {
+            int day = extractIntFromMatches(m, patternHeader.getDateDayMatchGroup(),
+                    patternHeader.getDateDay());
+            int month = extractIntFromMatches(m, patternHeader.getDateMonthMatchGroup(),
+                    patternHeader.getDateMonth());
+            int year = extractIntFromMatches(m, patternHeader.getDateYearMatchGroup(),
+                    patternHeader.getDateYear());
+
+            SimpleDate today = SimpleDate.today();
+            if (year <= 0)
+                year = today.year;
+            if (month <= 0)
+                month = today.month;
+            if (day <= 0)
+                day = today.day;
+
+            transactionDate = new SimpleDate(year, month, day);
+
+            Logger.debug("pattern", "setting transaction date to " + transactionDate);
+        }
+
+        NewTransactionModel.Item head = viewModel.getItem(0);
+        head.ensureType(NewTransactionModel.ItemType.generalData);
+        final String transactionDescription =
+                extractStringFromMatches(m, patternHeader.getTransactionDescriptionMatchGroup(),
+                        patternHeader.getTransactionDescription());
+        head.setDescription(transactionDescription);
+        Logger.debug("pattern", "Setting transaction description to " + transactionDescription);
+        final String transactionComment =
+                extractStringFromMatches(m, patternHeader.getTransactionCommentMatchGroup(),
+                        patternHeader.getTransactionComment());
+        head.setTransactionComment(transactionComment);
+        Logger.debug("pattern", "Setting transaction comment to " + transactionComment);
+        head.setDate(transactionDate);
+        listAdapter.notifyItemChanged(0);
+
+        DB.get()
+          .getPatternDAO()
+          .getPatternWithAccounts(patternHeader.getId())
+          .observe(getViewLifecycleOwner(), entry -> {
+              int rowIndex = 0;
+              final boolean accountsInInitialState = viewModel.accountsInInitialState();
+              for (PatternAccount acc : entry.accounts) {
+                  rowIndex++;
+
+                  String accountName = extractStringFromMatches(m, acc.getAccountNameMatchGroup(),
+                          acc.getAccountName());
+                  String accountComment =
+                          extractStringFromMatches(m, acc.getAccountCommentMatchGroup(),
+                                  acc.getAccountComment());
+                  Float amount =
+                          extractFloatFromMatches(m, acc.getAmountMatchGroup(), acc.getAmount());
+
+                  if (accountsInInitialState) {
+                      NewTransactionModel.Item item = viewModel.getItem(rowIndex);
+                      if (item == null) {
+                          Logger.debug("pattern", String.format(Locale.US,
+                                  "Adding new account item [%s][c:%s][a:%s]", accountName,
+                                  accountComment, amount));
+                          final LedgerTransactionAccount ledgerAccount =
+                                  new LedgerTransactionAccount(accountName);
+                          ledgerAccount.setComment(accountComment);
+                          if (amount != null)
+                              ledgerAccount.setAmount(amount);
+                          // TODO currency
+                          viewModel.addAccount(ledgerAccount);
+                          listAdapter.notifyItemInserted(viewModel.items.size() - 1);
+                      }
+                      else {
+                          Logger.debug("pattern", String.format(Locale.US,
+                                  "Stamping account item #%d [%s][c:%s][a:%s]", rowIndex,
+                                  accountName, accountComment, amount));
+
+                          item.setAccountName(accountName);
+                          item.setComment(accountComment);
+                          if (amount != null)
+                              item.getAccount()
+                                  .setAmount(amount);
+
+                          listAdapter.notifyItemChanged(rowIndex);
+                      }
+                  }
+                  else {
+                      final LedgerTransactionAccount transactionAccount =
+                              new LedgerTransactionAccount(accountName);
+                      transactionAccount.setComment(accountComment);
+                      if (amount != null)
+                          transactionAccount.setAmount(amount);
+                      // TODO currency
+                      Logger.debug("pattern", String.format(Locale.US,
+                              "Adding trailing account item [%s][c:%s][a:%s]", accountName,
+                              accountComment, amount));
+
+                      viewModel.addAccount(transactionAccount);
+                      listAdapter.notifyItemInserted(viewModel.items.size() - 1);
+                  }
+              }
+
+              listAdapter.checkTransactionSubmittable();
+          });
+    }
+    private int extractIntFromMatches(Matcher m, Integer group, Integer literal) {
+        if (literal != null)
+            return literal;
+
+        if (group != null) {
+            int grp = group;
+            if (grp > 0 & grp <= m.groupCount())
+                try {
+                    return Integer.parseInt(m.group(grp));
+                }
+                catch (NumberFormatException e) {
+                    Snackbar.make(requireView(),
+                            "Error extracting transaction date: " + e.getMessage(),
+                            BaseTransientBottomBar.LENGTH_INDEFINITE)
+                            .show();
+                }
+        }
 
-            listAdapter.checkTransactionSubmittable();
+        return 0;
+    }
+    private String extractStringFromMatches(Matcher m, Integer group, String literal) {
+        if (literal != null)
+            return literal;
+
+        if (group != null) {
+            int grp = group;
+            if (grp > 0 & grp <= m.groupCount())
+                return m.group(grp);
         }
+
+        return null;
+    }
+    private Float extractFloatFromMatches(Matcher m, Integer group, Float literal) {
+        if (literal != null)
+            return literal;
+
+        if (group != null) {
+            int grp = group;
+            if (grp > 0 & grp <= m.groupCount())
+                try {
+                    return Float.valueOf(m.group(grp));
+                }
+                catch (NumberFormatException e) {
+                    Snackbar.make(requireView(),
+                            "Error extracting transaction amount: " + e.getMessage(),
+                            BaseTransientBottomBar.LENGTH_INDEFINITE)
+                            .show();
+                }
+        }
+
+        return null;
     }
     @Override
     public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
index ad8c23642ff43afd4715621e5e01c6d8b8a70132..c787769481cd6078759e899484f4d83236986890 100644 (file)
@@ -38,6 +38,7 @@ import java.util.Objects;
 
 public class PatternsActivity extends CrashReportingActivity
         implements PatternListFragment.OnPatternListFragmentInteractionListener {
+    public static final String ARG_ADD_PATTERN = "add-pattern";
     private ActivityPatternsBinding b;
     private NavController navController;
     @Override
index 69ef1d8310b478a9534e94d08536327b7e36c91c..5682e8a4f13eb3de6d04a44bd5fc6b208ad54872 100644 (file)
     <string name="unnamed_pattern">Pattern with no name</string>
     <string name="add_button_description">Add pattern</string>
     <string name="save_button_description">Save pattern</string>
+    <string name="no_pattern_matches">No pattern matches</string>
+    <string name="choose_pattern_to_apply">Choose pattern to apply</string>
+    <string name="pattern_name">Pattern name</string>
+    <string name="implementation_pending">Not implemented yet</string>
 </resources>