Software Is Hardwork

ISimplicityAffinative: The endless pursuit of anti-complexity.
The technology-centric blog of D. P. Bullington.

Email D. P. Bullington View D. P. Bullington\ Follow D. P. Bullington on Twitter Get Software Is Hardwork code on CodePlex

Blog Post(s)

QIF Parser Code
Thursday, August 21, 2008

I recently converted from MS Money to a simple MS Access database. MS Money has become useless to me; it does not run on Vista SP1/Server 2008 reliably due to DEP issues. I used the following code to parse a QIF file exported from MS Money which can format as a tab separated values file:

/*
    Copyright ©2002-2008 D. P. Bullington
    Distributed under the MIT license: http://www.opensource.org/licenses/mit-license.php
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace SoftwareIsHardwork.Tools.QifConvUtil
{
    public class NonInvestmentAccount
    {
        #region Constructors/Destructors

        public NonInvestmentAccount()
        {
        }

        #endregion

        #region Fields/Constants

        private readonly IList<NonInvestmentAccountTransaction> transactions = new List<NonInvestmentAccountTransaction>();

        #endregion

        #region Properties/Indexers/Events

        public double Balance
        {
            get
            {
                double balanace = 0.0;

                foreach (NonInvestmentAccountTransaction transaction in this.transactions)
                    balanace += transaction.Amount ?? 0.0;

                return balanace;
            }
        }

        public int Count
        {
            get
            {
                return this.transactions.Count;
            }
        }

        public IEnumerable<NonInvestmentAccountTransaction> Transactions
        {
            get
            {
                return this.transactions;
            }
        }

        #endregion

        #region Methods/Operators

        public void PostTransaction(NonInvestmentAccountTransaction transaction)
        {
            if (transaction == null)
                throw new ArgumentNullException("transaction");

            if (this.transactions.Contains(transaction))
                throw new InvalidOperationException("Transaction already exists in account.");

            this.transactions.Add(transaction);
        }

        #endregion
    }

    public class NonInvestmentAccountTransaction
    {
        #region Constructors/Destructors

        public NonInvestmentAccountTransaction(double? amount,
                                               string category,
                                               bool? cleared,
                                               DateTime? date,
                                               string memo,
                                               string number,
                                               string payee)
        {
            this.amount = amount;
            this.category = category;
            this.cleared = cleared;
            this.date = date;
            this.memo = memo;
            this.number = number;
            this.payee = payee;
        }

        #endregion

        #region Fields/Constants

        private readonly double? amount;
        private readonly string category;
        private readonly bool? cleared;
        private readonly DateTime? date;
        private readonly string memo;
        private readonly string number;
        private readonly string payee;

        #endregion

        #region Properties/Indexers/Events

        public double? Amount
        {
            get
            {
                return this.amount;
            }
        }

        public string Category
        {
            get
            {
                return this.category;
            }
        }

        public bool? Cleared
        {
            get
            {
                return this.cleared;
            }
        }

        public DateTime? Date
        {
            get
            {
                return this.date;
            }
        }

        public string Memo
        {
            get
            {
                return this.memo;
            }
        }

        public string Number
        {
            get
            {
                return this.number;
            }
        }

        public string Payee
        {
            get
            {
                return this.payee;
            }
        }

        #endregion
    }

    public static class QifParser
    {
        #region Methods/Operators

        public static void FormatToTsvFile(NonInvestmentAccount account, string filePath)
        {
            StreamWriter swa, swp, swc;
            string line;
            Dictionary<string, object> payees, categories;

            if (account == null)
                throw new ArgumentNullException("account");

            payees = new Dictionary<string, object>();
            categories = new Dictionary<string, object>();

            using (swa = new StreamWriter(new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 32, FileOptions.None), Encoding.ASCII))
            {
                foreach (NonInvestmentAccountTransaction transaction in account.Transactions)
                {
                    if (!payees.ContainsKey(transaction.Payee ?? ""))
                        payees.Add(transaction.Payee ?? "", null);

                    if (!categories.ContainsKey(transaction.Category ?? ""))
                        categories.Add(transaction.Category ?? "", null);

                    line = string.Format("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\n",
                                         transaction.Number, transaction.Payee, transaction.Category, transaction.Date.Value.ToString("MM/dd/yyyy"),
                                         (transaction.Amount ?? 0).ToString("n"), transaction.Cleared ?? false, transaction.Memo);
                    swa.Write(line);
                }

                using (swp = new StreamWriter(new FileStream(filePath + ".pay", FileMode.Create, FileAccess.Write, FileShare.None, 32, FileOptions.None), Encoding.ASCII))
                {
                    foreach (KeyValuePair<string, object> payee in payees)
                        swp.WriteLine(payee.Key);
                }

                using (swc = new StreamWriter(new FileStream(filePath + ".cat", FileMode.Create, FileAccess.Write, FileShare.None, 32, FileOptions.None), Encoding.ASCII))
                {
                    foreach (KeyValuePair<string, object> category in categories)
                        swc.WriteLine(category.Key);
                }
            }
        }

        public static NonInvestmentAccount ParseNonInvestmentAccountFile(string filePath)
        {
            StreamReader sr;
            string line;
            NonInvestmentAccount account;
            NonInvestmentAccountTransaction transaction;
            Dictionary<string, object> context;
            const string QIF_HEADER = "!Type:Bank";
            const string QIF_TX_TERM = "^";
            const string QIF_CLRDIND = "*";

            const string QIF_KEY_DATE = "D";
            const string QIF_KEY_AMOUNT = "T";
            const string QIF_KEY_CLEARED = "C";
            const string QIF_KEY_NUMBER = "N";
            const string QIF_KEY_PAYEE = "P";
            const string QIF_KEY_MEMO = "M";
            const string QIF_KEY_CATEGORY = "L";

            account = new NonInvestmentAccount();
            context = new Dictionary<string, object>();

            using (sr = new StreamReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 32, FileOptions.None), Encoding.ASCII))
            {
                if ((line = sr.ReadLine() ?? "") != "")
                {
                    if (line != QIF_HEADER)
                        throw new InvalidOperationException("Invalid QIF header: " + line);
                }

                while ((line = sr.ReadLine() ?? "") != "")
                {
                    if (line == QIF_TX_TERM)
                    {
                        if (context.Count == 0)
                            throw new InvalidOperationException("Invalid QIF transaction context");

                        transaction = new NonInvestmentAccountTransaction(
                            context.ContainsKey(QIF_KEY_AMOUNT) ? (double?)context[QIF_KEY_AMOUNT] : null,
                            context.ContainsKey(QIF_KEY_CATEGORY) ? (string)context[QIF_KEY_CATEGORY] : null,
                            context.ContainsKey(QIF_KEY_CLEARED) ? (bool?)context[QIF_KEY_CLEARED] : null,
                            context.ContainsKey(QIF_KEY_DATE) ? (DateTime?)context[QIF_KEY_DATE] : null,
                            context.ContainsKey(QIF_KEY_MEMO) ? (string)context[QIF_KEY_MEMO] : null,
                            context.ContainsKey(QIF_KEY_NUMBER) ? (string)context[QIF_KEY_NUMBER] : null,
                            context.ContainsKey(QIF_KEY_PAYEE) ? (string)context[QIF_KEY_PAYEE] : null);

                        account.PostTransaction(transaction);

                        context.Clear();
                        continue;
                    }
                    else
                    {
                        string key;
                        object value;

                        if (line.Length < 2)
                            throw new InvalidOperationException("Invalid QIF item length: " + line);

                        key = line[0].ToString();
                        line = line.Substring(1);

                        switch (key)
                        {
                            case QIF_KEY_AMOUNT:
                                value = double.Parse(line);
                                break;

                            case QIF_KEY_CATEGORY:
                                value = line.ToUpper();
                                break;

                            case QIF_KEY_CLEARED:
                                value = line == QIF_CLRDIND;
                                break;

                            case QIF_KEY_DATE:
                                value = DateTime.Parse(line.Replace("'", "/"));
                                break;

                            case QIF_KEY_MEMO:
                                value = line.ToUpper();
                                break;

                            case QIF_KEY_NUMBER:
                                value = line.ToUpper();
                                break;

                            case QIF_KEY_PAYEE:
                                value = line.ToUpper();
                                break;

                            default:
                                throw new InvalidOperationException("Invalid QIF item key: " + key);
                        }

                        context.Add(key, value);
                    }
                }
            }

            return account;
        }

        #endregion
    }
}

0 comments:

Speaking Enagements

  • 11/18/2010 | Charlottesville .NET Users Group | Charlottesville, VA | Topic TBD
  • 09/14/2010 | Hampton Roads .NET Users Group | Cheaspeake, VA | Topic TBD
  • 07/01/2010 | Richmond .NET Users Group | Richmond, VA | Topic TBD
  • (past) 12/08/2009 | Hampton Roads .NET Users Group | Cheaspeake, VA | SharePoint Antithesis - A Case Study in Pragmatic Software Architecture and Engineering Processes
  • (past) 10/04/2009 | Richmond Code Camp 2009.2 | Richmond, VA | Soothing the Pain Points: Data Access, Validation, Rules, UI, Presentation, et. al
  • (past) 07/23/2009 | Charlottesville .NET Users Group | Charlottesville, VA | Debugging on the Windows Platform
  • (past) 05/23/2008 | NoVa CodeCamp 2009.01 | Reston, VA | Going Proxy-less - The WCF Proxy Factory
  • (past) 04/25/2009 | Richmond Code Camp 2009.1 | Richmond, VA | Software Programmer to Software Engineer: Concepts to Span the Divide
  • (past) 02/05/2009 | Richmond .NET Users Group | Richmond, VA | Debugging on the Windows Platform
  • (past) 10/04/2008 | Richmond Code Camp 2008.2 | Richmond, VA | Going Proxy-less - The WCF Proxy Factory

Blog Archive

Post Labels

.NET (64) .NETv4.0 (3) ACID (1) ActiveDirectory (1) ADF (2) Affiliate (1) Agile (6) AJAX (1) Allocator (3) Analysis (1) AOP (4) ASP.NET (6) ASP.NET MVC (1) Assembly (2) BadIdeaPile (1) BagOfBolts (5) Blogger (1) Books (2) BuildMgmt (8) C# (46) ChoDNUG (1) CLR (1) CLRv4.0 (2) CMP (1) CMS (2) CodeCamp (2) COM (1) Conversation (1) Coverage (1) CUI (1) Database (2) DDD (1) DeadFxs (1) Debugging (9) Design (4) DevAuto (3) DevCfg (1) Development (118) DI (6) DiffMerge (1) Domain (1) DTfW (2) EclipseIDE (1) ECM (1) EntityFramework (1) Estimating (1) FileShare (1) Frameworks (7) GAC (2) Google (1) Hardware (2) HRNUG (1) Humor (6) IIS (4) ILDASM (1) Impersonation (2) InstallError (1) IoC (6) KingTodd (1) LinkedIn (1) LINQtoSQL (2) MarketingHype (1) MBUnit (1) Mentoring (22) Metadata (1) Microsoft (7) MOSS2007 (5) MSBuild (2) MSIL (4) MSSCCI (2) NAnt (2) NCore (2) NCover (1) NDatabase (4) NetUse (1) NHibernate (2) NoVaCodeCamp (2) NTSD/CDB (2) NUnit (1) Observation (2) Office (2) OOD (7) OOP (6) OpenSource (14) Opinion (19) Personal (3) PMP (1) Polymorphism (1) PowerPoint (1) PowerShell (2) Presentation (3) Process (4) ProjectManagement (2) PublicKeyToken (1) QA (2) RDNUG (1) Reflection (2) Registry (2) Resharper (1) Reversing (2) RichmondCodeCamp (5) SCM (11) Scrum (5) Security (2) Series (3) Server2008 (4) ServicePack (1) SES (7) SharePoint (7) Silverlight (1) SoC (3) Software (49) SoftwareIsHardwork (17) Speaking (7) SQL (2) SSO (2) StrongName (2) Suite2008 (9) Suite2010 (1) SwEng (19) TechBlunder (1) Testing (14) Thread (3) Tools (8) Troubleshooting (10) Twitter (3) Types (2) UAC (1) UIP (1) Vault (2) VB6 (1) VC (1) Vista (3) VisualStudio (15) VSIP (2) VSTS (1) WCF (4) Web (4) WebForms (1) Win32 (3) WinDBG (3) WindowsIdentity (3) WinForms (1) WIT (1) Workhorse (1) WoW64 (1) WPF (1) WSS3 (2) x64 (2) x86 (2) xUnit (1)

Disclaimer

© D. P. Bullington, all rights reserved. Everything posted on this blog is my personal opinion and does not represent the views of my employer nor serves to infringe on the sovereignty of any nation.