Added code for the csv2ledger parser that I use personally.
This commit is contained in:
parent
a42ecd5938
commit
8cebce5676
4 changed files with 354 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,6 +3,7 @@
|
||||||
*.cp
|
*.cp
|
||||||
*.dSYM
|
*.dSYM
|
||||||
*.elc
|
*.elc
|
||||||
|
*.exe
|
||||||
*.fn
|
*.fn
|
||||||
*.ky
|
*.ky
|
||||||
*.la
|
*.la
|
||||||
|
|
|
||||||
165
contrib/CSVReader.cs
Normal file
165
contrib/CSVReader.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
// This code is in the public domain. I can't remember where I found it on the Web, but it
|
||||||
|
// didn't come with any license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CSVReader {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A data-reader style interface for reading CSV files.
|
||||||
|
/// </summary>
|
||||||
|
public class CSVReader : IDisposable {
|
||||||
|
|
||||||
|
#region Private variables
|
||||||
|
|
||||||
|
private Stream stream;
|
||||||
|
private StreamReader reader;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new reader for the given stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="s">The stream to read the CSV from.</param>
|
||||||
|
public CSVReader(Stream s) : this(s, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new reader for the given stream and encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="s">The stream to read the CSV from.</param>
|
||||||
|
/// <param name="enc">The encoding used.</param>
|
||||||
|
public CSVReader(Stream s, Encoding enc) {
|
||||||
|
|
||||||
|
this.stream = s;
|
||||||
|
if (!s.CanRead) {
|
||||||
|
throw new CSVReaderException("Could not read the given CSV stream!");
|
||||||
|
}
|
||||||
|
reader = (enc != null) ? new StreamReader(s, enc) : new StreamReader(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new reader for the given text file path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The name of the file to be read.</param>
|
||||||
|
public CSVReader(string filename) : this(filename, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new reader for the given text file path and encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The name of the file to be read.</param>
|
||||||
|
/// <param name="enc">The encoding used.</param>
|
||||||
|
public CSVReader(string filename, Encoding enc)
|
||||||
|
: this(new FileStream(filename, FileMode.Open), enc) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the fields for the next row of CSV data (or null if at eof)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A string array of fields or null if at the end of file.</returns>
|
||||||
|
public string[] GetCSVLine() {
|
||||||
|
|
||||||
|
string data = reader.ReadLine();
|
||||||
|
if (data == null) return null;
|
||||||
|
if (data.Length == 0) return new string[0];
|
||||||
|
|
||||||
|
ArrayList result = new ArrayList();
|
||||||
|
|
||||||
|
ParseCSVFields(result, data);
|
||||||
|
|
||||||
|
return (string[])result.ToArray(typeof(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses the CSV fields and pushes the fields into the result arraylist
|
||||||
|
private void ParseCSVFields(ArrayList result, string data) {
|
||||||
|
|
||||||
|
int pos = -1;
|
||||||
|
while (pos < data.Length)
|
||||||
|
result.Add(ParseCSVField(data, ref pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses the field at the given position of the data, modified pos to match
|
||||||
|
// the first unparsed position and returns the parsed field
|
||||||
|
private string ParseCSVField(string data, ref int startSeparatorPosition) {
|
||||||
|
|
||||||
|
if (startSeparatorPosition == data.Length-1) {
|
||||||
|
startSeparatorPosition++;
|
||||||
|
// The last field is empty
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
int fromPos = startSeparatorPosition + 1;
|
||||||
|
|
||||||
|
// Determine if this is a quoted field
|
||||||
|
if (data[fromPos] == '"') {
|
||||||
|
// If we're at the end of the string, let's consider this a field that
|
||||||
|
// only contains the quote
|
||||||
|
if (fromPos == data.Length-1) {
|
||||||
|
fromPos++;
|
||||||
|
return "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return a string of appropriate length with double quotes collapsed
|
||||||
|
// Note that FSQ returns data.Length if no single quote was found
|
||||||
|
int nextSingleQuote = FindSingleQuote(data, fromPos+1);
|
||||||
|
startSeparatorPosition = nextSingleQuote+1;
|
||||||
|
return data.Substring(fromPos+1, nextSingleQuote-fromPos-1).Replace("\"\"", "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The field ends in the next comma or EOL
|
||||||
|
int nextComma = data.IndexOf(',', fromPos);
|
||||||
|
if (nextComma == -1) {
|
||||||
|
startSeparatorPosition = data.Length;
|
||||||
|
return data.Substring(fromPos);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
startSeparatorPosition = nextComma;
|
||||||
|
return data.Substring(fromPos, nextComma-fromPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the index of the next single quote mark in the string
|
||||||
|
// (starting from startFrom)
|
||||||
|
private int FindSingleQuote(string data, int startFrom) {
|
||||||
|
|
||||||
|
int i = startFrom-1;
|
||||||
|
while (++i < data.Length)
|
||||||
|
if (data[i] == '"') {
|
||||||
|
// If this is a double quote, bypass the chars
|
||||||
|
if (i < data.Length-1 && data[i+1] == '"') {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
// If no quote found, return the end value of i (data.Length)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the CSVReader. The underlying stream is closed.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose() {
|
||||||
|
// Closing the reader closes the underlying stream, too
|
||||||
|
if (reader != null) reader.Close();
|
||||||
|
else if (stream != null)
|
||||||
|
stream.Close(); // In case we failed before the reader was constructed
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception class for CSVReader exceptions.
|
||||||
|
/// </summary>
|
||||||
|
public class CSVReaderException : ApplicationException {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new exception object with the given message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The exception message.</param>
|
||||||
|
public CSVReaderException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
4
contrib/Makefile
Normal file
4
contrib/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
all: ParseCcStmt.exe
|
||||||
|
|
||||||
|
ParseCcStmt.exe: ParseCcStmt.cs CSVReader.cs
|
||||||
|
gmcs -out:ParseCcStmt.exe ParseCcStmt.cs CSVReader.cs
|
||||||
184
contrib/ParseCcStmt.cs
Normal file
184
contrib/ParseCcStmt.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2003-2008, John Wiegley. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are
|
||||||
|
* met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* - Neither the name of New Artisans LLC nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
using CSVReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file ParseCcStmt.cs
|
||||||
|
*
|
||||||
|
* @brief Provides a .NET way to turn a CSV report into Ledger entries.
|
||||||
|
*
|
||||||
|
* I use this code for converting the statements from my own credit card
|
||||||
|
* issuer. I realize it's strange for this to be in C#, but I wrote it
|
||||||
|
* during a phase of C# contracting. The code is solid enough now --
|
||||||
|
* and the Mono project is portable enough -- that I haven't seen the
|
||||||
|
* need to rewrite it into another language like Python.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace JohnWiegley
|
||||||
|
{
|
||||||
|
public class Transaction
|
||||||
|
{
|
||||||
|
public DateTime Date;
|
||||||
|
public DateTime PostedDate;
|
||||||
|
public string Code;
|
||||||
|
public string Payee;
|
||||||
|
public Decimal Amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IStatementConverter
|
||||||
|
{
|
||||||
|
List<Transaction> ConvertRecords(Stream s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConvertGoldMasterCardStatement : IStatementConverter
|
||||||
|
{
|
||||||
|
public List<Transaction> ConvertRecords(Stream s)
|
||||||
|
{
|
||||||
|
List<Transaction> xacts = new List<Transaction>();
|
||||||
|
|
||||||
|
using (CSVReader.CSVReader csv = new CSVReader.CSVReader(s)) {
|
||||||
|
string[] fields;
|
||||||
|
while ((fields = csv.GetCSVLine()) != null) {
|
||||||
|
if (fields[0] == "TRANSACTION DATE")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Transaction xact = new Transaction();
|
||||||
|
|
||||||
|
xact.Date = DateTime.ParseExact(fields[0], "mm/dd/yy", null);
|
||||||
|
xact.PostedDate = DateTime.ParseExact(fields[1], "mm/dd/yy", null);
|
||||||
|
xact.Payee = fields[2].Trim();
|
||||||
|
xact.Code = fields[3].Trim();
|
||||||
|
xact.Amount = Convert.ToDecimal(fields[4].Trim());
|
||||||
|
|
||||||
|
if (xact.Code.Length == 0)
|
||||||
|
xact.Code = null;
|
||||||
|
|
||||||
|
xacts.Add(xact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xacts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConvertMastercardStatement : IStatementConverter
|
||||||
|
{
|
||||||
|
public List<Transaction> ConvertRecords(Stream s)
|
||||||
|
{
|
||||||
|
List<Transaction> xacts = new List<Transaction>();
|
||||||
|
|
||||||
|
using (CSVReader.CSVReader csv = new CSVReader.CSVReader(s)) {
|
||||||
|
string[] fields;
|
||||||
|
while ((fields = csv.GetCSVLine()) != null) {
|
||||||
|
Transaction xact = new Transaction();
|
||||||
|
|
||||||
|
xact.Date = DateTime.ParseExact(fields[0], "m/dd/yyyy", null);
|
||||||
|
xact.Payee = fields[2].Trim();
|
||||||
|
xact.Code = fields[3].Trim();
|
||||||
|
xact.Amount = - Convert.ToDecimal(fields[4].Trim());
|
||||||
|
|
||||||
|
if (xact.Code.Length == 0)
|
||||||
|
xact.Code = null;
|
||||||
|
|
||||||
|
xacts.Add(xact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xacts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PrintTransactions
|
||||||
|
{
|
||||||
|
public string DefaultAccount(Transaction xact) {
|
||||||
|
if (Regex.IsMatch(xact.Payee, "IGA"))
|
||||||
|
return "Expenses:Food";
|
||||||
|
return "Expenses:Food";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Print(string AccountName, string PayAccountName,
|
||||||
|
List<Transaction> xacts)
|
||||||
|
{
|
||||||
|
foreach (Transaction xact in xacts) {
|
||||||
|
if (xact.Amount < 0) {
|
||||||
|
Console.WriteLine("{0} * {1}{2}", xact.Date.ToString("yyyy/mm/dd"),
|
||||||
|
xact.Code != null ? "(" + xact.Code + ") " : "",
|
||||||
|
xact.Payee);
|
||||||
|
Console.WriteLine(" {0,-36}{1,12}", AccountName,
|
||||||
|
"$" + (- xact.Amount).ToString());
|
||||||
|
Console.WriteLine(" {0}", PayAccountName);
|
||||||
|
} else {
|
||||||
|
Console.WriteLine("{0} {1}{2}", xact.Date.ToString("yyyy/mm/dd"),
|
||||||
|
xact.Code != null ? "(" + xact.Code + ") " : "",
|
||||||
|
xact.Payee);
|
||||||
|
Console.WriteLine(" {0,-36}{1,12}", DefaultAccount(xact),
|
||||||
|
"$" + xact.Amount.ToString());
|
||||||
|
Console.WriteLine(" * {0}", AccountName);
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParseCcStmt
|
||||||
|
{
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
StreamReader reader = new StreamReader(args[0]);
|
||||||
|
string firstLine = reader.ReadLine();
|
||||||
|
|
||||||
|
string CardAccount = args[1];
|
||||||
|
string BankAccount = args[2];
|
||||||
|
|
||||||
|
IStatementConverter converter;
|
||||||
|
|
||||||
|
if (firstLine.StartsWith("TRANSACTION DATE")) {
|
||||||
|
converter = new ConvertGoldMasterCardStatement();
|
||||||
|
} else {
|
||||||
|
converter = new ConvertMastercardStatement();
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = new StreamReader(args[0]);
|
||||||
|
List<Transaction> xacts = converter.ConvertRecords(reader.BaseStream);
|
||||||
|
|
||||||
|
PrintTransactions printer = new PrintTransactions();
|
||||||
|
printer.Print(CardAccount, BankAccount, xacts);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue