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