]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
7d01880057901954a2f9ad4c439050531aeabc78
[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 = new ItemsForCurrency();
390         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
391         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
392         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
393         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
394         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
395         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
396
397         try {
398             if ((descriptionText == null) || descriptionText.trim()
399                                                             .isEmpty())
400             {
401                 Logger.debug("submittable", "Transaction not submittable: missing description");
402                 submittable = false;
403             }
404
405             for (int i = 0; i < model.items.size(); i++) {
406                 NewTransactionModel.Item item = model.items.get(i);
407
408                 LedgerTransactionAccount acc = item.getAccount();
409                 String acc_name = acc.getAccountName()
410                                      .trim();
411                 String currName = acc.getCurrency();
412
413                 itemsForCurrency.add(currName, item);
414
415                 if (acc_name.isEmpty()) {
416                     itemsWithEmptyAccountForCurrency.add(currName, item);
417
418                     if (acc.isAmountSet()) {
419                         // 2) each amount has account name
420                         Logger.debug("submittable", String.format(
421                                 "Transaction not submittable: row %d has no account name, but" +
422                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
423                         submittable = false;
424                     }
425                     else {
426                         emptyRowsForCurrency.add(currName, item);
427                     }
428                 }
429                 else {
430                     accounts++;
431                     itemsWithAccountForCurrency.add(currName, item);
432                 }
433
434                 if (acc.isAmountSet()) {
435                     itemsWithAmountForCurrency.add(currName, item);
436                     balance.add(currName, acc.getAmount());
437                 }
438                 else {
439                     itemsWithEmptyAmountForCurrency.add(currName, item);
440
441                     if (!acc_name.isEmpty())
442                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
443                 }
444             }
445
446             // 1) has at least two account names
447             if (accounts < 2) {
448                 if (accounts == 0)
449                     Logger.debug("submittable",
450                             "Transaction not submittable: no account " + "names");
451                 else if (accounts == 1)
452                     Logger.debug("submittable",
453                             "Transaction not submittable: only one account name");
454                 else
455                     Logger.debug("submittable",
456                             String.format("Transaction not submittable: only %d account names",
457                                     accounts));
458                 submittable = false;
459             }
460
461             // 3) for each commodity:
462             // 3a) amount must balance to 0, or
463             // 3b) there must be exactly one empty amount (with account)
464             for (String balCurrency : itemsForCurrency.currencies()) {
465                 float currencyBalance = balance.get(balCurrency);
466                 if (Misc.isZero(currencyBalance)) {
467                     // remove hints from all amount inputs in that currency
468                     for (NewTransactionModel.Item item : model.items) {
469                         if (Currency.equal(item.getCurrency(), balCurrency))
470                             item.setAmountHint(null);
471                     }
472                 }
473                 else {
474                     List<NewTransactionModel.Item> list =
475                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
476                     int balanceReceiversCount = list.size();
477                     if (balanceReceiversCount != 1) {
478                         if (BuildConfig.DEBUG) {
479                             if (balanceReceiversCount == 0)
480                                 Logger.debug("submittable", String.format(
481                                         "Transaction not submittable [%s]: non-zero balance " +
482                                         "with no empty amounts with accounts", balCurrency));
483                             else
484                                 Logger.debug("submittable", String.format(
485                                         "Transaction not submittable [%s]: non-zero balance " +
486                                         "with multiple empty amounts with accounts", balCurrency));
487                         }
488                         submittable = false;
489                     }
490
491                     List<NewTransactionModel.Item> emptyAmountList =
492                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
493
494                     // suggest off-balance amount to a row and remove hints on other rows
495                     NewTransactionModel.Item receiver = null;
496                     if (!list.isEmpty())
497                         receiver = list.get(0);
498                     else if (!emptyAmountList.isEmpty())
499                         receiver = emptyAmountList.get(0);
500
501                     for (NewTransactionModel.Item item : model.items) {
502                         if (!Currency.equal(item.getCurrency(), balCurrency))
503                             continue;
504
505                         if (item.equals(receiver)) {
506                             if (BuildConfig.DEBUG)
507                                 Logger.debug("submittable",
508                                         String.format("Setting amount hint to %1.2f [%s]",
509                                                 -currencyBalance, balCurrency));
510                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
511                         }
512                         else {
513                             if (BuildConfig.DEBUG)
514                                 Logger.debug("submittable",
515                                         String.format("Resetting hint of '%s' [%s]",
516                                                 (item.getAccount() == null) ? "" : item.getAccount()
517                                                                                        .getAccountName(),
518                                                 balCurrency));
519                             item.setAmountHint(null);
520                         }
521                     }
522                 }
523             }
524
525             // 5) a row with an empty account name or empty amount is guaranteed to exist for
526             // each commodity
527             for (String balCurrency : balance.currencies()) {
528                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
529                 int currRows = itemsForCurrency.size(balCurrency);
530                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
531                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
532                 if ((currEmptyRows == 0) &&
533                     ((currRows == currAccounts) || (currRows == currAmounts)))
534                 {
535                     // perhaps there already is an unused empty row for another currency that
536                     // is not used?
537 //                        boolean foundIt = false;
538 //                        for (Item item : emptyRows) {
539 //                            Currency itemCurrency = item.getCurrency();
540 //                            String itemCurrencyName =
541 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
542 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
543 //                                item.setCurrency(Currency.loadByName(balCurrency));
544 //                                item.setAmountHint(
545 //                                        String.format("%1.2f", -balance.get(balCurrency)));
546 //                                foundIt = true;
547 //                                break;
548 //                            }
549 //                        }
550 //
551 //                        if (!foundIt)
552                     addRow(balCurrency);
553                 }
554             }
555
556             // drop extra empty rows, not needed
557             for (String currName : emptyRowsForCurrency.currencies()) {
558                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
559                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
560                     NewTransactionModel.Item item = emptyItems.get(1);
561                     emptyItems.remove(1);
562                     model.removeRow(item, this);
563                 }
564
565                 // unused currency, remove last item (which is also an empty one)
566                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
567                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
568
569                     if (currItems.size() == 1) {
570                         NewTransactionModel.Item item = emptyItems.get(0);
571                         model.removeRow(item, this);
572                     }
573                 }
574             }
575
576             // 6) at least two rows need to be present in the ledger
577             while (model.items.size() < 2)
578                 addRow();
579
580
581             debug("submittable", submittable ? "YES" : "NO");
582             model.isSubmittable.setValue(submittable);
583
584             if (BuildConfig.DEBUG) {
585                 debug("submittable", "== Dump of all items");
586                 for (int i = 0; i < model.items.size(); i++) {
587                     NewTransactionModel.Item item = model.items.get(i);
588                     LedgerTransactionAccount acc = item.getAccount();
589                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
590                             acc.isAmountSet() ? acc.getAmount() : 0,
591                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
592                             acc.getAccountName(), acc.getComment()));
593                 }
594             }
595         }
596         catch (NumberFormatException e) {
597             debug("submittable", "NO (because of NumberFormatException)");
598             model.isSubmittable.setValue(false);
599         }
600         catch (Exception e) {
601             e.printStackTrace();
602             debug("submittable", "NO (because of an Exception)");
603             model.isSubmittable.setValue(false);
604         }
605     }
606     private class BalanceForCurrency {
607         private HashMap<String, Float> hashMap = new HashMap<>();
608         float get(String currencyName) {
609             Float f = hashMap.get(currencyName);
610             if (f == null) {
611                 f = 0f;
612                 hashMap.put(currencyName, f);
613             }
614             return f;
615         }
616         void add(String currencyName, float amount) {
617             hashMap.put(currencyName, get(currencyName) + amount);
618         }
619         Set<String> currencies() {
620             return hashMap.keySet();
621         }
622         boolean containsCurrency(String currencyName) {
623             return hashMap.containsKey(currencyName);
624         }
625     }
626
627     private class ItemsForCurrency {
628         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
629         @NonNull
630         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
631             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
632             if (list == null) {
633                 list = new ArrayList<>();
634                 hashMap.put(currencyName, list);
635             }
636             return list;
637         }
638         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
639             getList(currencyName).add(item);
640         }
641         int size(@Nullable String currencyName) {
642             return this.getList(currencyName)
643                        .size();
644         }
645         Set<String> currencies() {
646             return hashMap.keySet();
647         }
648     }
649 }