]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
rework new transaction activity with a RecyclerView
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionModel.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui.activity;
19
20 import androidx.annotation.NonNull;
21 import androidx.lifecycle.LiveData;
22 import androidx.lifecycle.MutableLiveData;
23 import androidx.lifecycle.ViewModel;
24
25 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
26
27 import org.jetbrains.annotations.NotNull;
28
29 import java.util.ArrayList;
30 import java.util.Calendar;
31 import java.util.Date;
32 import java.util.GregorianCalendar;
33 import java.util.Locale;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36
37 import static net.ktnx.mobileledger.utils.Logger.debug;
38 import static net.ktnx.mobileledger.utils.Misc.isZero;
39
40 public class NewTransactionModel extends ViewModel {
41     static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
42     static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
43     static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
44     private final Item header = new Item(this, null, "");
45     private final Item trailer = new Item(this);
46     private final ArrayList<Item> items = new ArrayList<>();
47     private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
48     private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(null);
49     private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
50     public int getAccountCount() {
51         return items.size();
52     }
53     public Date getDate() {
54         return header.date.getValue();
55     }
56     public String getDescription() {
57         return header.description.getValue();
58     }
59     public LiveData<Boolean> isSubmittable() {
60         return this.isSubmittable;
61     }
62     void reset() {
63         header.date.setValue(null);
64         header.description.setValue(null);
65         items.clear();
66         items.add(new Item(this, new LedgerTransactionAccount("")));
67         items.add(new Item(this, new LedgerTransactionAccount("")));
68         focusedItem.setValue(0);
69     }
70     public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
71                                    @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
72         this.focusedItem.observe(owner, observer);
73     }
74     public void stopObservingFocusedItem(
75             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
76         this.focusedItem.removeObserver(observer);
77     }
78     public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
79                                     @NonNull
80                                             androidx.lifecycle.Observer<? super Integer> observer) {
81         this.accountCount.observe(owner, observer);
82     }
83     public void stopObservingAccountCount(
84             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
85         this.accountCount.removeObserver(observer);
86     }
87     public void setFocusedItem(int position) {
88         focusedItem.setValue(position);
89     }
90     public int addAccount(LedgerTransactionAccount acc) {
91         items.add(new Item(this, acc));
92         accountCount.setValue(getAccountCount());
93         return items.size();
94     }
95     boolean accountsInInitialState() {
96         for (Item item : items) {
97             LedgerTransactionAccount acc = item.getAccount();
98             if (acc.isAmountSet()) return false;
99             if (!acc.getAccountName()
100                     .trim()
101                     .isEmpty()) return false;
102         }
103
104         return true;
105     }
106     LedgerTransactionAccount getAccount(int index) {
107         return items.get(index)
108                     .getAccount();
109     }
110     public Item getItem(int index) {
111         if (index == 0) {
112             return header;
113         }
114         else if (index <= items.size()) return items.get(index - 1);
115         else return trailer;
116     }
117     // rules:
118     // 1) at least two account names
119     // 2) each amount must have account name
120     // 3) amounts must balance to 0, or
121     // 3a) there must be exactly one empty amount
122     // 4) empty accounts with empty amounts are ignored
123     // 5) a row with an empty account name or empty amount is guaranteed to exist
124     public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
125         int accounts = 0;
126         int accounts_with_values = 0;
127         int amounts = 0;
128         int amounts_with_accounts = 0;
129         int empty_rows = 0;
130         Item empty_amount = null;
131         boolean single_empty_amount = false;
132         boolean single_empty_amount_has_account = false;
133         float running_total = 0f;
134         final String descriptionText = getDescription();
135         final boolean have_description = ((descriptionText != null) && !descriptionText.isEmpty());
136
137         try {
138             for (int i = 0; i < this.items.size(); i++) {
139                 Item item = this.items.get(i);
140
141                 LedgerTransactionAccount acc = item.getAccount();
142                 String acc_name = acc.getAccountName()
143                                      .trim();
144                 if (!acc_name.isEmpty()) {
145                     accounts++;
146
147                     if (acc.isAmountSet()) {
148                         accounts_with_values++;
149                     }
150                 }
151                 else empty_rows++;
152
153                 if (!acc.isAmountSet()) {
154                     if (empty_amount == null) {
155                         empty_amount = item;
156                         single_empty_amount = true;
157                         single_empty_amount_has_account = !acc_name.isEmpty();
158                     }
159                     else if (!acc_name.isEmpty()) single_empty_amount = false;
160                 }
161                 else {
162                     amounts++;
163                     if (!acc_name.isEmpty()) amounts_with_accounts++;
164                     running_total += acc.getAmount();
165                 }
166             }
167
168             if ((empty_rows == 0) &&
169                 ((this.items.size() == accounts) || (this.items.size() == amounts)))
170             {
171                 adapter.addRow();
172             }
173
174             if (single_empty_amount) {
175                 empty_amount.setAmountHint(String.format(Locale.US, "%1.2f",
176                         (Math.abs(running_total) > 0.005) ? -running_total : 0f));
177             }
178
179             debug("submittable", String.format(Locale.US,
180                     "%s, accounts=%d, accounts_with_values=%s, " +
181                     "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " +
182                     "single_empty_with_acc=%s", have_description ? "description" : "NO description",
183                     accounts, accounts_with_values, amounts_with_accounts, amounts, running_total,
184                     (single_empty_amount && single_empty_amount_has_account) ? "true" : "false"));
185
186             if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) &&
187                 (amounts_with_accounts == amounts) &&
188                 (single_empty_amount && single_empty_amount_has_account || isZero(running_total)))
189             {
190                 debug("submittable", "YES");
191                 isSubmittable.setValue(true);
192             }
193             else {
194                 debug("submittable", "NO");
195                 isSubmittable.setValue(false);
196             }
197
198         }
199         catch (NumberFormatException e) {
200             debug("submittable", "NO (because of NumberFormatException)");
201             isSubmittable.setValue(false);
202         }
203         catch (Exception e) {
204             e.printStackTrace();
205             debug("submittable", "NO (because of an Exception)");
206             isSubmittable.setValue(false);
207         }
208     }
209     public void removeItem(int pos, NewTransactionItemsAdapter adapter) {
210         items.remove(pos);
211         accountCount.setValue(getAccountCount());
212         checkTransactionSubmittable(adapter);
213     }
214     enum ItemType {generalData, transactionRow, bottomFiller}
215
216     class Item extends Object {
217         private ItemType type;
218         private MutableLiveData<Date> date = new MutableLiveData<>();
219         private MutableLiveData<String> description = new MutableLiveData<>();
220         private LedgerTransactionAccount account;
221         private MutableLiveData<String> amountHint = new MutableLiveData<>();
222         private NewTransactionModel model;
223         private boolean editable = true;
224         public Item(NewTransactionModel model) {
225             this.model = model;
226             type = ItemType.bottomFiller;
227         }
228         public Item(NewTransactionModel model, Date date, String description) {
229             this.model = model;
230             this.type = ItemType.generalData;
231             this.date.setValue(date);
232             this.description.setValue(description);
233         }
234         public Item(NewTransactionModel model, LedgerTransactionAccount account) {
235             this.model = model;
236             this.type = ItemType.transactionRow;
237             this.account = account;
238         }
239         public NewTransactionModel getModel() {
240             return model;
241         }
242         public boolean isEditable() {
243             ensureType(ItemType.transactionRow);
244             return editable;
245         }
246         public void setEditable(boolean editable) {
247             ensureType(ItemType.transactionRow);
248             this.editable = editable;
249         }
250         public String getAmountHint() {
251             ensureType(ItemType.transactionRow);
252             return amountHint.getValue();
253         }
254         public void setAmountHint(String amountHint) {
255             ensureType(ItemType.transactionRow);
256             this.amountHint.setValue(amountHint);
257         }
258         public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
259                                       @NonNull
260                                               androidx.lifecycle.Observer<? super String> observer) {
261             this.amountHint.observe(owner, observer);
262         }
263         public void stopObservingAmountHint(
264                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
265             this.amountHint.removeObserver(observer);
266         }
267         public ItemType getType() {
268             return type;
269         }
270         public void ensureType(ItemType wantedType) {
271             if (type != wantedType) {
272                 throw new RuntimeException(
273                         String.format("Actual type (%d) differs from wanted (%s)", type,
274                                 wantedType));
275             }
276         }
277         public Date getDate() {
278             ensureType(ItemType.generalData);
279             return date.getValue();
280         }
281         public void setDate(Date date) {
282             ensureType(ItemType.generalData);
283             this.date.setValue(date);
284         }
285         public void setDate(String text) {
286             int year, month, day;
287             final Calendar c = GregorianCalendar.getInstance();
288             Matcher m = reYMD.matcher(text);
289             if (m.matches()) {
290                 year = Integer.parseInt(m.group(1));
291                 month = Integer.parseInt(m.group(2)) - 1;   // month is 0-based
292                 day = Integer.parseInt(m.group(3));
293             }
294             else {
295                 year = c.get(Calendar.YEAR);
296                 m = reMD.matcher(text);
297                 if (m.matches()) {
298                     month = Integer.parseInt(m.group(1)) - 1;
299                     day = Integer.parseInt(m.group(2));
300                 }
301                 else {
302                     month = c.get(Calendar.MONTH);
303                     m = reD.matcher(text);
304                     if (m.matches()) {
305                         day = Integer.parseInt(m.group(1));
306                     }
307                     else {
308                         day = c.get(Calendar.DAY_OF_MONTH);
309                     }
310                 }
311             }
312
313             c.set(year, month, day);
314
315             this.setDate(c.getTime());
316         }
317         public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
318                                 @NonNull androidx.lifecycle.Observer<? super Date> observer) {
319             this.date.observe(owner, observer);
320         }
321         public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
322             this.date.removeObserver(observer);
323         }
324         public String getDescription() {
325             ensureType(ItemType.generalData);
326             return description.getValue();
327         }
328         public void setDescription(String description) {
329             ensureType(ItemType.generalData);
330             this.description.setValue(description);
331         }
332         public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
333                                        @NonNull
334                                                androidx.lifecycle.Observer<? super String> observer) {
335             this.description.observe(owner, observer);
336         }
337         public void stopObservingDescription(
338                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
339             this.description.removeObserver(observer);
340         }
341         public LedgerTransactionAccount getAccount() {
342             ensureType(ItemType.transactionRow);
343             return account;
344         }
345         public void setAccountName(String name) {
346             account.setAccountName(name);
347         }
348         /**
349          * getFormattedDate()
350          *
351          * @return nicely formatted, shortest available date representation
352          */
353         public String getFormattedDate() {
354             if (date == null) return null;
355             Date time = date.getValue();
356             if (time == null) return null;
357
358             Calendar c = GregorianCalendar.getInstance();
359             c.setTime(time);
360             Calendar today = GregorianCalendar.getInstance();
361
362             final int myYear = c.get(Calendar.YEAR);
363             final int myMonth = c.get(Calendar.MONTH);
364             final int myDay = c.get(Calendar.DAY_OF_MONTH);
365
366             if (today.get(Calendar.YEAR) != myYear) {
367                 return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth, myDay);
368             }
369
370             if (today.get(Calendar.MONTH) != myMonth) {
371                 return String.format(Locale.US, "%d/%02d", myMonth, myDay);
372             }
373
374             return String.valueOf(myDay);
375         }
376     }
377 }