View Javadoc
1   /*
2    * MoneyWise: Finance Application
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.moneywise.quicken.file;
18  
19  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseCategoryBase;
20  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataSet;
21  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDeposit;
22  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDeposit.MoneyWiseDepositList;
23  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePayee;
24  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio;
25  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity;
26  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityPrice;
27  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityPrice.MoneyWiseSecurityPriceList;
28  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
29  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
30  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransTag;
31  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
32  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction.MoneyWiseTransactionList;
33  import io.github.tonywasher.joceanus.moneywise.lethe.data.analysis.data.MoneyWiseAnalysis;
34  import io.github.tonywasher.joceanus.moneywise.quicken.definitions.MoneyWiseQIFPreference.MoneyWiseQIFPreferenceKey;
35  import io.github.tonywasher.joceanus.moneywise.quicken.definitions.MoneyWiseQIFPreference.MoneyWiseQIFPreferences;
36  import io.github.tonywasher.joceanus.moneywise.quicken.definitions.MoneyWiseQIFType;
37  import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
38  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusMoney;
39  
40  import java.util.ArrayList;
41  import java.util.HashMap;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Objects;
46  
47  /**
48   * QIF File representation.
49   */
50  public class MoneyWiseQIFFile
51          implements MoneyWiseQIFRegister {
52      /**
53       * Holding suffix.
54       */
55      protected static final String HOLDING_SUFFIX = "Holding";
56  
57      /**
58       * Type of file.
59       */
60      private final MoneyWiseQIFType theFileType;
61  
62      /**
63       * Start event Date.
64       */
65      private OceanusDate theStartDate;
66  
67      /**
68       * Last event Date.
69       */
70      private OceanusDate theLastDate;
71  
72      /**
73       * Map of Accounts with Events.
74       */
75      private final Map<String, MoneyWiseQIFAccountEvents> theAccountMap;
76  
77      /**
78       * Sorted List of Accounts with Events.
79       */
80      private final List<MoneyWiseQIFAccountEvents> theAccounts;
81  
82      /**
83       * Map of Payees.
84       */
85      private final Map<String, MoneyWiseQIFPayee> thePayeeMap;
86  
87      /**
88       * Sorted List of Payees.
89       */
90      private final List<MoneyWiseQIFPayee> thePayees;
91  
92      /**
93       * Map of Securities with Prices.
94       */
95      private final Map<String, MoneyWiseQIFSecurityPrices> theSecurityMap;
96  
97      /**
98       * Sorted List of Securities with Prices.
99       */
100     private final List<MoneyWiseQIFSecurityPrices> theSecurities;
101 
102     /**
103      * Map of Symbols to Securities.
104      */
105     private final Map<String, MoneyWiseQIFSecurity> theSymbolMap;
106 
107     /**
108      * Map of Parent Categories.
109      */
110     private final Map<String, MoneyWiseQIFParentCategory> theParentMap;
111 
112     /**
113      * Sorted List of Parent Categories.
114      */
115     private final List<MoneyWiseQIFParentCategory> theParentCategories;
116 
117     /**
118      * Map of Categories.
119      */
120     private final Map<String, MoneyWiseQIFEventCategory> theCategories;
121 
122     /**
123      * Map of Classes.
124      */
125     private final Map<String, MoneyWiseQIFClass> theClassMap;
126 
127     /**
128      * Sorted List of Classes.
129      */
130     private final List<MoneyWiseQIFClass> theClasses;
131 
132     /**
133      * Constructor.
134      *
135      * @param pType the file type
136      */
137     public MoneyWiseQIFFile(final MoneyWiseQIFType pType) {
138         /* Store file type */
139         theFileType = pType;
140 
141         /* Allocate maps */
142         theAccountMap = new HashMap<>();
143         thePayeeMap = new HashMap<>();
144         theSecurityMap = new HashMap<>();
145         theSymbolMap = new HashMap<>();
146         theParentMap = new HashMap<>();
147         theCategories = new HashMap<>();
148         theClassMap = new HashMap<>();
149 
150         /* Allocate maps */
151         theAccounts = new ArrayList<>();
152         thePayees = new ArrayList<>();
153         theSecurities = new ArrayList<>();
154         theParentCategories = new ArrayList<>();
155         theClasses = new ArrayList<>();
156     }
157 
158     @Override
159     public MoneyWiseQIFType getFileType() {
160         return theFileType;
161     }
162 
163     /**
164      * Does the file have classes?
165      *
166      * @return true/false
167      */
168     protected boolean hasClasses() {
169         return !theClasses.isEmpty();
170     }
171 
172     /**
173      * Obtain the number of class.
174      *
175      * @return the number
176      */
177     protected int numClasses() {
178         return theClasses.size();
179     }
180 
181     /**
182      * Obtain the classes iterator.
183      *
184      * @return the iterator
185      */
186     protected Iterator<MoneyWiseQIFClass> classIterator() {
187         return theClasses.iterator();
188     }
189 
190     /**
191      * Obtain the number of categories.
192      *
193      * @return the number
194      */
195     protected int numCategories() {
196         return theCategories.size();
197     }
198 
199     /**
200      * Obtain the category iterator.
201      *
202      * @return the iterator
203      */
204     protected Iterator<MoneyWiseQIFParentCategory> categoryIterator() {
205         return theParentCategories.iterator();
206     }
207 
208     /**
209      * Obtain the number of accounts.
210      *
211      * @return the number
212      */
213     protected int numAccounts() {
214         return theAccounts.size();
215     }
216 
217     /**
218      * Obtain the account iterator.
219      *
220      * @return the iterator
221      */
222     protected Iterator<MoneyWiseQIFAccountEvents> accountIterator() {
223         return theAccounts.iterator();
224     }
225 
226     /**
227      * Does the file have securities?
228      *
229      * @return true/false
230      */
231     protected boolean hasSecurities() {
232         return !theSecurities.isEmpty();
233     }
234 
235     /**
236      * Obtain the number of securities.
237      *
238      * @return the number
239      */
240     protected int numSecurities() {
241         return theSecurities.size();
242     }
243 
244     /**
245      * Obtain the account iterator.
246      *
247      * @return the iterator
248      */
249     protected Iterator<MoneyWiseQIFSecurityPrices> securityIterator() {
250         return theSecurities.iterator();
251     }
252 
253     /**
254      * Sort the lists.
255      */
256     protected void sortLists() {
257         /* Sort the classes */
258         theClasses.sort(null);
259 
260         /* Sort the payees */
261         thePayees.sort(null);
262 
263         /* Sort the categories */
264         theParentCategories.sort(null);
265         final Iterator<MoneyWiseQIFParentCategory> myCatIterator = categoryIterator();
266         while (myCatIterator.hasNext()) {
267             final MoneyWiseQIFParentCategory myParent = myCatIterator.next();
268 
269             /* Sort the children */
270             myParent.sortChildren();
271         }
272 
273         /* Sort the securities */
274         theSecurities.sort(null);
275         final Iterator<MoneyWiseQIFSecurityPrices> mySecIterator = securityIterator();
276         while (mySecIterator.hasNext()) {
277             final MoneyWiseQIFSecurityPrices mySecurity = mySecIterator.next();
278 
279             /* Sort the prices */
280             mySecurity.sortPrices();
281         }
282 
283         /* Sort the accounts */
284         theAccounts.sort(null);
285         final Iterator<MoneyWiseQIFAccountEvents> myAccIterator = accountIterator();
286         while (myAccIterator.hasNext()) {
287             final MoneyWiseQIFAccountEvents myAccount = myAccIterator.next();
288 
289             /* Sort the events */
290             myAccount.sortEvents();
291         }
292     }
293 
294     /**
295      * Build QIF File from data.
296      *
297      * @param pData        the data
298      * @param pAnalysis    the analysis
299      * @param pPreferences the preferences
300      * @return the QIF File
301      */
302     public static MoneyWiseQIFFile buildQIFFile(final MoneyWiseDataSet pData,
303                                                 final MoneyWiseAnalysis pAnalysis,
304                                                 final MoneyWiseQIFPreferences pPreferences) {
305         /* Access preference details */
306         final MoneyWiseQIFType myType = pPreferences.getEnumValue(MoneyWiseQIFPreferenceKey.QIFTYPE, MoneyWiseQIFType.class);
307         final OceanusDate myLastDate = pPreferences.getDateValue(MoneyWiseQIFPreferenceKey.LASTEVENT);
308 
309         /* Create new QIF File */
310         final MoneyWiseQIFFile myFile = new MoneyWiseQIFFile(myType);
311 
312         /* Build the data for the accounts */
313         myFile.buildData(pData, pAnalysis, myLastDate);
314         myFile.sortLists();
315 
316         /* Return the QIF File */
317         return myFile;
318     }
319 
320     @Override
321     public MoneyWiseQIFClass registerClass(final MoneyWiseTransTag pClass) {
322         /* Locate an existing class */
323         final String myName = pClass.getName();
324         return theClassMap.computeIfAbsent(myName, n -> {
325             final MoneyWiseQIFClass myClass = new MoneyWiseQIFClass(pClass);
326             theClasses.add(myClass);
327             return myClass;
328         });
329     }
330 
331     @Override
332     public void registerClass(final MoneyWiseQIFClass pClass) {
333         /* Locate an existing class */
334         final String myName = pClass.getName();
335         theClassMap.computeIfAbsent(myName, n -> {
336             theClasses.add(pClass);
337             return pClass;
338         });
339     }
340 
341     @Override
342     public MoneyWiseQIFEventCategory registerCategory(final MoneyWiseTransCategory pCategory) {
343         /* Locate an existing category */
344         final String myName = pCategory.getName();
345         return theCategories.computeIfAbsent(myName, n -> {
346             final MoneyWiseQIFEventCategory myCat = new MoneyWiseQIFEventCategory(pCategory);
347             registerCategoryToParent(pCategory.getParentCategory(), myCat);
348             return myCat;
349         });
350     }
351 
352     /**
353      * Register parent category.
354      *
355      * @param pParent   the parent category
356      * @param pCategory the QIFEventCategory to register
357      */
358     private void registerCategoryToParent(final MoneyWiseTransCategory pParent,
359                                           final MoneyWiseQIFEventCategory pCategory) {
360         /* Locate an existing parent category */
361         final String myName = pParent.getName();
362         final MoneyWiseQIFParentCategory myParent = theParentMap.computeIfAbsent(myName, n -> {
363             final MoneyWiseQIFParentCategory myParCat = new MoneyWiseQIFParentCategory(pParent);
364             theParentCategories.add(myParCat);
365             return myParCat;
366         });
367 
368         /* Register the category */
369         myParent.registerChild(pCategory);
370     }
371 
372     /**
373      * Register category.
374      *
375      * @param pCategory the category
376      */
377     public void registerCategory(final MoneyWiseQIFEventCategory pCategory) {
378         /* Locate an existing category */
379         final String myName = pCategory.getName();
380         final MoneyWiseQIFEventCategory myCat = theCategories.get(myName);
381         if (myCat == null) {
382             /* Locate parent separator */
383             final int myPos = myName.indexOf(MoneyWiseCategoryBase.STR_SEP);
384 
385             /* If this is a parent category */
386             if (myPos < 0) {
387                 /* Create the new Parent Category */
388                 final MoneyWiseQIFParentCategory myParent = new MoneyWiseQIFParentCategory(pCategory);
389                 theParentMap.put(myName, myParent);
390                 theParentCategories.add(myParent);
391 
392                 /* else this is a standard category */
393             } else {
394                 /* Register the new category */
395                 theCategories.put(myName, pCategory);
396 
397                 /* Determine parent name */
398                 final String myParentName = myName.substring(0, myPos);
399 
400                 /* Locate an existing parent category */
401                 final MoneyWiseQIFParentCategory myParent = theParentMap.get(myParentName);
402 
403                 /* Register against parent */
404                 myParent.registerChild(pCategory);
405             }
406         }
407     }
408 
409     @Override
410     public MoneyWiseQIFAccountEvents registerAccount(final MoneyWiseTransAsset pAccount) {
411         /* Locate an existing account */
412         final String myName = pAccount.getName();
413         return theAccountMap.computeIfAbsent(myName, n -> {
414             final MoneyWiseQIFAccountEvents myAccount = new MoneyWiseQIFAccountEvents(pAccount);
415             theAccounts.add(myAccount);
416             return myAccount;
417         });
418     }
419 
420     /**
421      * Register holding account.
422      *
423      * @param pPortfolio the portfolio
424      * @return the QIFAccount representation
425      */
426     public MoneyWiseQIFAccountEvents registerHoldingAccount(final MoneyWisePortfolio pPortfolio) {
427         /* Locate an existing account */
428         final String myName = pPortfolio.getName() + HOLDING_SUFFIX;
429         return theAccountMap.computeIfAbsent(myName, n -> {
430             final MoneyWiseQIFAccountEvents myAccount = new MoneyWiseQIFAccountEvents(myName);
431             theAccounts.add(myAccount);
432             return myAccount;
433         });
434     }
435 
436     /**
437      * Register account.
438      *
439      * @param pAccount the account
440      * @return the QIFAccount representation
441      */
442     public MoneyWiseQIFAccountEvents registerAccount(final MoneyWiseQIFAccount pAccount) {
443         /* Locate an existing account */
444         final String myName = pAccount.getName();
445         return theAccountMap.computeIfAbsent(myName, n -> {
446             final MoneyWiseQIFAccountEvents myAccount = new MoneyWiseQIFAccountEvents(pAccount);
447             theAccounts.add(myAccount);
448             return myAccount;
449         });
450     }
451 
452     @Override
453     public MoneyWiseQIFPayee registerPayee(final MoneyWisePayee pPayee) {
454         /* Locate an existing payee */
455         final String myName = pPayee.getName();
456         return thePayeeMap.computeIfAbsent(myName, n -> {
457             final MoneyWiseQIFPayee myPayee = new MoneyWiseQIFPayee(pPayee);
458             thePayees.add(myPayee);
459             return myPayee;
460         });
461     }
462 
463     @Override
464     public MoneyWiseQIFPayee registerPayee(final String pPayee) {
465         /* Locate an existing payee */
466         return thePayeeMap.computeIfAbsent(pPayee, n -> {
467             final MoneyWiseQIFPayee myPayee = new MoneyWiseQIFPayee(pPayee);
468             thePayees.add(myPayee);
469             return myPayee;
470         });
471     }
472 
473     @Override
474     public MoneyWiseQIFSecurity registerSecurity(final MoneyWiseSecurity pSecurity) {
475         /* Locate an existing security */
476         final String myName = pSecurity.getName();
477         final MoneyWiseQIFSecurityPrices mySecurity = theSecurityMap.computeIfAbsent(myName, n -> {
478             final MoneyWiseQIFSecurityPrices mySec = new MoneyWiseQIFSecurityPrices(this, pSecurity);
479             theSymbolMap.put(pSecurity.getSymbol(), mySec.getSecurity());
480             theSecurities.add(mySec);
481             return mySec;
482         });
483 
484         /* Return the security */
485         return mySecurity.getSecurity();
486     }
487 
488     /**
489      * Register security.
490      *
491      * @param pSecurity the security
492      */
493     public void registerSecurity(final MoneyWiseQIFSecurity pSecurity) {
494         /* Locate an existing security */
495         final String myName = pSecurity.getName();
496         theSecurityMap.computeIfAbsent(myName, n -> {
497             final MoneyWiseQIFSecurityPrices mySecurity = new MoneyWiseQIFSecurityPrices(this, pSecurity);
498             theSymbolMap.put(pSecurity.getSymbol(), mySecurity.getSecurity());
499             theSecurities.add(mySecurity);
500             return mySecurity;
501         });
502     }
503 
504     /**
505      * Register price.
506      *
507      * @param pPrice the price
508      */
509     public void registerPrice(final MoneyWiseSecurityPrice pPrice) {
510         /* Locate an existing security price list */
511         final MoneyWiseSecurity mySecurity = pPrice.getSecurity();
512         final MoneyWiseQIFSecurityPrices mySecurityList = theSecurityMap.get(mySecurity.getName());
513         if (mySecurityList != null) {
514             /* Add price to the list */
515             mySecurityList.addPrice(pPrice);
516         }
517     }
518 
519     /**
520      * Register price.
521      *
522      * @param pPrice the price
523      */
524     public void registerPrice(final MoneyWiseQIFPrice pPrice) {
525         /* Locate an existing security price list */
526         final MoneyWiseQIFSecurity mySecurity = pPrice.getSecurity();
527         final MoneyWiseQIFSecurityPrices mySecurityList = theSecurityMap.get(mySecurity.getName());
528         if (mySecurityList != null) {
529             /* Loop through the prices */
530             final Iterator<MoneyWiseQIFPrice> myIterator = pPrice.priceIterator();
531             while (myIterator.hasNext()) {
532                 final MoneyWiseQIFPrice myPrice = myIterator.next();
533 
534                 /* Add price to the list */
535                 mySecurityList.addPrice(myPrice);
536             }
537         }
538     }
539 
540     @Override
541     public MoneyWiseQIFEventCategory getCategory(final String pName) {
542         /* Lookup the category */
543         return theCategories.get(pName);
544     }
545 
546     @Override
547     public MoneyWiseQIFAccount getAccount(final String pName) {
548         /* Lookup the security */
549         final MoneyWiseQIFAccountEvents myAccount = getAccountEvents(pName);
550         return (myAccount == null)
551                 ? null
552                 : myAccount.getAccount();
553     }
554 
555     /**
556      * Obtain account events.
557      *
558      * @param pName the name of the account
559      * @return the account
560      */
561     protected MoneyWiseQIFAccountEvents getAccountEvents(final String pName) {
562         /* Lookup the account */
563         return theAccountMap.get(pName);
564     }
565 
566     @Override
567     public MoneyWiseQIFSecurity getSecurity(final String pName) {
568         /* Lookup the security */
569         final MoneyWiseQIFSecurityPrices myList = getSecurityPrices(pName);
570         return myList == null
571                 ? null
572                 : myList.getSecurity();
573     }
574 
575     @Override
576     public MoneyWiseQIFSecurity getSecurityBySymbol(final String pSymbol) {
577         /* Lookup the security */
578         return theSymbolMap.get(pSymbol);
579     }
580 
581     /**
582      * Obtain security prices.
583      *
584      * @param pName the name of the security
585      * @return the security
586      */
587     protected MoneyWiseQIFSecurityPrices getSecurityPrices(final String pName) {
588         /* Lookup the security */
589         return theSecurityMap.get(pName);
590     }
591 
592     @Override
593     public MoneyWiseQIFClass getClass(final String pName) {
594         /* Lookup the class */
595         return theClassMap.get(pName);
596     }
597 
598     /**
599      * Build data.
600      *
601      * @param pData     the data
602      * @param pAnalysis the analysis
603      * @param pLastDate the last date
604      */
605     public void buildData(final MoneyWiseDataSet pData,
606                           final MoneyWiseAnalysis pAnalysis,
607                           final OceanusDate pLastDate) {
608         /* Create a builder */
609         final MoneyWiseQIFBuilder myBuilder = new MoneyWiseQIFBuilder(this, pData, pAnalysis);
610 
611         /* Store dates */
612         theStartDate = pData.getDateRange().getStart();
613         theLastDate = pLastDate;
614 
615         /* Build opening balances */
616         buildOpeningBalances(myBuilder, pData.getDeposits());
617 
618         /* Loop through the events */
619         final MoneyWiseTransactionList myEvents = pData.getTransactions();
620         final Iterator<MoneyWiseTransaction> myIterator = myEvents.iterator();
621         while (myIterator.hasNext()) {
622             final MoneyWiseTransaction myEvent = myIterator.next();
623 
624             /* Break loop if the event is too late */
625             final OceanusDate myDate = myEvent.getDate();
626             if (myDate.compareTo(pLastDate) > 0) {
627                 break;
628             }
629 
630             /* Process the event */
631             myBuilder.processEvent(myEvent);
632         }
633 
634         /* Build prices for securities */
635         buildPrices(pData.getSecurityPrices());
636     }
637 
638     /**
639      * Build opening balances.
640      *
641      * @param pBuilder     the builder
642      * @param pDepositList the deposit list
643      */
644     private void buildOpeningBalances(final MoneyWiseQIFBuilder pBuilder,
645                                       final MoneyWiseDepositList pDepositList) {
646         /* Loop through the prices */
647         final Iterator<MoneyWiseDeposit> myIterator = pDepositList.iterator();
648         while (myIterator.hasNext()) {
649             final MoneyWiseDeposit myDeposit = myIterator.next();
650 
651             /* Ignore if no opening balance */
652             final OceanusMoney myBalance = myDeposit.getOpeningBalance();
653             if (myBalance == null) {
654                 continue;
655             }
656 
657             /* Process the balance */
658             pBuilder.processBalance(myDeposit, theStartDate, myBalance);
659         }
660     }
661 
662     /**
663      * Build prices.
664      *
665      * @param pPriceList the price list
666      */
667     private void buildPrices(final MoneyWiseSecurityPriceList pPriceList) {
668         /* Loop through the prices */
669         final Iterator<MoneyWiseSecurityPrice> myIterator = pPriceList.iterator();
670         while (myIterator.hasNext()) {
671             final MoneyWiseSecurityPrice myPrice = myIterator.next();
672 
673             /* Break loop if the price is too late */
674             final OceanusDate myDate = myPrice.getDate();
675             if (myDate.compareTo(theLastDate) > 0) {
676                 break;
677             }
678 
679             /* Register the price */
680             registerPrice(myPrice);
681         }
682     }
683 
684     @Override
685     public boolean equals(final Object pThat) {
686         /* Handle trivial cases */
687         if (this == pThat) {
688             return true;
689         }
690         if (pThat == null) {
691             return false;
692         }
693 
694         /* Check class */
695         if (!(pThat instanceof MoneyWiseQIFFile myThat)) {
696             return false;
697         }
698 
699         /* Check file type */
700         if (!theFileType.equals(myThat.theFileType)) {
701             return false;
702         }
703 
704         /* Check class list */
705         if (!theClasses.equals(myThat.theClasses)) {
706             return false;
707         }
708 
709         /* Check parent categories */
710         if (!theParentCategories.equals(myThat.theParentCategories)) {
711             return false;
712         }
713 
714         /* Check securities list */
715         if (!theSecurities.equals(myThat.theSecurities)) {
716             return false;
717         }
718 
719         /* Check payees list */
720         if (!thePayees.equals(myThat.thePayees)) {
721             return false;
722         }
723 
724         /* Check accounts */
725         return theAccounts.equals(myThat.theAccounts);
726     }
727 
728     @Override
729     public int hashCode() {
730         return Objects.hash(theFileType, theClasses, theParentCategories, theSecurities, thePayees, theAccounts);
731     }
732 }