]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
clean unneeded import
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionItemsAdapter.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 import android.app.Activity;
22 import android.database.Cursor;
23 import android.view.LayoutInflater;
24 import android.view.ViewGroup;
25 import android.widget.LinearLayout;
26
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.recyclerview.widget.ItemTouchHelper;
30 import androidx.recyclerview.widget.RecyclerView;
31
32 import net.ktnx.mobileledger.BuildConfig;
33 import net.ktnx.mobileledger.R;
34 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
35 import net.ktnx.mobileledger.model.Currency;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.LedgerTransaction;
38 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
39 import net.ktnx.mobileledger.model.MobileLedgerProfile;
40 import net.ktnx.mobileledger.utils.Logger;
41 import net.ktnx.mobileledger.utils.MLDB;
42 import net.ktnx.mobileledger.utils.Misc;
43
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Locale;
48 import java.util.Set;
49
50 import static net.ktnx.mobileledger.utils.Logger.debug;
51
52 class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
53         implements DescriptionSelectedCallback {
54     private NewTransactionModel model;
55     private MobileLedgerProfile mProfile;
56     private ItemTouchHelper touchHelper;
57     private RecyclerView recyclerView;
58     private int checkHoldCounter = 0;
59     NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
60         super();
61         model = viewModel;
62         mProfile = profile;
63         int size = model.getAccountCount();
64         while (size < 2) {
65             Logger.debug("new-transaction",
66                     String.format(Locale.US, "%d accounts is too little, Calling addRow()", size));
67             size = addRow();
68         }
69
70         NewTransactionItemsAdapter adapter = this;
71
72         touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
73             @Override
74             public boolean isLongPressDragEnabled() {
75                 return true;
76             }
77             @Override
78             public boolean canDropOver(@NonNull RecyclerView recyclerView,
79                                        @NonNull RecyclerView.ViewHolder current,
80                                        @NonNull RecyclerView.ViewHolder target) {
81                 final int adapterPosition = target.getAdapterPosition();
82
83                 // first and last items are immovable
84                 if (adapterPosition == 0)
85                     return false;
86                 if (adapterPosition == adapter.getItemCount() - 1)
87                     return false;
88
89                 return super.canDropOver(recyclerView, current, target);
90             }
91             @Override
92             public int getMovementFlags(@NonNull RecyclerView recyclerView,
93                                         @NonNull RecyclerView.ViewHolder viewHolder) {
94                 int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
95                 // the top (date and description) and the bottom (padding) items are always there
96                 final int adapterPosition = viewHolder.getAdapterPosition();
97                 if ((adapterPosition > 0) && (adapterPosition < adapter.getItemCount() - 1)) {
98                     flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
99                             ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
100                              makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
101                                      ItemTouchHelper.START | ItemTouchHelper.END);
102                 }
103
104                 return flags;
105             }
106             @Override
107             public boolean onMove(@NonNull RecyclerView recyclerView,
108                                   @NonNull RecyclerView.ViewHolder viewHolder,
109                                   @NonNull RecyclerView.ViewHolder target) {
110
111                 model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
112                 notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
113                 return true;
114             }
115             @Override
116             public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
117                 int pos = viewHolder.getAdapterPosition();
118                 viewModel.removeItem(pos - 1);
119                 notifyItemRemoved(pos);
120                 viewModel.sendCountNotifications(); // needed after items re-arrangement
121                 checkTransactionSubmittable();
122             }
123         });
124     }
125     public void setProfile(MobileLedgerProfile profile) {
126         mProfile = profile;
127     }
128     private int addRow() {
129         return addRow(null);
130     }
131     private int addRow(String commodity) {
132         final int newAccountCount = model.addAccount(new LedgerTransactionAccount("", commodity));
133         Logger.debug("new-transaction",
134                 String.format(Locale.US, "invoking notifyItemInserted(%d)", newAccountCount));
135         // the header is at position 0
136         notifyItemInserted(newAccountCount);
137         model.sendCountNotifications(); // needed after holders' positions have changed
138         return newAccountCount;
139     }
140     @NonNull
141     @Override
142     public NewTransactionItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
143         LinearLayout row = (LinearLayout) LayoutInflater.from(parent.getContext())
144                                                         .inflate(R.layout.new_transaction_row,
145                                                                 parent, false);
146
147         return new NewTransactionItemHolder(row, this);
148     }
149     @Override
150     public void onBindViewHolder(@NonNull NewTransactionItemHolder holder, int position) {
151         Logger.debug("bind", String.format(Locale.US, "Binding item at position %d", position));
152         NewTransactionModel.Item item = model.getItem(position);
153         holder.setData(item);
154         Logger.debug("bind", String.format(Locale.US, "Bound %s item at position %d", item.getType()
155                                                                                           .toString(),
156                 position));
157     }
158     @Override
159     public int getItemCount() {
160         return model.getAccountCount() + 2;
161     }
162     private boolean accountListIsEmpty() {
163         for (int i = 0; i < model.getAccountCount(); i++) {
164             LedgerTransactionAccount acc = model.getAccount(i);
165             if (!acc.getAccountName()
166                     .isEmpty())
167                 return false;
168             if (acc.isAmountSet())
169                 return false;
170         }
171
172         return true;
173     }
174     @Override
175     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
176         super.onAttachedToRecyclerView(recyclerView);
177         this.recyclerView = recyclerView;
178         touchHelper.attachToRecyclerView(recyclerView);
179     }
180     @Override
181     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
182         touchHelper.attachToRecyclerView(null);
183         super.onDetachedFromRecyclerView(recyclerView);
184         this.recyclerView = null;
185     }
186     public void descriptionSelected(String description) {
187         debug("descr selected", description);
188         if (!accountListIsEmpty())
189             return;
190
191         String accFilter = mProfile.getPreferredAccountsFilter();
192
193         ArrayList<String> params = new ArrayList<>();
194         StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
195
196         if (!Misc.isEmptyOrNull(accFilter)) {
197             sb.append(" JOIN transaction_accounts ta")
198               .append(" ON ta.profile = t.profile")
199               .append(" AND ta.transaction_id = t.id");
200         }
201
202         sb.append(" WHERE t.description=?");
203         params.add(description);
204
205         if (!Misc.isEmptyOrNull(accFilter)) {
206             sb.append(" AND ta.account_name LIKE '%'||?||'%'");
207             params.add(accFilter);
208         }
209
210         sb.append(" ORDER BY t.date DESC LIMIT 1");
211
212         final String sql = sb.toString();
213         debug("descr", sql);
214         debug("descr", params.toString());
215
216         Activity activity = (Activity) recyclerView.getContext();
217         // FIXME: handle exceptions?
218         MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
219             @Override
220             public void onStart() {
221                 model.incrementBusyCounter();
222             }
223             @Override
224             public void onDone() {
225                 model.decrementBusyCounter();
226             }
227             @Override
228             public boolean onRow(@NonNull Cursor cursor) {
229                 final String profileUUID = cursor.getString(0);
230                 final int transactionId = cursor.getInt(1);
231                 activity.runOnUiThread(() -> loadTransactionIntoModel(profileUUID, transactionId));
232                 return false; // limit 1, by the way
233             }
234             @Override
235             public void onNoRows() {
236                 if (Misc.isEmptyOrNull(accFilter))
237                     return;
238
239                 debug("descr", "Trying transaction search without preferred account filter");
240
241                 final String broaderSql =
242                         "select t.profile, t.id from transactions t where t.description=?" +
243                         " ORDER BY date desc LIMIT 1";
244                 params.remove(1);
245                 debug("descr", broaderSql);
246                 debug("descr", description);
247
248                 MLDB.queryInBackground(broaderSql, new String[]{description},
249                         new MLDB.CallbackHelper() {
250                             @Override
251                             public void onStart() {
252                                 model.incrementBusyCounter();
253                             }
254                             @Override
255                             public boolean onRow(@NonNull Cursor cursor) {
256                                 final String profileUUID = cursor.getString(0);
257                                 final int transactionId = cursor.getInt(1);
258                                 activity.runOnUiThread(
259                                         () -> loadTransactionIntoModel(profileUUID, transactionId));
260                                 return false;
261                             }
262                             @Override
263                             public void onDone() {
264                                 model.decrementBusyCounter();
265                             }
266                         });
267             }
268         });
269     }
270     private void loadTransactionIntoModel(String profileUUID, int transactionId) {
271         LedgerTransaction tr;
272         MobileLedgerProfile profile = Data.getProfile(profileUUID);
273         if (profile == null)
274             throw new RuntimeException(String.format(
275                     "Unable to find profile %s, which is supposed to contain transaction %d",
276                     profileUUID, transactionId));
277
278         tr = profile.loadTransaction(transactionId);
279         ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
280         NewTransactionModel.Item firstNegative = null;
281         NewTransactionModel.Item firstPositive = null;
282         int singleNegativeIndex = -1;
283         int singlePositiveIndex = -1;
284         int negativeCount = 0;
285         for (int i = 0; i < accounts.size(); i++) {
286             LedgerTransactionAccount acc = accounts.get(i);
287             NewTransactionModel.Item item;
288             if (model.getAccountCount() < i + 1) {
289                 model.addAccount(acc);
290                 notifyItemInserted(i + 1);
291             }
292             item = model.getItem(i + 1);
293
294             item.getAccount()
295                 .setAccountName(acc.getAccountName());
296             item.setComment(acc.getComment());
297             if (acc.isAmountSet()) {
298                 item.getAccount()
299                     .setAmount(acc.getAmount());
300                 if (acc.getAmount() < 0) {
301                     if (firstNegative == null) {
302                         firstNegative = item;
303                         singleNegativeIndex = i;
304                     }
305                     else
306                         singleNegativeIndex = -1;
307                 }
308                 else {
309                     if (firstPositive == null) {
310                         firstPositive = item;
311                         singlePositiveIndex = i;
312                     }
313                     else
314                         singlePositiveIndex = -1;
315                 }
316             }
317             else
318                 item.getAccount()
319                     .resetAmount();
320             notifyItemChanged(i + 1);
321         }
322
323         if (singleNegativeIndex != -1) {
324             firstNegative.getAccount()
325                          .resetAmount();
326             model.moveItemLast(singleNegativeIndex);
327         }
328         else if (singlePositiveIndex != -1) {
329             firstPositive.getAccount()
330                          .resetAmount();
331             model.moveItemLast(singlePositiveIndex);
332         }
333
334         checkTransactionSubmittable();
335         model.setFocusedItem(1);
336     }
337     public void toggleAllEditing(boolean editable) {
338         // item 0 is the header
339         for (int i = 0; i <= model.getAccountCount(); i++) {
340             model.getItem(i)
341                  .setEditable(editable);
342             notifyItemChanged(i);
343             // TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
344         }
345     }
346     void reset() {
347         int presentItemCount = model.getAccountCount();
348         model.reset();
349         notifyItemChanged(0);       // header changed
350         notifyItemRangeChanged(1, 2);    // the two empty rows
351         if (presentItemCount > 2)
352             notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
353     }
354     void updateFocusedItem(int position) {
355         model.updateFocusedItem(position);
356     }
357     void noteFocusIsOnAccount(int position) {
358         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
359     }
360     void noteFocusIsOnAmount(int position) {
361         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
362     }
363     void noteFocusIsOnComment(int position) {
364         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
365     }
366     private void holdSubmittableChecks() {
367         checkHoldCounter++;
368     }
369     private void releaseSubmittableChecks() {
370         if (checkHoldCounter == 0)
371             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
372         checkHoldCounter--;
373     }
374     void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
375         Currency oldCurrency = item.getCurrency();
376         if (!Currency.equal(newCurrency, oldCurrency)) {
377             holdSubmittableChecks();
378             try {
379                 item.setCurrency(newCurrency);
380 //                for (Item i : items) {
381 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
382 //                        i.setCurrency(newCurrency);
383 //                }
384             }
385             finally {
386                 releaseSubmittableChecks();
387             }
388
389             checkTransactionSubmittable();
390         }
391     }
392     /*
393          A transaction is submittable if:
394          0) has description
395          1) has at least two account names
396          2) each row with amount has account name
397          3) for each commodity:
398          3a) amounts must balance to 0, or
399          3b) there must be exactly one empty amount (with account)
400          4) empty accounts with empty amounts are ignored
401          Side effects:
402          5) a row with an empty account name or empty amount is guaranteed to exist for each
403          commodity
404          6) at least two rows need to be present in the ledger
405
406         */
407     @SuppressLint("DefaultLocale")
408     void checkTransactionSubmittable() {
409         if (checkHoldCounter > 0)
410             return;
411
412         int accounts = 0;
413         final BalanceForCurrency balance = new BalanceForCurrency();
414         final String descriptionText = model.getDescription();
415         boolean submittable = true;
416         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
417         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
418         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
419         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
420         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
421         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
422         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
423         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
424
425         try {
426             if ((descriptionText == null) || descriptionText.trim()
427                                                             .isEmpty())
428             {
429                 Logger.debug("submittable", "Transaction not submittable: missing description");
430                 submittable = false;
431             }
432
433             for (int i = 0; i < model.items.size(); i++) {
434                 NewTransactionModel.Item item = model.items.get(i);
435
436                 LedgerTransactionAccount acc = item.getAccount();
437                 String acc_name = acc.getAccountName()
438                                      .trim();
439                 String currName = acc.getCurrency();
440
441                 itemsForCurrency.add(currName, item);
442
443                 if (acc_name.isEmpty()) {
444                     itemsWithEmptyAccountForCurrency.add(currName, item);
445
446                     if (acc.isAmountSet()) {
447                         // 2) each amount has account name
448                         Logger.debug("submittable", String.format(
449                                 "Transaction not submittable: row %d has no account name, but" +
450                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
451                         submittable = false;
452                     }
453                     else {
454                         emptyRowsForCurrency.add(currName, item);
455                     }
456                 }
457                 else {
458                     accounts++;
459                     itemsWithAccountForCurrency.add(currName, item);
460                 }
461
462                 if (acc.isAmountSet()) {
463                     itemsWithAmountForCurrency.add(currName, item);
464                     balance.add(currName, acc.getAmount());
465                 }
466                 else {
467                     itemsWithEmptyAmountForCurrency.add(currName, item);
468
469                     if (!acc_name.isEmpty())
470                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
471                 }
472             }
473
474             // 1) has at least two account names
475             if (accounts < 2) {
476                 if (accounts == 0)
477                     Logger.debug("submittable",
478                             "Transaction not submittable: no account " + "names");
479                 else if (accounts == 1)
480                     Logger.debug("submittable",
481                             "Transaction not submittable: only one account name");
482                 else
483                     Logger.debug("submittable",
484                             String.format("Transaction not submittable: only %d account names",
485                                     accounts));
486                 submittable = false;
487             }
488
489             // 3) for each commodity:
490             // 3a) amount must balance to 0, or
491             // 3b) there must be exactly one empty amount (with account)
492             for (String balCurrency : itemsForCurrency.currencies()) {
493                 float currencyBalance = balance.get(balCurrency);
494                 if (Misc.isZero(currencyBalance)) {
495                     // remove hints from all amount inputs in that currency
496                     for (NewTransactionModel.Item item : model.items) {
497                         if (Currency.equal(item.getCurrency(), balCurrency))
498                             item.setAmountHint(null);
499                     }
500                 }
501                 else {
502                     List<NewTransactionModel.Item> list =
503                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
504                     int balanceReceiversCount = list.size();
505                     if (balanceReceiversCount != 1) {
506                         if (BuildConfig.DEBUG) {
507                             if (balanceReceiversCount == 0)
508                                 Logger.debug("submittable", String.format(
509                                         "Transaction not submittable [%s]: non-zero balance " +
510                                         "with no empty amounts with accounts", balCurrency));
511                             else
512                                 Logger.debug("submittable", String.format(
513                                         "Transaction not submittable [%s]: non-zero balance " +
514                                         "with multiple empty amounts with accounts", balCurrency));
515                         }
516                         submittable = false;
517                     }
518
519                     List<NewTransactionModel.Item> emptyAmountList =
520                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
521
522                     // suggest off-balance amount to a row and remove hints on other rows
523                     NewTransactionModel.Item receiver = null;
524                     if (!list.isEmpty())
525                         receiver = list.get(0);
526                     else if (!emptyAmountList.isEmpty())
527                         receiver = emptyAmountList.get(0);
528
529                     for (NewTransactionModel.Item item : model.items) {
530                         if (!Currency.equal(item.getCurrency(), balCurrency))
531                             continue;
532
533                         if (item.equals(receiver)) {
534                             if (BuildConfig.DEBUG)
535                                 Logger.debug("submittable",
536                                         String.format("Setting amount hint to %1.2f [%s]",
537                                                 -currencyBalance, balCurrency));
538                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
539                         }
540                         else {
541                             if (BuildConfig.DEBUG)
542                                 Logger.debug("submittable",
543                                         String.format("Resetting hint of '%s' [%s]",
544                                                 (item.getAccount() == null) ? "" : item.getAccount()
545                                                                                        .getAccountName(),
546                                                 balCurrency));
547                             item.setAmountHint(null);
548                         }
549                     }
550                 }
551             }
552
553             // 5) a row with an empty account name or empty amount is guaranteed to exist for
554             // each commodity
555             for (String balCurrency : balance.currencies()) {
556                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
557                 int currRows = itemsForCurrency.size(balCurrency);
558                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
559                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
560                 if ((currEmptyRows == 0) &&
561                     ((currRows == currAccounts) || (currRows == currAmounts)))
562                 {
563                     // perhaps there already is an unused empty row for another currency that
564                     // is not used?
565 //                        boolean foundIt = false;
566 //                        for (Item item : emptyRows) {
567 //                            Currency itemCurrency = item.getCurrency();
568 //                            String itemCurrencyName =
569 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
570 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
571 //                                item.setCurrency(Currency.loadByName(balCurrency));
572 //                                item.setAmountHint(
573 //                                        String.format("%1.2f", -balance.get(balCurrency)));
574 //                                foundIt = true;
575 //                                break;
576 //                            }
577 //                        }
578 //
579 //                        if (!foundIt)
580                     addRow(balCurrency);
581                 }
582             }
583
584             // drop extra empty rows, not needed
585             for (String currName : emptyRowsForCurrency.currencies()) {
586                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
587                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
588                     NewTransactionModel.Item item = emptyItems.get(1);
589                     emptyItems.remove(1);
590                     model.removeRow(item, this);
591                 }
592
593                 // unused currency, remove last item (which is also an empty one)
594                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
595                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
596
597                     if (currItems.size() == 1) {
598                         NewTransactionModel.Item item = emptyItems.get(0);
599                         model.removeRow(item, this);
600                     }
601                 }
602             }
603
604             // 6) at least two rows need to be present in the ledger
605             while (model.items.size() < 2)
606                 addRow();
607
608
609             debug("submittable", submittable ? "YES" : "NO");
610             model.isSubmittable.setValue(submittable);
611
612             if (BuildConfig.DEBUG) {
613                 debug("submittable", "== Dump of all items");
614                 for (int i = 0; i < model.items.size(); i++) {
615                     NewTransactionModel.Item item = model.items.get(i);
616                     LedgerTransactionAccount acc = item.getAccount();
617                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
618                             acc.isAmountSet() ? acc.getAmount() : 0,
619                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
620                             acc.getAccountName(), acc.getComment()));
621                 }
622             }
623         }
624         catch (NumberFormatException e) {
625             debug("submittable", "NO (because of NumberFormatException)");
626             model.isSubmittable.setValue(false);
627         }
628         catch (Exception e) {
629             e.printStackTrace();
630             debug("submittable", "NO (because of an Exception)");
631             model.isSubmittable.setValue(false);
632         }
633     }
634
635     private static class BalanceForCurrency {
636         private HashMap<String, Float> hashMap = new HashMap<>();
637         float get(String currencyName) {
638             Float f = hashMap.get(currencyName);
639             if (f == null) {
640                 f = 0f;
641                 hashMap.put(currencyName, f);
642             }
643             return f;
644         }
645         void add(String currencyName, float amount) {
646             hashMap.put(currencyName, get(currencyName) + amount);
647         }
648         Set<String> currencies() {
649             return hashMap.keySet();
650         }
651         boolean containsCurrency(String currencyName) {
652             return hashMap.containsKey(currencyName);
653         }
654     }
655
656     private static class ItemsForCurrency {
657         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
658         @NonNull
659         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
660             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
661             if (list == null) {
662                 list = new ArrayList<>();
663                 hashMap.put(currencyName, list);
664             }
665             return list;
666         }
667         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
668             getList(currencyName).add(item);
669         }
670         int size(@Nullable String currencyName) {
671             return this.getList(currencyName)
672                        .size();
673         }
674         Set<String> currencies() {
675             return hashMap.keySet();
676         }
677     }
678 }