diff --git a/OFX-to-XLSX/.gitignore b/OFX-to-XLSX/.gitignore
new file mode 100644
index 0000000..8a41c8b
--- /dev/null
+++ b/OFX-to-XLSX/.gitignore
@@ -0,0 +1,54 @@
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio cache/options directory
+.vs/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# Click-Once directory
+[Pp]ublish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+
+# The packages folder can be ignored because of Package Restore except build/, which is used as an MSBuild target.
+**/[Pp]ackages/*
+!**/[Pp]ackages/build/
+
+# XML Documentation file
+Easy-Core.xml
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Samples/1.ofx b/OFX-to-XLSX/Documentation/OFX_102_Samples/1.ofx
new file mode 100644
index 0000000..3897c5c
--- /dev/null
+++ b/OFX-to-XLSX/Documentation/OFX_102_Samples/1.ofx
@@ -0,0 +1,72 @@
+
+OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+0
+INFO
+
+00000000000000
+POR
+
+
+
+
+1001
+
+0
+INFO
+
+
+BRL
+
+0237
+99999
+CHECKING
+
+
+20190509120000
+20190509120000
+
+CREDIT
+20190430120000
+10,00
+N1013F
+3243801
+RESGATE INVEST
+
+
+CREDIT
+20190430120000
+24783,31
+N10153
+3243801
+RESGATE INVEST
+
+
+DEBIT
+20190430120000
+-22785,76
+N10167
+936909
+APLICACAO EM FUNDOS
+
+
+
+17071,23
+00000000
+
+
+
+
+
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Samples/2.ofx b/OFX-to-XLSX/Documentation/OFX_102_Samples/2.ofx
new file mode 100644
index 0000000..73216a5
--- /dev/null
+++ b/OFX-to-XLSX/Documentation/OFX_102_Samples/2.ofx
@@ -0,0 +1,76 @@
+OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+
+ 20140304100000[-03:EST]
+ POR
+
+ MYBANK
+ MYBANK
+
+
+
+
+
+ 1001 (can be a GUID)
+
+ 0
+ INFO
+
+
+ BRL
+
+ 0341
+ 9999999999
+ CHECKING
+
+
+ 20131205100000[-03:EST] or 20240601040000.000
+ 20140228100000[-03:EST]
+
+ DEBIT
+ 20131209100000[-03:EST]
+ -666.66
+ 20131209001 (can be hex encoded)
+ 20131209001
+ PROPERTY TAX
+ RSHOP
+
+
+ CREDIT
+ 20131209100000[-03:EST]
+ 99.99
+ 20131209002
+ 20131209002
+ REND PAGO APLIC AUT MAIS
+
+
+ DEBIT
+ 20131210100000[-03:EST]
+ -77.77
+ 20131210001
+ 20131210001
+ SISDEB
+
+
+
+ -9999.99
+ 20140304100000[-03:EST]
+
+
+
+
+
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Samples/3.ofx b/OFX-to-XLSX/Documentation/OFX_102_Samples/3.ofx
new file mode 100644
index 0000000..071eab8
--- /dev/null
+++ b/OFX-to-XLSX/Documentation/OFX_102_Samples/3.ofx
@@ -0,0 +1,78 @@
+OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+
+ 20140203182251[-3:GMT]
+ ENG
+
+ SANTANDER
+ SANTANDER
+
+
+
+
+
+ 1
+
+ 0
+ INFO
+
+
+ BRL
+
+ 033
+ 9999999999999
+ CHECKING
+
+
+ 20140203182251[-3:GMT]
+ 20140203182251[-3:GMT]
+
+ OTHER
+ 20131107000000[-3:GMT]
+ -11.11
+ 00510367
+ 00510367
+ 0
+ DEBITO VISA ELECTRON BRASIL
+
+
+ OTHER
+ 20131107000000[-3:GMT]
+ -222,22
+ 00000105
+ 00000105
+ 0
+ COMPENSACAO INTERNA DE CHEQUE
+
+
+ OTHER
+ 20131108000000[-3:GMT]
+ -333.33
+ 00330867
+ 00330867
+ 0
+ DEBITO VISA ELECTRON BRASIL
+
+
+
+ 9999.99
+ 20140203182251[-3:GMT]
+
+
+
+
+
\ No newline at end of file
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin1.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin1.doc
new file mode 100644
index 0000000..8b92478
Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin1.doc differ
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin2.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin2.doc
new file mode 100644
index 0000000..e2d658c
Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin2.doc differ
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin3.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin3.doc
new file mode 100644
index 0000000..71ba350
Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin3.doc differ
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin4.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin4.doc
new file mode 100644
index 0000000..eb24342
Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin4.doc differ
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin5.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin5.doc
new file mode 100644
index 0000000..939fb42
Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin5.doc differ
diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin6.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin6.doc
new file mode 100644
index 0000000..b13a474
Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin6.doc differ
diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.Designer.cs b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.Designer.cs
new file mode 100644
index 0000000..250f10b
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.Designer.cs
@@ -0,0 +1,155 @@
+namespace OFX_To_XLSX_Converter
+{
+ partial class ConverterForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ labelOpenFile = new Label();
+ openFileDialog = new OpenFileDialog();
+ buttonOpenFile = new Button();
+ textBoxOfxFile = new TextBox();
+ textBoxXlsxFile = new TextBox();
+ buttonSaveFile = new Button();
+ saveFileDialog = new SaveFileDialog();
+ buttonConvert = new Button();
+ labelSaveFile = new Label();
+ richTextBoxResults = new RichTextBox();
+ SuspendLayout();
+ //
+ // labelOpenFile
+ //
+ labelOpenFile.AutoSize = true;
+ labelOpenFile.Location = new Point(12, 9);
+ labelOpenFile.Name = "labelOpenFile";
+ labelOpenFile.Size = new Size(142, 25);
+ labelOpenFile.TabIndex = 0;
+ labelOpenFile.Text = "Choose OFX File";
+ //
+ // openFileDialog
+ //
+ openFileDialog.FileName = "openFileDialog";
+ openFileDialog.Filter = "OFX Files|*.ofx|All Files|*.*";
+ //
+ // buttonOpenFile
+ //
+ buttonOpenFile.Location = new Point(613, 34);
+ buttonOpenFile.Name = "buttonOpenFile";
+ buttonOpenFile.Size = new Size(175, 34);
+ buttonOpenFile.TabIndex = 1;
+ buttonOpenFile.Text = "Browse...";
+ buttonOpenFile.UseVisualStyleBackColor = true;
+ buttonOpenFile.Click += OnOpenFileClicked;
+ //
+ // textBoxOfxFile
+ //
+ textBoxOfxFile.Location = new Point(12, 37);
+ textBoxOfxFile.Name = "textBoxOfxFile";
+ textBoxOfxFile.Size = new Size(595, 31);
+ textBoxOfxFile.TabIndex = 2;
+ //
+ // textBoxXlsxFile
+ //
+ textBoxXlsxFile.Location = new Point(12, 121);
+ textBoxXlsxFile.Name = "textBoxXlsxFile";
+ textBoxXlsxFile.Size = new Size(595, 31);
+ textBoxXlsxFile.TabIndex = 3;
+ //
+ // buttonSaveFile
+ //
+ buttonSaveFile.Location = new Point(613, 118);
+ buttonSaveFile.Name = "buttonSaveFile";
+ buttonSaveFile.Size = new Size(175, 34);
+ buttonSaveFile.TabIndex = 4;
+ buttonSaveFile.Text = "Set Name...";
+ buttonSaveFile.UseVisualStyleBackColor = true;
+ buttonSaveFile.Click += OnSaveFileClicked;
+ //
+ // saveFileDialog
+ //
+ saveFileDialog.DefaultExt = "xlsx";
+ saveFileDialog.Filter = "Excel Files|*.xlsx|All Files|*.*";
+ //
+ // buttonConvert
+ //
+ buttonConvert.Location = new Point(12, 220);
+ buttonConvert.Name = "buttonConvert";
+ buttonConvert.Size = new Size(776, 84);
+ buttonConvert.TabIndex = 5;
+ buttonConvert.Text = "Convert!";
+ buttonConvert.UseVisualStyleBackColor = true;
+ buttonConvert.Click += OnConvertFileClicked;
+ //
+ // labelSaveFile
+ //
+ labelSaveFile.AutoSize = true;
+ labelSaveFile.Location = new Point(12, 93);
+ labelSaveFile.Name = "labelSaveFile";
+ labelSaveFile.Size = new Size(148, 25);
+ labelSaveFile.TabIndex = 6;
+ labelSaveFile.Text = "Save To XLSX File";
+ //
+ // richTextBoxResults
+ //
+ richTextBoxResults.Location = new Point(12, 310);
+ richTextBoxResults.Name = "richTextBoxResults";
+ richTextBoxResults.Size = new Size(776, 163);
+ richTextBoxResults.TabIndex = 7;
+ richTextBoxResults.Text = "";
+ //
+ // ConverterForm
+ //
+ AutoScaleDimensions = new SizeF(10F, 25F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(800, 485);
+ Controls.Add(richTextBoxResults);
+ Controls.Add(labelSaveFile);
+ Controls.Add(buttonConvert);
+ Controls.Add(buttonSaveFile);
+ Controls.Add(textBoxXlsxFile);
+ Controls.Add(textBoxOfxFile);
+ Controls.Add(buttonOpenFile);
+ Controls.Add(labelOpenFile);
+ Name = "ConverterForm";
+ Text = "Convert OFX To XLSX";
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private Label labelOpenFile;
+ private OpenFileDialog openFileDialog;
+ private Button buttonOpenFile;
+ private TextBox textBoxOfxFile;
+ private TextBox textBoxXlsxFile;
+ private Button buttonSaveFile;
+ private SaveFileDialog saveFileDialog;
+ private Button buttonConvert;
+ private Label labelSaveFile;
+ private RichTextBox richTextBoxResults;
+ }
+}
\ No newline at end of file
diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.cs b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.cs
new file mode 100644
index 0000000..f22a204
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.cs
@@ -0,0 +1,57 @@
+using OFX_To_XLSX;
+
+namespace OFX_To_XLSX_Converter;
+
+public partial class ConverterForm : Form
+{
+ public ConverterForm()
+ {
+ InitializeComponent();
+ }
+
+ private void OnOpenFileClicked(object sender, EventArgs args)
+ {
+ if (openFileDialog.ShowDialog() == DialogResult.OK)
+ textBoxOfxFile.Text = openFileDialog.FileName;
+ }
+
+ private void OnSaveFileClicked(object sender, EventArgs args)
+ {
+ if (saveFileDialog.ShowDialog() == DialogResult.OK )
+ textBoxXlsxFile.Text = saveFileDialog.FileName;
+ }
+
+ private void OnConvertFileClicked(object sender, EventArgs args)
+ {
+ richTextBoxResults.ResetText();
+
+ if (string.IsNullOrWhiteSpace(textBoxOfxFile.Text) || string.IsNullOrWhiteSpace(textBoxXlsxFile.Text))
+ {
+ richTextBoxResults.AppendText("Must provide both OFX and XLSX files.");
+ return;
+ }
+ else if (File.Exists(textBoxOfxFile.Text) == false || Directory.Exists(Path.GetDirectoryName(textBoxXlsxFile.Text)) == false)
+ {
+ richTextBoxResults.AppendText("OFX file and directory to save XLSX file must both exist.");
+ return;
+ }
+
+ try
+ {
+ var converter = new OfxParser();
+ var ofx = converter.ReadOfxFile(textBoxOfxFile.Text);
+
+ if (ofx == null)
+ richTextBoxResults.AppendText("Failed reading OFX file.");
+ else if (converter.ConvertOfxToXlsx(textBoxXlsxFile.Text, ofx))
+ richTextBoxResults.AppendText("Converted OFX to XLSX successfully.");
+ else
+ richTextBoxResults.AppendText("Failed converting OFX to XLSX.");
+ }
+ catch (Exception e)
+ {
+ richTextBoxResults.AppendText("Failed converting OFX to XLSX.");
+ richTextBoxResults.AppendText($"Error details {e.GetType().Name}, {e.Message}.");
+ }
+ }
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.resx b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.resx
new file mode 100644
index 0000000..fd94e54
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+ 203, 17
+
+
\ No newline at end of file
diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/OFX-To-XLSX-Converter.csproj b/OFX-to-XLSX/OFX-To-XLSX-Converter/OFX-To-XLSX-Converter.csproj
new file mode 100644
index 0000000..33d6bbe
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/OFX-To-XLSX-Converter.csproj
@@ -0,0 +1,21 @@
+
+
+
+ WinExe
+ net8.0-windows
+ $(MSBuildProjectName.Replace("-", "_"))
+ true
+ enable
+ enable
+ NF Software Inc.
+ Copyright 2024 $(Company)
+ 1.0.0.0
+ 1.0.0.0
+ 1.0.0
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/Program.cs b/OFX-to-XLSX/OFX-To-XLSX-Converter/Program.cs
new file mode 100644
index 0000000..96ec68c
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/Program.cs
@@ -0,0 +1,14 @@
+namespace OFX_To_XLSX_Converter;
+
+internal static class Program
+{
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ static void Main()
+ {
+ ApplicationConfiguration.Initialize();
+ Application.Run(new ConverterForm());
+ }
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX.sln b/OFX-to-XLSX/OFX-To-XLSX.sln
new file mode 100644
index 0000000..ca09366
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35027.167
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OFX-To-XLSX-Converter", "OFX-To-XLSX-Converter\OFX-To-XLSX-Converter.csproj", "{47377EB8-AD53-428F-912D-17D359CB5901}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFX-To-XLSX", "OFX-To-XLSX\OFX-To-XLSX.csproj", "{361385AD-7B50-41F0-8E11-28E86F4555A1}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {47377EB8-AD53-428F-912D-17D359CB5901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47377EB8-AD53-428F-912D-17D359CB5901}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47377EB8-AD53-428F-912D-17D359CB5901}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47377EB8-AD53-428F-912D-17D359CB5901}.Release|Any CPU.Build.0 = Release|Any CPU
+ {361385AD-7B50-41F0-8E11-28E86F4555A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {361385AD-7B50-41F0-8E11-28E86F4555A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {361385AD-7B50-41F0-8E11-28E86F4555A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {361385AD-7B50-41F0-8E11-28E86F4555A1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1B7E494D-101C-43C0-ACA7-C3B9A19A79FF}
+ EndGlobalSection
+EndGlobal
diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxAccount.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxAccount.cs
new file mode 100644
index 0000000..a85a90e
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxAccount.cs
@@ -0,0 +1,19 @@
+namespace OFX_To_XLSX;
+
+public class OfxAccount
+{
+ ///
+ /// Routing and transit number.
+ ///
+ public string? BankId { get; set; }
+
+ ///
+ /// Account number.
+ ///
+ public string? AccountId { get; set; }
+
+ ///
+ /// Type of account.
+ ///
+ public string? AccountType { get; set; }
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxFile.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxFile.cs
new file mode 100644
index 0000000..77a207f
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxFile.cs
@@ -0,0 +1,49 @@
+namespace OFX_To_XLSX;
+
+public class OfxFile
+{
+ ///
+ /// Contains the header data from the OFX file.
+ ///
+ public OfxHeader Header { get; init; } = new();
+
+ ///
+ /// The Signon message set includes the signon message, USERPASS change message, and challenge message, which must appear in that order.
+ ///
+ public OfxSignOnResponse SignOnResponse { get; init; } = new();
+
+ ///
+ /// Contains details on the account the OFX file is for.
+ ///
+ public OfxAccount Account { get; init; } = new();
+
+ ///
+ /// Default currency for the statement.
+ ///
+ public string? Currency { get; set; }
+
+ ///
+ /// Start date of statement requested.
+ ///
+ public DateTime? Start { get; set; }
+
+ ///
+ /// End date of statement requested.
+ ///
+ public DateTime? End { get; set; }
+
+ ///
+ /// Ledger balance amount.
+ ///
+ public decimal? Balance { get; set; }
+
+ ///
+ /// Balance date.
+ ///
+ public DateTime? BalanceDate { get; set; }
+
+ ///
+ /// Contains the transactions from the OFX file.
+ ///
+ public List Transactions { get; init; } = [];
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxHeader.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxHeader.cs
new file mode 100644
index 0000000..5e2b8ec
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxHeader.cs
@@ -0,0 +1,49 @@
+namespace OFX_To_XLSX;
+
+public class OfxHeader
+{
+ ///
+ /// OFXHEADER specifies the version number of the Open Financial Exchange headers.
+ ///
+ public int HeaderVersion { get; set; }
+
+ ///
+ /// DATA specifies the content type, in this case OFXSGML.
+ ///
+ public string? ContentType { get; set; }
+
+ ///
+ /// VERSION specifies the version number of the Document Type Definition (DTD) used for parsing.
+ ///
+ public int Version { get; set; }
+
+ ///
+ /// SECURITY defines the type of application-level security, if any, that is used for the OFX block. The values for SECURITY can be NONE or TYPE1.
+ ///
+ public string? Security { get; set; }
+
+ ///
+ /// ENCODING defines the text encoding used for character data. The values for ENCODING can be UNICODE or USASCII.
+ ///
+ public string? Encoding { get; set; }
+
+ ///
+ /// CHARSET defines the character set used for character data.
+ ///
+ public string? CharacterSet { get; set; }
+
+ ///
+ /// A future version of the specification will define compression.
+ ///
+ public string? Compression { get; set; }
+
+ ///
+ /// NEWFILEUID uniquely identifies this request file.
+ ///
+ public string? NewFileId { get; set; }
+
+ ///
+ /// OLDFILEUID is used together with NEWFILEUID only when the client and server support file-based error recovery.
+ ///
+ public string? OldFileId { get; set; }
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxSignOnResponse.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxSignOnResponse.cs
new file mode 100644
index 0000000..31009ad
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxSignOnResponse.cs
@@ -0,0 +1,34 @@
+namespace OFX_To_XLSX;
+
+public class OfxSignOnResponse
+{
+ ///
+ /// Organization defining this Financial Institution name space.
+ ///
+ public string? Organization { get; set; }
+
+ ///
+ /// Financial Institution ID.
+ ///
+ public string? OrganizationId { get; set; }
+
+ ///
+ /// Contains any error code that occurs during sign-on.
+ ///
+ public int Status { get; set; }
+
+ ///
+ /// Contains any severity level of error codes that occur during sign-on.
+ ///
+ public string? StatusSeverity { get; set; }
+
+ ///
+ /// Language used in text responses.
+ ///
+ public string? Language { get; set; }
+
+ ///
+ /// Date and time of the server response.
+ ///
+ public DateTime? ServerTime { get; set; }
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxTransaction.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxTransaction.cs
new file mode 100644
index 0000000..6d8c981
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxTransaction.cs
@@ -0,0 +1,39 @@
+namespace OFX_To_XLSX;
+
+public class OfxTransaction
+{
+ ///
+ /// Transaction type.
+ ///
+ public string? TransactionType { get; set; }
+
+ ///
+ /// Date transaction was posted to account.
+ ///
+ public DateTime? Posted { get; set; }
+
+ ///
+ /// Amount of transaction.
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// Transaction ID issued by financial institution.
+ ///
+ public string? TransactionId { get; set; }
+
+ ///
+ /// Check (or other reference) number.
+ ///
+ public string? CheckNumber { get; set; }
+
+ ///
+ /// Name of payee or description of transaction.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Extra information (not in NAME).
+ ///
+ public string? Memo { get; set; }
+}
diff --git a/OFX-to-XLSX/OFX-To-XLSX/OFX-To-XLSX.csproj b/OFX-to-XLSX/OFX-To-XLSX/OFX-To-XLSX.csproj
new file mode 100644
index 0000000..78a0027
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/OFX-To-XLSX.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ $(MSBuildProjectName.Replace("-", "_"))
+ enable
+ enable
+ NF Software Inc.
+ Copyright 2024 $(Company)
+ 1.0.0.0
+ 1.0.0.0
+ 1.0.0
+ Library
+
+
+
+
+
+
+
+
+
diff --git a/OFX-to-XLSX/OFX-To-XLSX/OfxParser.cs b/OFX-to-XLSX/OFX-To-XLSX/OfxParser.cs
new file mode 100644
index 0000000..6076dfe
--- /dev/null
+++ b/OFX-to-XLSX/OFX-To-XLSX/OfxParser.cs
@@ -0,0 +1,219 @@
+using easy_core;
+using SpreadsheetLight;
+using System;
+using System.Globalization;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace OFX_To_XLSX;
+
+public class OfxParser
+{
+ ///
+ /// Reads the specified file and parses the OFX data.
+ ///
+ /// The path to the file to parse.
+ public OfxFile? ReadOfxFile(string file)
+ {
+ if (File.Exists(file) == false)
+ return null;
+
+ var text = File.ReadAllText(file);
+ var lines = text.ToLines().Where(x => string.IsNullOrWhiteSpace(x) == false).Select(x => x.Trim()).ToArray();
+ var ofx = new OfxFile();
+
+ while (lines.Length > 0 && lines[0].FirstOrDefault() != '<')
+ {
+ if (lines[0].StartsWith("OFXHEADER"))
+ ofx.Header.HeaderVersion = int.TryParse(lines[0].Split(':').Last(), out var version) ? version : 0;
+ else if (lines[0].StartsWith("DATA"))
+ ofx.Header.ContentType = lines[0].Split(':').Skip(1).FirstOrDefault();
+ else if (lines[0].StartsWith("VERSION"))
+ ofx.Header.Version = int.TryParse(lines[0].Split(':').Last(), out var version) ? version : 0;
+ else if (lines[0].StartsWith("SECURITY"))
+ ofx.Header.Security = lines[0].Split(':').Skip(1).FirstOrDefault();
+ else if (lines[0].StartsWith("ENCODING"))
+ ofx.Header.Encoding = lines[0].Split(':').Skip(1).FirstOrDefault();
+ else if (lines[0].StartsWith("CHARSET"))
+ ofx.Header.CharacterSet = lines[0].Split(':').Skip(1).FirstOrDefault();
+ else if (lines[0].StartsWith("COMPRESSION"))
+ ofx.Header.Compression = lines[0].Split(':').Skip(1).FirstOrDefault();
+ else if (lines[0].StartsWith("OLDFILEUID"))
+ ofx.Header.OldFileId = lines[0].Split(':').Skip(1).FirstOrDefault();
+ else if (lines[0].StartsWith("NEWFILEUID"))
+ ofx.Header.NewFileId = lines[0].Split(':').Skip(1).FirstOrDefault();
+
+ lines = lines.Skip(1).ToArray();
+ }
+
+ var xml = ConvertOfxToXml(lines);
+
+ if (string.IsNullOrWhiteSpace(xml))
+ return null;
+
+ ParseXmlData(xml, ofx);
+ return ofx;
+ }
+
+ private string ConvertOfxToXml(string[] lines)
+ {
+ using var writer = new StringWriter();
+ using var xml = XmlWriter.Create(writer);
+
+ xml.WriteStartDocument();
+
+ foreach (var line in lines)
+ {
+ if (line.StartsWith("") && line.EndsWith('>'))
+ xml.WriteEndElement();
+ else if (line.StartsWith('<') && line.EndsWith('>'))
+ xml.WriteStartElement(line.TrimStart('<').TrimEnd('>'));
+ else if (line.Contains('>') && line.Length > line.IndexOf('>') + 1)
+ xml.WriteElementString(line.Split('>').First().TrimStart('<'), line.Split('>').Last());
+ }
+
+ xml.WriteEndDocument();
+ xml.Flush();
+
+ return writer.ToString();
+ }
+
+ private void ParseXmlData(string xml, OfxFile ofx)
+ {
+ var document = XDocument.Parse(xml);
+
+ var signon = document.Descendants("SONRS").SingleOrDefault();
+ var account = document.Descendants("BANKACCTFROM").FirstOrDefault();
+ var ledger = document.Descendants("LEDGERBAL").FirstOrDefault();
+ var transactions = document.Descendants("STMTTRN");
+
+ if (signon != null && signon.HasElements)
+ {
+ var institution = signon.Element("FI");
+
+ if (institution != null)
+ {
+ ofx.SignOnResponse.Organization = institution.Element("ORG")?.Value;
+ ofx.SignOnResponse.OrganizationId = institution.Element("FID")?.Value;
+ }
+
+ var status = signon.Element("STATUS");
+
+ if (status != null)
+ {
+ ofx.SignOnResponse.Status = int.TryParse(status.Element("CODE")?.Value, out int code) ? code : 0;
+ ofx.SignOnResponse.StatusSeverity = status.Element("SEVERITY")?.Value;
+ }
+
+ ofx.SignOnResponse.Language = signon.Element("LANGUAGE")?.Value;
+ ofx.SignOnResponse.ServerTime = ParseXmlDate(signon.Element("DTSERVER"));
+ }
+
+ if (account != null && account.HasElements)
+ {
+ ofx.Account.BankId = account.Element("BANKID")?.Value;
+ ofx.Account.AccountId = account.Element("ACCTID")?.Value;
+ ofx.Account.AccountType = account.Element("ACCTTYPE")?.Value;
+ }
+
+ ofx.Currency = document.Descendants("CURDEF").FirstOrDefault()?.Value;
+ ofx.Start = ParseXmlDate(document.Descendants("DTSTART").FirstOrDefault());
+ ofx.End = ParseXmlDate(document.Descendants("DTEND").FirstOrDefault());
+
+ if (ledger != null && ledger.HasElements)
+ {
+ ofx.Balance = decimal.TryParse(ledger.Element("BALAMT")?.Value, out decimal balance) ? balance : null;
+ ofx.BalanceDate = ParseXmlDate(ledger.Element("DTASOF"));
+ }
+
+ foreach (var transaction in transactions.Where(x => x.HasElements))
+ {
+ ofx.Transactions.Add(new OfxTransaction
+ {
+ TransactionType = transaction.Element("TRNTYPE")?.Value,
+ Amount = decimal.TryParse(transaction.Element("TRNAMT")?.Value, out decimal amount) ? amount : 0,
+ Posted = ParseXmlDate(transaction.Element("DTPOSTED")),
+ TransactionId = transaction.Element("FITID")?.Value,
+ CheckNumber = transaction.Element("CHECKNUM")?.Value,
+ Name = transaction.Element("NAME")?.Value,
+ Memo = transaction.Element("MEMO")?.Value,
+ });
+ }
+ }
+
+ private DateTime? ParseXmlDate(XElement? element) => DateTime.TryParseExact(element?.Value[..14], "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime time) ? time : null;
+
+ ///
+ /// Converts the provided OFX file into an XLSX file and saves it at the specified path.
+ ///
+ /// The path and filename to save the XLSX file to.
+ /// Object containing the parsed OFX data.
+ ///
+ ///
+ ///
+ ///
+ public bool ConvertOfxToXlsx(string file, OfxFile ofx, bool includeHeader = false, bool includeSignOn = false, bool includeDetails = true, bool useTableHeader = true)
+ {
+ // Prepare styles
+ var excel = new SLDocument();
+
+ var title = excel.CreateStyle();
+ var accent1 = excel.CreateStyle();
+ var accent2 = excel.CreateStyle();
+ var dates = excel.CreateStyle();
+ var numbers = excel.CreateStyle();
+
+ title.ApplyNamedCellStyle(SLNamedCellStyleValues.Title);
+ accent1.ApplyNamedCellStyle(SLNamedCellStyleValues.Accent1);
+ accent2.ApplyNamedCellStyle(SLNamedCellStyleValues.Accent2);
+ dates.FormatCode = "yyyy-mm-dd hh:mm:ss";
+ numbers.FormatCode = "0.00";
+
+ // Prepare data
+ if (useTableHeader)
+ {
+ excel.SetCellValue("A1", "Transaction Id");
+ excel.SetCellValue("B1", "Transaction Type");
+ excel.SetCellValue("C1", "Posted");
+ excel.SetCellValue("D1", "Amount");
+ excel.SetCellValue("E1", "Detail");
+ excel.SetCellValue("F1", "Reference");
+ excel.SetCellValue("G1", "Memo");
+
+ excel.SetCellStyle("A1", "G1", accent1);
+ }
+
+ var offset = useTableHeader ? 2 : 1;
+
+ for (var i = 0; i < ofx.Transactions.Count; i++)
+ {
+ excel.SetCellValue($"A{i + offset}", ofx.Transactions[i].TransactionId);
+ excel.SetCellValue($"B{i + offset}", ofx.Transactions[i].TransactionType);
+
+ if (ofx.Transactions[i].Posted != null)
+ excel.SetCellValue($"C{i + offset}", ofx.Transactions[i].Posted!.Value);
+
+ excel.SetCellValue($"D{i + offset}", ofx.Transactions[i].Amount);
+ excel.SetCellValue($"E{i + offset}", ofx.Transactions[i].Name);
+ excel.SetCellValue($"F{i + offset}", ofx.Transactions[i].CheckNumber);
+ excel.SetCellValue($"G{i + offset}", ofx.Transactions[i].Memo);
+ }
+
+ excel.SetCellStyle("C" + offset, "C" + (offset + ofx.Transactions.Count - 1), dates);
+ excel.SetCellStyle("D" + offset, "D" + (offset + ofx.Transactions.Count - 1), numbers);
+
+ // Autofit
+ excel.AutoFitColumn("A", "G");
+
+ // Save
+ try
+ {
+ excel.SaveAs(file);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/README.md b/README.md
index 2e700b4..b9788ba 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,9 @@ These tools are intentionally pragmatic:
> If you’re looking for curated, stable, long-term maintained tooling… this repo probably isn’t it.
> If you’re looking for *“I just need this done once”* helpers — welcome. 🙂
-## Contents
+# Contents
-### ✅ Extract Copilot Chat
+## ✅ Extract Copilot Chat
Export Copilot chat transcripts from **JSON files saved via browser DevTools (Network tab)** into **Markdown**.
@@ -24,7 +24,13 @@ Copilot UIs change frequently and export options are inconsistent across account
2. Save the conversation JSON response(s) to disk
3. Convert to readable `.md` files you can archive, search, and sort yourself
-## Extract Copilot Chat
+## ✅ OFX to XLSX Converter
+
+Convert basic **OFX (Open Financial Exchange)** bank export files into simple **Excel (.xlsx)** spreadsheets.
+
+This is a **best-effort parser**, built for practical use with real-world bank exports — not a full spec-compliant OFX implementation.
+
+# Extract Copilot Chat
### What it does
@@ -94,7 +100,65 @@ Each markdown file includes:
* **Deduping is heuristic**: It aims to remove common duplicates but may occasionally merge distinct messages that are identical in content.
* **No guarantee of completeness**: If the network response didn’t include older turns, the output won’t either.
-## Contributing
+# OFX to XLSX Converter
+
+### What it does
+
+* Reads a single `.ofx` file
+* Extracts common transaction fields, including:
+ * Date
+ * Amount
+ * Description / Memo
+ * Transaction type (if available)
+ * Account / balance info (when present)
+* Outputs a flat `.xlsx` file for easy filtering, sorting, and analysis
+
+### Supported scenarios
+
+* Typical bank export files (checking / credit accounts)
+* OFX files that resemble:
+ * SGML-style (older OFX 1.x)
+ * Loosely structured, non-strict variants many banks produce
+
+### Not supported
+
+* Full OFX 2.x / XML compliance
+* Every possible tag or edge case
+* Investment account structures (partial at best)
+* Multi-account aggregation in a single file
+
+## How to use
+
+### Run
+
+```bash
+dotnet run --project easy-mini-tools/OFX-To-XLSX/OFX-To-XLSX-Converter
+```
+
+## Output format
+
+The generated spreadsheet includes:
+
+* One row per transaction
+* Columns for:
+ * Type
+ * Date
+ * Amount
+ * Id
+ * Check Number
+ * Name
+ * Description
+* Minimal formatting — intended for **post-processing in Excel**, not presentation
+
+## Known limitations / disclaimers
+
+* **Best-effort parsing**: OFX files are notoriously inconsistent between institutions.
+* **Loose parsing strategy**: The tool prioritizes “getting usable data out” over strict correctness.
+* **Field coverage varies**: Some banks omit fields or use non-standard tags.
+* **No validation guarantees**: Always sanity-check totals and balances against your source data.
+* **Encoding quirks**: Some OFX files include odd encoding/line break issues that may require manual cleanup.
+
+# Contributing
PRs are welcome, but the spirit of this repo is **small + useful + low-maintenance**.
@@ -104,22 +168,27 @@ If you submit improvements, please keep them aligned with the theme:
* No dependency explosions
* Prefer “drop-in and run” simplicity
-## Repo structure (suggested)
+# Repo structure
A simple layout that stays scalable as you add more mini-tools:
- /
- easy-mini-tools/
- ExtractCopilotChat/
- Program.cs
- ExtractCopilotChat.csproj
- README.md
- LICENSE
+```
+/
+ easy-mini-tools/
+ ExtractCopilotChat/
+ Program.cs
+ ExtractCopilotChat.slnx
+ OFX-To-XLSX/
+ Program.cs
+ OFX-To-XLSX.sln
+ README.md
+ LICENSE
+```
-## Authors
+# Authors
* **NF Software Inc.**
-## License
+# License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details