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