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