diff --git a/QFXparser.Testing/ParsingTest.cs b/QFXparser.Testing/ParsingTest.cs
new file mode 100644
index 0000000..3718816
--- /dev/null
+++ b/QFXparser.Testing/ParsingTest.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Linq;
+using Xunit;
+
+namespace QFXparser.Testing
+{
+ public class ParsingTest
+ {
+ [Fact]
+ public void TestSimple()
+ {
+ var parser = new FileParser("test.qfx");
+ var statement = parser.BuildStatement();
+ var transactions = statement.Transactions.ToList();
+ Assert.Equal(3, transactions.Count);
+ Assert.Equal(-768.33m, statement.LedgerBalance.Amount);
+ Assert.Equal(new DateTime(2018, 5, 25, 0, 0, 0, DateTimeKind.Utc), statement.LedgerBalance.AsOf);
+ Assert.Equal(-27.18m, transactions[0].Amount);
+ Assert.Equal("AMAZON.COM AMZN.COM/BILL AMZN.CO", transactions[0].Memo);
+ Assert.Equal(new DateTime(2018, 4, 27, 16, 0, 0, DateTimeKind.Utc), transactions[0].PostedOn);
+ Assert.Equal(0m, transactions[2].Amount); // test for default value if invalid in .qfx
+ }
+
+ [Fact]
+ public void TestDateTimeNoTimeZoneAssumesUtc()
+ {
+ Assert.Equal(new DateTime(2018, 5, 25, 0, 0, 0, DateTimeKind.Utc), ParsingHelper.ParseDate("20180525000000"));
+ }
+
+ [Fact]
+ public void TestDateTimeWithUtcTimeZone()
+ {
+ Assert.Equal(new DateTime(2018, 5, 25, 0, 0, 0, DateTimeKind.Utc), ParsingHelper.ParseDate("20180525000000[0:UTC]"));
+ }
+
+ [Fact]
+ public void TestDateTimeWithMstTimeZone()
+ {
+ Assert.Equal(new DateTime(2018, 1, 19, 0, 0, 0, DateTimeKind.Utc), ParsingHelper.ParseDate("20180119000000.000[-7:MST]"));
+ }
+ }
+}
diff --git a/QFXparser.Testing/QFXparser.Testing.csproj b/QFXparser.Testing/QFXparser.Testing.csproj
new file mode 100644
index 0000000..4bc75c2
--- /dev/null
+++ b/QFXparser.Testing/QFXparser.Testing.csproj
@@ -0,0 +1,25 @@
+
+
+
+ netcoreapp2.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/QFXparser.Testing/test.qfx b/QFXparser.Testing/test.qfx
new file mode 100644
index 0000000..7c02972
--- /dev/null
+++ b/QFXparser.Testing/test.qfx
@@ -0,0 +1,79 @@
+OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+
+ 20180708011545[0:UTC]
+ ENG
+
+ Bank of America
+ 9876
+
+ 3456
+
+
+
+
+ 20180708011545[0:UTC]
+
+ 0
+ INFO
+
+
+ USD
+
+ 1234123409870987
+
+
+ 20180427160000[0:UTC]
+ 20180525160000[0:UTC]
+
+ PAYMENT
+ 20180427160000[0:UTC]
+ -27.18
+ 555444
+ 555444
+ REPLACE
+ AMAZON.COM AMZN.COM/BILL AMZN.CO
+ AMAZON.COM AMZN.COM/BILL AMZN.CO
+
+
+ PAYMENT
+ 20180430160000[0:UTC]
+ -25.00
+ 666876
+ 666876
+ REPLACE
+ NEW JERSEY E-ZPASS 888-288-6865
+ NEW JERSEY E-ZPASS 888-288-6865
+
+
+ CREDIT
+ 20180501160000[0:UTC]
+ 56.77invalid
+ 46576
+ 46576
+ REPLACE
+ PMT FROM BILL PAYER SERVICE
+
+
+
+ -768.33
+ 20180525000000[0:UTC]
+
+
+
+
+
\ No newline at end of file
diff --git a/QFXparser.sln b/QFXparser.sln
index ee052b0..9facaab 100644
--- a/QFXparser.sln
+++ b/QFXparser.sln
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QFXparser.TestApp", "QFXpar
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QFXparser", "QFXparser\QFXparser.csproj", "{F99BA880-DE1C-4D30-BEB3-EB0406E41955}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QFXparser.Testing", "QFXparser.Testing\QFXparser.Testing.csproj", "{AE23D849-AF0F-4231-81DD-C345C7321702}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,6 +43,18 @@ Global
{F99BA880-DE1C-4D30-BEB3-EB0406E41955}.Release|x64.Build.0 = Release|Any CPU
{F99BA880-DE1C-4D30-BEB3-EB0406E41955}.Release|x86.ActiveCfg = Release|Any CPU
{F99BA880-DE1C-4D30-BEB3-EB0406E41955}.Release|x86.Build.0 = Release|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Debug|x86.Build.0 = Debug|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Release|x64.Build.0 = Release|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE23D849-AF0F-4231-81DD-C345C7321702}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/QFXparser/AssemblyInfo.cs b/QFXparser/AssemblyInfo.cs
new file mode 100644
index 0000000..b763456
--- /dev/null
+++ b/QFXparser/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("QFXparser.Testing")]
diff --git a/QFXparser/LedgerBalance.cs b/QFXparser/LedgerBalance.cs
new file mode 100644
index 0000000..5876671
--- /dev/null
+++ b/QFXparser/LedgerBalance.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace QFXparser
+{
+ public class LedgerBalance
+ {
+ public decimal Amount { get; set; }
+
+ public DateTime AsOf { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/QFXparser/NodeType.cs b/QFXparser/NodeType.cs
index 0f3ff95..9cac71a 100644
--- a/QFXparser/NodeType.cs
+++ b/QFXparser/NodeType.cs
@@ -11,6 +11,9 @@ internal enum NodeType
TransactionOpen,
TransactionClose,
StatementProp,
- TransactionProp
+ TransactionProp,
+ LedgerBalanceOpen,
+ LedgerBalanceClose,
+ LedgerBalanceProp
}
}
diff --git a/QFXparser/ParsingHelper.cs b/QFXparser/ParsingHelper.cs
new file mode 100644
index 0000000..56bc92f
--- /dev/null
+++ b/QFXparser/ParsingHelper.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace QFXparser
+{
+ internal static class ParsingHelper
+ {
+ private static readonly Regex _dateTimeRegex = new Regex(
+ "^(?\\d{4})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d{2})(\\[(?\\d+):(?\\w{3})\\])?");
+
+ public static DateTime? ParseDate(string content)
+ {
+ DateTime? result;
+ var match = _dateTimeRegex.Match(content);
+ if (!match.Success)
+ {
+ result = null;
+ }
+ else
+ {
+ var groups = match.Groups;
+ var year = int.Parse(groups["year"].Value);
+ var month = int.Parse(groups["month"].Value);
+ var day = int.Parse(groups["day"].Value);
+ var hour = int.Parse(groups["hour"].Value);
+ var minute = int.Parse(groups["min"].Value);
+ var second = int.Parse(groups["sec"].Value);
+ var timeZone = groups["timezone"].Success ? groups["timezone"].Value : "UTC";
+
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
+ result = new DateTimeOffset(year, month, day, hour, minute, second, timeZoneInfo.BaseUtcOffset).ToUniversalTime().DateTime;
+ }
+ return result;
+ }
+ }
+}
diff --git a/QFXparser/QFXparser.cs b/QFXparser/QFXparser.cs
index 168f5c8..6442898 100644
--- a/QFXparser/QFXparser.cs
+++ b/QFXparser/QFXparser.cs
@@ -1,12 +1,14 @@
-using System.IO;
+using System;
+using System.IO;
using System.Linq;
using System.Reflection;
namespace QFXparser
{
public class FileParser
- {
+ {
private string _fileText;
+ private RawLedgerBalance _ledgerBalance;
public FileParser(string fileNamePath)
{
@@ -30,19 +32,19 @@ public Statement BuildStatement()
{
RawStatement rawStatement = BuildRaw();
- Statement statement = new Statement()
+ Statement statement = new Statement
{
AccountNum = rawStatement.AccountNum
};
foreach (var rawTrans in rawStatement.Transactions)
{
- Transaction trans = new Transaction()
+ Transaction trans = new Transaction
{
Amount = rawTrans.Amount,
Memo = rawTrans.Memo,
Name = rawTrans.Name,
- PostedOn = rawTrans.DatePosted,
+ PostedOn = rawTrans.PostedOn,
RefNumber = rawTrans.RefNumber,
TransactionId = rawTrans.TransactionId,
Type = rawTrans.Type
@@ -50,6 +52,12 @@ public Statement BuildStatement()
statement.Transactions.Add(trans);
}
+ statement.LedgerBalance = new LedgerBalance
+ {
+ Amount = rawStatement.LedgerBalance.Amount,
+ AsOf = rawStatement.LedgerBalance.AsOf
+ };
+
return statement;
}
@@ -91,6 +99,20 @@ private RawStatement BuildRaw()
case NodeType.TransactionProp:
currentMember = result.Member;
break;
+ case NodeType.LedgerBalanceOpen:
+ _ledgerBalance = new RawLedgerBalance();
+ break;
+ case NodeType.LedgerBalanceClose:
+ _statement.LedgerBalance.Amount = _ledgerBalance.Amount;
+ _statement.LedgerBalance.AsOf = _ledgerBalance.AsOf;
+ break;
+ case NodeType.LedgerBalanceProp:
+ if (_ledgerBalance == null)
+ {
+ _ledgerBalance = new RawLedgerBalance();
+ }
+ currentMember = result.Member;
+ break;
default:
break;
}
@@ -108,10 +130,13 @@ private RawStatement BuildRaw()
switch (property.DeclaringType.Name)
{
case "RawStatement":
- property.SetValue(_statement, token.Content);
+ property.SetValue(_statement, ConvertQfxType(token.Content, property.PropertyType));
break;
case "RawTransaction":
- property.SetValue(_currentTransaction, token.Content);
+ property.SetValue(_currentTransaction, ConvertQfxType(token.Content, property.PropertyType));
+ break;
+ case "RawLedgerBalance":
+ property.SetValue(_ledgerBalance, ConvertQfxType(token.Content, property.PropertyType));
break;
default:
break;
@@ -123,6 +148,27 @@ private RawStatement BuildRaw()
return _statement;
}
+ private object ConvertQfxType(string content, Type targetType)
+ {
+ object result;
+ if (targetType == typeof(DateTime))
+ {
+ result = ParsingHelper.ParseDate(content);
+ }
+ else
+ {
+ try
+ {
+ result = Convert.ChangeType(content, targetType);
+ }
+ catch (Exception)
+ {
+ result = targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
+ }
+ }
+ return result;
+ }
+
private PropertyResult GetPropertyInfo(string token)
{
var propertyResult = new PropertyResult();
@@ -155,6 +201,19 @@ private PropertyResult GetPropertyInfo(string token)
return propertyResult;
}
+ if (typeof(RawLedgerBalance).GetCustomAttribute().OpenTag == token)
+ {
+ propertyResult.Member = typeof(RawLedgerBalance);
+ propertyResult.Type = NodeType.LedgerBalanceOpen;
+ return propertyResult;
+ }
+
+ if (typeof(RawLedgerBalance).GetCustomAttribute().CloseTag == token)
+ {
+ propertyResult.Member = typeof(RawLedgerBalance);
+ propertyResult.Type = NodeType.LedgerBalanceClose;
+ return propertyResult;
+ }
var statementMember = typeof(RawStatement).GetProperties().FirstOrDefault(m => m.GetCustomAttribute().OpenTag == token);
@@ -175,9 +234,17 @@ private PropertyResult GetPropertyInfo(string token)
return propertyResult;
}
- return null;
+ var balanceMember = typeof(RawLedgerBalance).GetProperties().Where(m => m.GetCustomAttribute() != null)
+ .FirstOrDefault(m => m.GetCustomAttribute().OpenTag == token);
- }
+ if (balanceMember != null)
+ {
+ propertyResult.Member = balanceMember;
+ propertyResult.Type = NodeType.LedgerBalanceProp;
+ return propertyResult;
+ }
+ return null;
+ }
}
}
diff --git a/QFXparser/RawLedgerBalance.cs b/QFXparser/RawLedgerBalance.cs
new file mode 100644
index 0000000..385b90c
--- /dev/null
+++ b/QFXparser/RawLedgerBalance.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace QFXparser
+{
+ [NodeName("LEDGERBAL", "/LEDGERBAL")]
+ public class RawLedgerBalance
+ {
+ [NodeName("BALAMT")]
+ public decimal Amount { get; set; }
+
+ [NodeName("DTASOF")]
+ public DateTime AsOf { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/QFXparser/RawStatement.cs b/QFXparser/RawStatement.cs
index c5c1ef5..b917e79 100644
--- a/QFXparser/RawStatement.cs
+++ b/QFXparser/RawStatement.cs
@@ -12,5 +12,8 @@ internal class RawStatement
[NodeName("BANKTRANLIST")]
public ICollection Transactions { get; set; } = new List();
+
+ [NodeName("LEDGERBAL")]
+ public RawLedgerBalance LedgerBalance { get; set; } = new RawLedgerBalance();
}
}
diff --git a/QFXparser/RawTransaction.cs b/QFXparser/RawTransaction.cs
index 03e627f..68b50e8 100644
--- a/QFXparser/RawTransaction.cs
+++ b/QFXparser/RawTransaction.cs
@@ -11,10 +11,10 @@ internal class RawTransaction
public string Type { get; set; }
[NodeName("DTPOSTED")]
- public String PostedOn { get; set; } //20180119000000.000[-7:MST]
+ public DateTime PostedOn { get; set; }
[NodeName("TRNAMT")]
- public String StrAmount { get; set; }
+ public Decimal Amount { get; set; }
[NodeName("FITID")]
public string TransactionId { get; set; }
@@ -27,35 +27,5 @@ internal class RawTransaction
[NodeName("MEMO")]
public string Memo { get; set; }
-
- public DateTime DatePosted
- {
- get
- {
- var dateStr = PostedOn.Substring(0, 12) + "Z";
- Regex regex = new Regex(@"(?<=\[)([^)]+)(?=\])");
- var tzstr = regex.Match(PostedOn).Groups[0].Value;
- TimeZoneInfo tzi = TimeZoneInfo.Utc;
- if (!string.IsNullOrEmpty(tzstr))
- {
- var tzstrSplit = tzstr.Split(':');
- var timeSpan = Convert.ToDouble(tzstrSplit[0]);
- tzi = TimeZoneInfo.CreateCustomTimeZone(tzstrSplit[1], TimeSpan.FromHours(timeSpan), tzstrSplit[1], tzstrSplit[1]);
- }
- var date = DateTimeOffset.ParseExact(dateStr, "yyyyMMddHHmmZ", CultureInfo.InvariantCulture);
- var newdate = TimeZoneInfo.ConvertTime(date, tzi);
- return newdate.DateTime;
- }
- }
-
- public Decimal Amount
- {
- get
- {
- decimal amount = 0;
- Decimal.TryParse(StrAmount, out amount);
- return amount;
- }
- }
}
}
diff --git a/QFXparser/Statement.cs b/QFXparser/Statement.cs
index d51be55..45ff655 100644
--- a/QFXparser/Statement.cs
+++ b/QFXparser/Statement.cs
@@ -6,5 +6,6 @@ public class Statement
{
public string AccountNum { get; set; }
public ICollection Transactions { get; set; } = new List();
+ public LedgerBalance LedgerBalance { get; set; } = new LedgerBalance();
}
}