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.
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.
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/>.
18 package net.ktnx.mobileledger.ui.activity;
20 import android.annotation.SuppressLint;
22 import androidx.annotation.NonNull;
23 import androidx.lifecycle.LiveData;
24 import androidx.lifecycle.MutableLiveData;
25 import androidx.lifecycle.Observer;
26 import androidx.lifecycle.ViewModel;
28 import net.ktnx.mobileledger.BuildConfig;
29 import net.ktnx.mobileledger.model.Currency;
30 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
31 import net.ktnx.mobileledger.utils.Logger;
32 import net.ktnx.mobileledger.utils.Misc;
34 import org.jetbrains.annotations.NotNull;
36 import java.util.ArrayList;
37 import java.util.Calendar;
38 import java.util.Collections;
39 import java.util.Date;
40 import java.util.GregorianCalendar;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
46 import static net.ktnx.mobileledger.utils.Logger.debug;
48 public class NewTransactionModel extends ViewModel {
49 static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
50 static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
51 static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
52 private final Item header = new Item(this, null, "");
53 private final Item trailer = new Item(this);
54 private final ArrayList<Item> items = new ArrayList<>();
55 private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
56 private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
57 private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
58 private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
59 final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
60 public boolean getSimulateSave() {
61 return simulateSave.getValue();
63 public void setSimulateSave(boolean simulateSave) {
64 this.simulateSave.setValue(simulateSave);
66 public void toggleSimulateSave() {
67 simulateSave.setValue(!simulateSave.getValue());
69 public void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
71 androidx.lifecycle.Observer<? super Boolean> observer) {
72 this.simulateSave.observe(owner, observer);
74 public int getAccountCount() {
77 public Date getDate() {
78 return header.date.getValue();
80 public String getDescription() {
81 return header.description.getValue();
83 public LiveData<Boolean> isSubmittable() {
84 return this.isSubmittable;
87 header.date.setValue(null);
88 header.description.setValue(null);
90 items.add(new Item(this, new LedgerTransactionAccount("")));
91 items.add(new Item(this, new LedgerTransactionAccount("")));
92 focusedItem.setValue(0);
94 public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
95 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
96 this.focusedItem.observe(owner, observer);
98 public void stopObservingFocusedItem(
99 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
100 this.focusedItem.removeObserver(observer);
102 public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
104 androidx.lifecycle.Observer<? super Integer> observer) {
105 this.accountCount.observe(owner, observer);
107 public void stopObservingAccountCount(
108 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
109 this.accountCount.removeObserver(observer);
111 public int getFocusedItem() { return focusedItem.getValue(); }
112 public void setFocusedItem(int position) {
113 focusedItem.setValue(position);
115 public int addAccount(LedgerTransactionAccount acc) {
116 items.add(new Item(this, acc));
117 accountCount.setValue(getAccountCount());
120 boolean accountsInInitialState() {
121 for (Item item : items) {
122 LedgerTransactionAccount acc = item.getAccount();
123 if (acc.isAmountSet())
125 if (!acc.getAccountName()
133 LedgerTransactionAccount getAccount(int index) {
134 return items.get(index)
137 public Item getItem(int index) {
142 if (index <= items.size())
143 return items.get(index - 1);
148 A transaction is submittable if:
150 1) has at least two account names
151 2) each amount has account name
152 3) amounts must balance to 0, or
153 3a) there must be exactly one empty amount (with account)
154 4) empty accounts with empty amounts are ignored
155 5) a row with an empty account name or empty amount is guaranteed to exist
157 @SuppressLint("DefaultLocale")
158 public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
163 final String descriptionText = getDescription();
164 boolean submittable = true;
165 List<Item> itemsWithEmptyAmount = new ArrayList<>();
166 List<Item> itemsWithAccountAndEmptyAmount = new ArrayList<>();
169 if ((descriptionText == null) || descriptionText.trim()
172 Logger.debug("submittable", "Transaction not submittable: missing description");
176 for (int i = 0; i < this.items.size(); i++) {
177 Item item = this.items.get(i);
179 LedgerTransactionAccount acc = item.getAccount();
180 String acc_name = acc.getAccountName()
182 if (acc_name.isEmpty()) {
185 if (acc.isAmountSet()) {
186 // 2) each amount has account name
187 Logger.debug("submittable", String.format(
188 "Transaction not submittable: row %d has no account name, but has" +
189 " amount %1.2f", i + 1, acc.getAmount()));
197 if (acc.isAmountSet()) {
199 balance += acc.getAmount();
202 itemsWithEmptyAmount.add(item);
204 if (!acc_name.isEmpty()) {
205 itemsWithAccountAndEmptyAmount.add(item);
210 // 1) has at least two account names
212 Logger.debug("submittable",
213 String.format("Transaction not submittable: only %d account names",
218 // 3) amount must balance to 0, or
219 // 3a) there must be exactly one empty amount (with account)
220 if (Misc.isZero(balance)) {
221 for (Item item : items) {
222 item.setAmountHint(null);
226 int balanceReceiversCount = itemsWithAccountAndEmptyAmount.size();
227 if (balanceReceiversCount != 1) {
228 Logger.debug("submittable", (balanceReceiversCount == 0) ?
229 "Transaction not submittable: non-zero balance " +
230 "with no empty amounts with accounts" :
231 "Transaction not submittable: non-zero balance " +
232 "with multiple empty amounts with accounts");
236 // suggest off-balance amount to a row and remove hints on other rows
237 Item receiver = null;
238 if (!itemsWithAccountAndEmptyAmount.isEmpty())
239 receiver = itemsWithAccountAndEmptyAmount.get(0);
240 else if (!itemsWithEmptyAmount.isEmpty())
241 receiver = itemsWithEmptyAmount.get(0);
243 for (Item item : items) {
244 if (item.equals(receiver)) {
245 Logger.debug("submittable",
246 String.format("Setting amount hint to %1.2f", -balance));
247 item.setAmountHint(String.format("%1.2f", -balance));
250 item.setAmountHint(null);
254 // 5) a row with an empty account name or empty amount is guaranteed to exist
255 if ((empty_rows == 0) &&
256 ((this.items.size() == accounts) || (this.items.size() == amounts)))
262 debug("submittable", submittable ? "YES" : "NO");
263 isSubmittable.setValue(submittable);
265 if (BuildConfig.DEBUG) {
266 debug("submittable", "== Dump of all items");
267 for (int i = 0; i < items.size(); i++) {
268 Item item = items.get(i);
269 LedgerTransactionAccount acc = item.getAccount();
270 debug("submittable", String.format("Item %2d: [%4.2f] %s (%s)", i,
271 acc.isAmountSet() ? acc.getAmount() : 0, acc.getAccountName(),
276 catch (NumberFormatException e) {
277 debug("submittable", "NO (because of NumberFormatException)");
278 isSubmittable.setValue(false);
280 catch (Exception e) {
282 debug("submittable", "NO (because of an Exception)");
283 isSubmittable.setValue(false);
286 public void removeItem(int pos) {
288 accountCount.setValue(getAccountCount());
290 public void sendCountNotifications() {
291 accountCount.setValue(getAccountCount());
293 public void sendFocusedNotification() {
294 focusedItem.setValue(focusedItem.getValue());
296 public void updateFocusedItem(int position) {
297 focusedItem.setValue(position);
299 public void noteFocusChanged(int position, FocusedElement element) {
300 getItem(position).setFocusedElement(element);
302 public void swapItems(int one, int two) {
303 Collections.swap(items, one - 1, two - 1);
305 public void toggleComment(int position) {
306 final MutableLiveData<Boolean> commentVisible = getItem(position).commentVisible;
307 commentVisible.postValue(!commentVisible.getValue());
309 public void moveItemLast(int index) {
313 3 <-- desired position
315 int itemCount = items.size();
317 if (index < itemCount - 1) {
318 Item acc = items.remove(index);
319 items.add(itemCount - 1, acc);
322 public void toggleCurrencyVisible() {
323 showCurrency.setValue(!showCurrency.getValue());
325 enum ItemType {generalData, transactionRow, bottomFiller}
327 //==========================================================================================
329 enum FocusedElement {Account, Comment, Amount}
332 private ItemType type;
333 private MutableLiveData<Date> date = new MutableLiveData<>();
334 private MutableLiveData<String> description = new MutableLiveData<>();
335 private LedgerTransactionAccount account;
336 private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
337 private NewTransactionModel model;
338 private MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
339 private FocusedElement focusedElement = FocusedElement.Account;
340 private MutableLiveData<String> comment = new MutableLiveData<>(null);
341 private MutableLiveData<Boolean> commentVisible = new MutableLiveData<>(false);
342 private MutableLiveData<Currency> currency = new MutableLiveData<>(null);
343 public Item(NewTransactionModel model) {
345 type = ItemType.bottomFiller;
346 editable.setValue(false);
348 public Item(NewTransactionModel model, Date date, String description) {
350 this.type = ItemType.generalData;
351 this.date.setValue(date);
352 this.description.setValue(description);
353 this.editable.setValue(true);
355 public Item(NewTransactionModel model, LedgerTransactionAccount account) {
357 this.type = ItemType.transactionRow;
358 this.account = account;
359 this.editable.setValue(true);
361 public FocusedElement getFocusedElement() {
362 return focusedElement;
364 public void setFocusedElement(FocusedElement focusedElement) {
365 this.focusedElement = focusedElement;
367 public NewTransactionModel getModel() {
370 public void setEditable(boolean editable) {
371 ensureType(ItemType.generalData, ItemType.transactionRow);
372 this.editable.setValue(editable);
374 private void ensureType(ItemType type1, ItemType type2) {
375 if ((type != type1) && (type != type2)) {
376 throw new RuntimeException(
377 String.format("Actual type (%s) differs from wanted (%s or %s)", type,
381 public String getAmountHint() {
382 ensureType(ItemType.transactionRow);
383 return amountHint.getValue();
385 public void setAmountHint(String amountHint) {
386 ensureType(ItemType.transactionRow);
388 // avoid unnecessary triggers
389 if (amountHint == null) {
390 if (this.amountHint.getValue() == null)
394 if (amountHint.equals(this.amountHint.getValue()))
398 this.amountHint.setValue(amountHint);
400 public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
402 androidx.lifecycle.Observer<? super String> observer) {
403 this.amountHint.observe(owner, observer);
405 public void stopObservingAmountHint(
406 @NonNull androidx.lifecycle.Observer<? super String> observer) {
407 this.amountHint.removeObserver(observer);
409 public ItemType getType() {
412 public void ensureType(ItemType wantedType) {
413 if (type != wantedType) {
414 throw new RuntimeException(
415 String.format("Actual type (%s) differs from wanted (%s)", type,
419 public Date getDate() {
420 ensureType(ItemType.generalData);
421 return date.getValue();
423 public void setDate(Date date) {
424 ensureType(ItemType.generalData);
425 this.date.setValue(date);
427 public void setDate(String text) {
428 if ((text == null) || text.trim()
431 setDate((Date) null);
435 int year, month, day;
436 final Calendar c = GregorianCalendar.getInstance();
437 Matcher m = reYMD.matcher(text);
439 year = Integer.parseInt(m.group(1));
440 month = Integer.parseInt(m.group(2)) - 1; // month is 0-based
441 day = Integer.parseInt(m.group(3));
444 year = c.get(Calendar.YEAR);
445 m = reMD.matcher(text);
447 month = Integer.parseInt(m.group(1)) - 1;
448 day = Integer.parseInt(m.group(2));
451 month = c.get(Calendar.MONTH);
452 m = reD.matcher(text);
454 day = Integer.parseInt(m.group(1));
457 day = c.get(Calendar.DAY_OF_MONTH);
462 c.set(year, month, day);
464 this.setDate(c.getTime());
466 public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
467 @NonNull androidx.lifecycle.Observer<? super Date> observer) {
468 this.date.observe(owner, observer);
470 public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
471 this.date.removeObserver(observer);
473 public String getDescription() {
474 ensureType(ItemType.generalData);
475 return description.getValue();
477 public void setDescription(String description) {
478 ensureType(ItemType.generalData);
479 this.description.setValue(description);
481 public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
483 androidx.lifecycle.Observer<? super String> observer) {
484 this.description.observe(owner, observer);
486 public void stopObservingDescription(
487 @NonNull androidx.lifecycle.Observer<? super String> observer) {
488 this.description.removeObserver(observer);
490 public LedgerTransactionAccount getAccount() {
491 ensureType(ItemType.transactionRow);
494 public void setAccountName(String name) {
495 account.setAccountName(name);
500 * @return nicely formatted, shortest available date representation
502 public String getFormattedDate() {
505 Date time = date.getValue();
509 Calendar c = GregorianCalendar.getInstance();
511 Calendar today = GregorianCalendar.getInstance();
513 final int myYear = c.get(Calendar.YEAR);
514 final int myMonth = c.get(Calendar.MONTH);
515 final int myDay = c.get(Calendar.DAY_OF_MONTH);
517 if (today.get(Calendar.YEAR) != myYear) {
518 return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay);
521 if (today.get(Calendar.MONTH) != myMonth) {
522 return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay);
525 return String.valueOf(myDay);
527 public void observeEditableFlag(NewTransactionActivity activity,
528 Observer<Boolean> observer) {
529 editable.observe(activity, observer);
531 public void stopObservingEditableFlag(Observer<Boolean> observer) {
532 editable.removeObserver(observer);
534 public void observeCommentVisible(NewTransactionActivity activity,
535 Observer<Boolean> observer) {
536 commentVisible.observe(activity, observer);
538 public void stopObservingCommentVisible(Observer<Boolean> observer) {
539 commentVisible.removeObserver(observer);
541 public void observeComment(NewTransactionActivity activity, Observer<String> observer) {
542 comment.observe(activity, observer);
544 public void stopObservingComment(Observer<String> observer) {
545 comment.removeObserver(observer);
547 public void setComment(String comment) {
548 getAccount().setComment(comment);
549 this.comment.postValue(comment);
551 public Currency getCurrency() {
552 return this.currency.getValue();
554 public void setCurrency(Currency currency) {
555 getAccount().setCurrency((currency != null && !currency.getName()
556 .isEmpty()) ? currency.getName()
558 this.currency.setValue(currency);
560 public void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
561 currency.observe(activity, observer);
563 public void stopObservingCurrency(Observer<Currency> observer) {
564 currency.removeObserver(observer);