]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
support multiple currencies in new transaction activity
[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 android.annotation.SuppressLint;
21
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.lifecycle.LiveData;
25 import androidx.lifecycle.MutableLiveData;
26 import androidx.lifecycle.Observer;
27 import androidx.lifecycle.ViewModel;
28
29 import net.ktnx.mobileledger.BuildConfig;
30 import net.ktnx.mobileledger.model.Currency;
31 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
32 import net.ktnx.mobileledger.utils.Logger;
33 import net.ktnx.mobileledger.utils.Misc;
34
35 import org.jetbrains.annotations.NotNull;
36
37 import java.util.ArrayList;
38 import java.util.Calendar;
39 import java.util.Collections;
40 import java.util.Date;
41 import java.util.GregorianCalendar;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Locale;
45 import java.util.Set;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48
49 import static net.ktnx.mobileledger.utils.Logger.debug;
50
51 public class NewTransactionModel extends ViewModel {
52     private static final Pattern reYMD =
53             Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
54     private static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
55     private static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
56     final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
57     private final Item header = new Item(this, null, "");
58     private final Item trailer = new Item(this);
59     private final ArrayList<Item> items = new ArrayList<>();
60     private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
61     private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
62     private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
63     private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
64     /*
65      Slots contain lists of items, all using the same currency, including the possible
66      item with no account/amount that is used to help balancing the transaction
67
68      There is one slot per currency
69      */
70     private final HashMap<String, List<Item>> slots = new HashMap<>();
71     private int checkHoldCounter = 0;
72     void holdSubmittableChecks() {
73         checkHoldCounter++;
74     }
75     void releaseSubmittableChecks() {
76         if (checkHoldCounter == 0)
77             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
78         checkHoldCounter--;
79     }
80     boolean getSimulateSave() {
81         return simulateSave.getValue();
82     }
83     public void setSimulateSave(boolean simulateSave) {
84         this.simulateSave.setValue(simulateSave);
85     }
86     void toggleSimulateSave() {
87         simulateSave.setValue(!simulateSave.getValue());
88     }
89     void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
90                              @NonNull androidx.lifecycle.Observer<? super Boolean> observer) {
91         this.simulateSave.observe(owner, observer);
92     }
93     int getAccountCount() {
94         return items.size();
95     }
96     public Date getDate() {
97         return header.date.getValue();
98     }
99     public String getDescription() {
100         return header.description.getValue();
101     }
102     LiveData<Boolean> isSubmittable() {
103         return this.isSubmittable;
104     }
105     void reset() {
106         header.date.setValue(null);
107         header.description.setValue(null);
108         items.clear();
109         items.add(new Item(this, new LedgerTransactionAccount("")));
110         items.add(new Item(this, new LedgerTransactionAccount("")));
111         focusedItem.setValue(0);
112     }
113     void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
114                             @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
115         this.focusedItem.observe(owner, observer);
116     }
117     void stopObservingFocusedItem(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
118         this.focusedItem.removeObserver(observer);
119     }
120     void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
121                              @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
122         this.accountCount.observe(owner, observer);
123     }
124     void stopObservingAccountCount(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
125         this.accountCount.removeObserver(observer);
126     }
127     int getFocusedItem() { return focusedItem.getValue(); }
128     void setFocusedItem(int position) {
129         focusedItem.setValue(position);
130     }
131     int addAccount(LedgerTransactionAccount acc) {
132         items.add(new Item(this, acc));
133         accountCount.setValue(getAccountCount());
134         return items.size();
135     }
136     boolean accountsInInitialState() {
137         for (Item item : items) {
138             LedgerTransactionAccount acc = item.getAccount();
139             if (acc.isAmountSet())
140                 return false;
141             if (!acc.getAccountName()
142                     .trim()
143                     .isEmpty())
144                 return false;
145         }
146
147         return true;
148     }
149     LedgerTransactionAccount getAccount(int index) {
150         return items.get(index)
151                     .getAccount();
152     }
153     Item getItem(int index) {
154         if (index == 0) {
155             return header;
156         }
157
158         if (index <= items.size())
159             return items.get(index - 1);
160
161         return trailer;
162     }
163     /*
164      A transaction is submittable if:
165      0) has description
166      1) has at least two account names
167      2) each row with amount has account name
168      3) for each commodity:
169      3a) amounts must balance to 0, or
170      3b) there must be exactly one empty amount (with account)
171      4) empty accounts with empty amounts are ignored
172      Side effects:
173      5) a row with an empty account name or empty amount is guaranteed to exist for each commodity
174      6) at least two rows need to be present in the ledger
175
176     */
177     @SuppressLint("DefaultLocale")
178     void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
179         if (checkHoldCounter > 0)
180             return;
181
182         int accounts = 0;
183         final BalanceForCurrency balance = new BalanceForCurrency();
184         final String descriptionText = getDescription();
185         boolean submittable = true;
186         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
187         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
188         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
189         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
190         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
191         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
192         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
193         final List<Item> emptyRows = new ArrayList<>();
194
195         try {
196             if ((descriptionText == null) || descriptionText.trim()
197                                                             .isEmpty())
198             {
199                 Logger.debug("submittable", "Transaction not submittable: missing description");
200                 submittable = false;
201             }
202
203             for (int i = 0; i < this.items.size(); i++) {
204                 Item item = this.items.get(i);
205
206                 LedgerTransactionAccount acc = item.getAccount();
207                 String acc_name = acc.getAccountName()
208                                      .trim();
209                 String currName = acc.getCurrency();
210
211                 itemsForCurrency.add(currName, item);
212
213                 if (acc_name.isEmpty()) {
214                     itemsWithEmptyAccountForCurrency.add(currName, item);
215
216                     if (acc.isAmountSet()) {
217                         // 2) each amount has account name
218                         Logger.debug("submittable", String.format(
219                                 "Transaction not submittable: row %d has no account name, but" +
220                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
221                         submittable = false;
222                     }
223                     else {
224                         emptyRowsForCurrency.add(currName, item);
225                     }
226                 }
227                 else {
228                     accounts++;
229                     itemsWithAccountForCurrency.add(currName, item);
230                 }
231
232                 if (acc.isAmountSet()) {
233                     itemsWithAmountForCurrency.add(currName, item);
234                     balance.add(currName, acc.getAmount());
235                 }
236                 else {
237                     itemsWithEmptyAmountForCurrency.add(currName, item);
238
239                     if (!acc_name.isEmpty())
240                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
241                 }
242             }
243
244             // 1) has at least two account names
245             if (accounts < 2) {
246                 if (accounts == 0)
247                     Logger.debug("submittable",
248                             "Transaction not submittable: no account " + "names");
249                 else if (accounts == 1)
250                     Logger.debug("submittable",
251                             "Transaction not submittable: only one account name");
252                 else
253                     Logger.debug("submittable",
254                             String.format("Transaction not submittable: only %d account names",
255                                     accounts));
256                 submittable = false;
257             }
258
259             // 3) for each commodity:
260             // 3a) amount must balance to 0, or
261             // 3b) there must be exactly one empty amount (with account)
262             for (String balCurrency : itemsForCurrency.currencies()) {
263                 float currencyBalance = balance.get(balCurrency);
264                 if (Misc.isZero(currencyBalance)) {
265                     // remove hints from all amount inputs in that currency
266                     for (Item item : items) {
267                         if (Currency.equal(item.getCurrency(), balCurrency))
268                             item.setAmountHint(null);
269                     }
270                 }
271                 else {
272                     List<Item> list =
273                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
274                     int balanceReceiversCount = list.size();
275                     if (balanceReceiversCount != 1) {
276                         if (BuildConfig.DEBUG) {
277                             if (balanceReceiversCount == 0)
278                                 Logger.debug("submittable", String.format(
279                                         "Transaction not submittable [%s]: non-zero balance " +
280                                         "with no empty amounts with accounts", balCurrency));
281                             else
282                                 Logger.debug("submittable", String.format(
283                                         "Transaction not submittable [%s]: non-zero balance " +
284                                         "with multiple empty amounts with accounts", balCurrency));
285                         }
286                         submittable = false;
287                     }
288
289                     List<Item> emptyAmountList =
290                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
291
292                     // suggest off-balance amount to a row and remove hints on other rows
293                     Item receiver = null;
294                     if (!list.isEmpty())
295                         receiver = list.get(0);
296                     else if (!emptyAmountList.isEmpty())
297                         receiver = emptyAmountList.get(0);
298
299                     for (Item item : items) {
300                         if (!Currency.equal(item.getCurrency(), balCurrency))
301                             continue;
302
303                         if (item.equals(receiver)) {
304                             if (BuildConfig.DEBUG)
305                                 Logger.debug("submittable",
306                                         String.format("Setting amount hint to %1.2f [%s]",
307                                                 -currencyBalance, balCurrency));
308                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
309                         }
310                         else {
311                             if (BuildConfig.DEBUG)
312                                 Logger.debug("submittable",
313                                         String.format("Resetting hint of '%s' [%s]",
314                                                 (item.getAccount() == null) ? "" : item.getAccount()
315                                                                                        .getAccountName(),
316                                                 balCurrency));
317                             item.setAmountHint(null);
318                         }
319                     }
320                 }
321             }
322
323             // 5) a row with an empty account name or empty amount is guaranteed to exist for
324             // each commodity
325             for (String balCurrency : balance.currencies()) {
326                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
327                 int currRows = itemsForCurrency.size(balCurrency);
328                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
329                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
330                 if ((currEmptyRows == 0) &&
331                     ((currRows == currAccounts) || (currRows == currAmounts)))
332                 {
333                     // perhaps there already is an unused empty row for another currency that
334                     // is not used?
335 //                        boolean foundIt = false;
336 //                        for (Item item : emptyRows) {
337 //                            Currency itemCurrency = item.getCurrency();
338 //                            String itemCurrencyName =
339 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
340 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
341 //                                item.setCurrency(Currency.loadByName(balCurrency));
342 //                                item.setAmountHint(
343 //                                        String.format("%1.2f", -balance.get(balCurrency)));
344 //                                foundIt = true;
345 //                                break;
346 //                            }
347 //                        }
348 //
349 //                        if (!foundIt)
350                     adapter.addRow(balCurrency);
351                 }
352             }
353
354             // drop extra empty rows, not needed
355             for (String currName : emptyRowsForCurrency.currencies()) {
356                 List<Item> emptyItems = emptyRowsForCurrency.getList(currName);
357                 while ((this.items.size() > 2) && (emptyItems.size() > 1)) {
358                     Item item = emptyItems.get(1);
359                     emptyItems.remove(1);
360                     removeRow(item, adapter);
361                 }
362
363                 // unused currency, remove last item (which is also an empty one)
364                 if ((items.size() > 2) && (emptyItems.size() == 1)) {
365                     List<Item> currItems = itemsForCurrency.getList(currName);
366
367                     if (currItems.size() == 1){
368                         Item item = emptyItems.get(0);
369                         removeRow(item, adapter);
370                     }
371                 }
372             }
373
374             // 6) at least two rows need to be present in the ledger
375             while (this.items.size() < 2)
376                 adapter.addRow();
377
378
379             debug("submittable", submittable ? "YES" : "NO");
380             isSubmittable.setValue(submittable);
381
382             if (BuildConfig.DEBUG) {
383                 debug("submittable", "== Dump of all items");
384                 for (int i = 0; i < items.size(); i++) {
385                     Item item = items.get(i);
386                     LedgerTransactionAccount acc = item.getAccount();
387                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
388                             acc.isAmountSet() ? acc.getAmount() : 0,
389                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
390                             acc.getAccountName(), acc.getComment()));
391                 }
392             }
393         }
394         catch (NumberFormatException e) {
395             debug("submittable", "NO (because of NumberFormatException)");
396             isSubmittable.setValue(false);
397         }
398         catch (Exception e) {
399             e.printStackTrace();
400             debug("submittable", "NO (because of an Exception)");
401             isSubmittable.setValue(false);
402         }
403     }
404     private void removeRow(Item item, NewTransactionItemsAdapter adapter) {
405         int pos = items.indexOf(item);
406         items.remove(pos);
407         if (adapter != null) {
408             adapter.notifyItemRemoved(pos + 1);
409             sendCountNotifications();
410         }
411     }
412     void removeItem(int pos) {
413         items.remove(pos);
414         accountCount.setValue(getAccountCount());
415     }
416     void sendCountNotifications() {
417         accountCount.setValue(getAccountCount());
418     }
419     public void sendFocusedNotification() {
420         focusedItem.setValue(focusedItem.getValue());
421     }
422     void updateFocusedItem(int position) {
423         focusedItem.setValue(position);
424     }
425     void noteFocusChanged(int position, FocusedElement element) {
426         getItem(position).setFocusedElement(element);
427     }
428     void swapItems(int one, int two) {
429         Collections.swap(items, one - 1, two - 1);
430     }
431     void toggleComment(int position) {
432         final MutableLiveData<Boolean> commentVisible = getItem(position).commentVisible;
433         commentVisible.postValue(!commentVisible.getValue());
434     }
435     void moveItemLast(int index) {
436         /*   0
437              1   <-- index
438              2
439              3   <-- desired position
440          */
441         int itemCount = items.size();
442
443         if (index < itemCount - 1) {
444             Item acc = items.remove(index);
445             items.add(itemCount - 1, acc);
446         }
447     }
448     void toggleCurrencyVisible() {
449         showCurrency.setValue(!showCurrency.getValue());
450     }
451     public void setItemCurrency(Item item, Currency newCurrency,
452                                 NewTransactionItemsAdapter adapter) {
453         Currency oldCurrency = item.getCurrency();
454         if (!Currency.equal(newCurrency, oldCurrency)) {
455             holdSubmittableChecks();
456             try {
457                 item.setCurrency(newCurrency);
458 //                for (Item i : items) {
459 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
460 //                        i.setCurrency(newCurrency);
461 //                }
462             }
463             finally {
464                 releaseSubmittableChecks();
465             }
466
467             checkTransactionSubmittable(adapter);
468         }
469     }
470     enum ItemType {generalData, transactionRow, bottomFiller}
471
472     enum FocusedElement {Account, Comment, Amount}
473
474     private class ItemsForCurrency {
475         private HashMap<String, List<Item>> hashMap = new HashMap<>();
476         @NonNull
477         List<Item> getList(@Nullable String currencyName) {
478             List<Item> list = hashMap.get(currencyName);
479             if (list == null) {
480                 list = new ArrayList<>();
481                 hashMap.put(currencyName, list);
482             }
483             return list;
484         }
485         void add(@Nullable String currencyName, @NonNull Item item) {
486             getList(currencyName).add(item);
487         }
488         int size(@Nullable String currencyName) {
489             return this.getList(currencyName)
490                        .size();
491         }
492         Set<String> currencies() {
493             return hashMap.keySet();
494         }
495     }
496
497     //==========================================================================================
498
499     private class BalanceForCurrency {
500         private HashMap<String, Float> hashMap = new HashMap<>();
501         float get(String currencyName) {
502             Float f = hashMap.get(currencyName);
503             if (f == null) {
504                 f = 0f;
505                 hashMap.put(currencyName, f);
506             }
507             return f;
508         }
509         void add(String currencyName, float amount) {
510             hashMap.put(currencyName, get(currencyName) + amount);
511         }
512         Set<String> currencies() {
513             return hashMap.keySet();
514         }
515         boolean containsCurrency(String currencyName) {
516             return hashMap.containsKey(currencyName);
517         }
518     }
519
520     class Item {
521         private ItemType type;
522         private MutableLiveData<Date> date = new MutableLiveData<>();
523         private MutableLiveData<String> description = new MutableLiveData<>();
524         private LedgerTransactionAccount account;
525         private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
526         private NewTransactionModel model;
527         private MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
528         private FocusedElement focusedElement = FocusedElement.Account;
529         private MutableLiveData<String> comment = new MutableLiveData<>(null);
530         private MutableLiveData<Boolean> commentVisible = new MutableLiveData<>(false);
531         private MutableLiveData<Currency> currency = new MutableLiveData<>(null);
532         private boolean amountHintIsSet = false;
533         Item(NewTransactionModel model) {
534             this.model = model;
535             type = ItemType.bottomFiller;
536             editable.setValue(false);
537         }
538         Item(NewTransactionModel model, Date date, String description) {
539             this.model = model;
540             this.type = ItemType.generalData;
541             this.date.setValue(date);
542             this.description.setValue(description);
543             this.editable.setValue(true);
544         }
545         Item(NewTransactionModel model, LedgerTransactionAccount account) {
546             this.model = model;
547             this.type = ItemType.transactionRow;
548             this.account = account;
549             String currName = account.getCurrency();
550             Currency curr = null;
551             if ((currName != null) && !currName.isEmpty())
552                 curr = Currency.loadByName(currName);
553             this.currency.setValue(curr);
554             this.editable.setValue(true);
555         }
556         FocusedElement getFocusedElement() {
557             return focusedElement;
558         }
559         void setFocusedElement(FocusedElement focusedElement) {
560             this.focusedElement = focusedElement;
561         }
562         public NewTransactionModel getModel() {
563             return model;
564         }
565         void setEditable(boolean editable) {
566             ensureType(ItemType.generalData, ItemType.transactionRow);
567             this.editable.setValue(editable);
568         }
569         private void ensureType(ItemType type1, ItemType type2) {
570             if ((type != type1) && (type != type2)) {
571                 throw new RuntimeException(
572                         String.format("Actual type (%s) differs from wanted (%s or %s)", type,
573                                 type1, type2));
574             }
575         }
576         String getAmountHint() {
577             ensureType(ItemType.transactionRow);
578             return amountHint.getValue();
579         }
580         void setAmountHint(String amountHint) {
581             ensureType(ItemType.transactionRow);
582
583             // avoid unnecessary triggers
584             if (amountHint == null) {
585                 if (this.amountHint.getValue() == null)
586                     return;
587                 amountHintIsSet = false;
588             }
589             else {
590                 if (amountHint.equals(this.amountHint.getValue()))
591                     return;
592                 amountHintIsSet = true;
593             }
594
595             this.amountHint.setValue(amountHint);
596         }
597         void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
598                                @NonNull androidx.lifecycle.Observer<? super String> observer) {
599             this.amountHint.observe(owner, observer);
600         }
601         void stopObservingAmountHint(
602                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
603             this.amountHint.removeObserver(observer);
604         }
605         ItemType getType() {
606             return type;
607         }
608         void ensureType(ItemType wantedType) {
609             if (type != wantedType) {
610                 throw new RuntimeException(
611                         String.format("Actual type (%s) differs from wanted (%s)", type,
612                                 wantedType));
613             }
614         }
615         public Date getDate() {
616             ensureType(ItemType.generalData);
617             return date.getValue();
618         }
619         public void setDate(Date date) {
620             ensureType(ItemType.generalData);
621             this.date.setValue(date);
622         }
623         public void setDate(String text) {
624             if ((text == null) || text.trim()
625                                       .isEmpty())
626             {
627                 setDate((Date) null);
628                 return;
629             }
630
631             int year, month, day;
632             final Calendar c = GregorianCalendar.getInstance();
633             Matcher m = reYMD.matcher(text);
634             if (m.matches()) {
635                 year = Integer.parseInt(m.group(1));
636                 month = Integer.parseInt(m.group(2)) - 1;   // month is 0-based
637                 day = Integer.parseInt(m.group(3));
638             }
639             else {
640                 year = c.get(Calendar.YEAR);
641                 m = reMD.matcher(text);
642                 if (m.matches()) {
643                     month = Integer.parseInt(m.group(1)) - 1;
644                     day = Integer.parseInt(m.group(2));
645                 }
646                 else {
647                     month = c.get(Calendar.MONTH);
648                     m = reD.matcher(text);
649                     if (m.matches()) {
650                         day = Integer.parseInt(m.group(1));
651                     }
652                     else {
653                         day = c.get(Calendar.DAY_OF_MONTH);
654                     }
655                 }
656             }
657
658             c.set(year, month, day);
659
660             this.setDate(c.getTime());
661         }
662         void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
663                          @NonNull androidx.lifecycle.Observer<? super Date> observer) {
664             this.date.observe(owner, observer);
665         }
666         void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
667             this.date.removeObserver(observer);
668         }
669         public String getDescription() {
670             ensureType(ItemType.generalData);
671             return description.getValue();
672         }
673         public void setDescription(String description) {
674             ensureType(ItemType.generalData);
675             this.description.setValue(description);
676         }
677         void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
678                                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
679             this.description.observe(owner, observer);
680         }
681         void stopObservingDescription(
682                 @NonNull androidx.lifecycle.Observer<? super String> observer) {
683             this.description.removeObserver(observer);
684         }
685         public LedgerTransactionAccount getAccount() {
686             ensureType(ItemType.transactionRow);
687             return account;
688         }
689         public void setAccountName(String name) {
690             account.setAccountName(name);
691         }
692         /**
693          * getFormattedDate()
694          *
695          * @return nicely formatted, shortest available date representation
696          */
697         String getFormattedDate() {
698             if (date == null)
699                 return null;
700             Date time = date.getValue();
701             if (time == null)
702                 return null;
703
704             Calendar c = GregorianCalendar.getInstance();
705             c.setTime(time);
706             Calendar today = GregorianCalendar.getInstance();
707
708             final int myYear = c.get(Calendar.YEAR);
709             final int myMonth = c.get(Calendar.MONTH);
710             final int myDay = c.get(Calendar.DAY_OF_MONTH);
711
712             if (today.get(Calendar.YEAR) != myYear) {
713                 return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay);
714             }
715
716             if (today.get(Calendar.MONTH) != myMonth) {
717                 return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay);
718             }
719
720             return String.valueOf(myDay);
721         }
722         void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
723             editable.observe(activity, observer);
724         }
725         void stopObservingEditableFlag(Observer<Boolean> observer) {
726             editable.removeObserver(observer);
727         }
728         void observeCommentVisible(NewTransactionActivity activity, Observer<Boolean> observer) {
729             commentVisible.observe(activity, observer);
730         }
731         void stopObservingCommentVisible(Observer<Boolean> observer) {
732             commentVisible.removeObserver(observer);
733         }
734         void observeComment(NewTransactionActivity activity, Observer<String> observer) {
735             comment.observe(activity, observer);
736         }
737         void stopObservingComment(Observer<String> observer) {
738             comment.removeObserver(observer);
739         }
740         public void setComment(String comment) {
741             getAccount().setComment(comment);
742             this.comment.postValue(comment);
743         }
744         public Currency getCurrency() {
745             return this.currency.getValue();
746         }
747         public void setCurrency(Currency currency) {
748             Currency present = this.currency.getValue();
749             if ((currency == null) && (present != null) ||
750                 (currency != null) && !currency.equals(present))
751             {
752                 getAccount().setCurrency((currency != null && !currency.getName()
753                                                                        .isEmpty())
754                                          ? currency.getName() : null);
755                 this.currency.setValue(currency);
756             }
757         }
758         void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
759             currency.observe(activity, observer);
760         }
761         void stopObservingCurrency(Observer<Currency> observer) {
762             currency.removeObserver(observer);
763         }
764         boolean isOfType(ItemType type) {
765             return this.type == type;
766         }
767         boolean isAmountHintSet() {
768             return amountHintIsSet;
769         }
770     }
771 }