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