about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2015-04-05 15:59:26 +0100
committerAlan Pearce2015-04-05 15:59:26 +0100
commit115cc1a38c67606909bf3df1cc41d257fa8120ba (patch)
tree6a103de27c79ff7bee64b16d090c7a9940065b90
parentd06ed0b1f4cc014c72d338f3578326e10cc12212 (diff)
downloadynab-ledger-115cc1a38c67606909bf3df1cc41d257fa8120ba.tar.lz
ynab-ledger-115cc1a38c67606909bf3df1cc41d257fa8120ba.tar.zst
ynab-ledger-115cc1a38c67606909bf3df1cc41d257fa8120ba.zip
Convert: Implement YNAB file parsing
-rw-r--r--src/Application.php1
-rw-r--r--src/Command/ConvertCommand.php152
-rw-r--r--src/Item/BudgetTransaction.php10
-rw-r--r--src/Item/RegisterTransaction.php15
4 files changed, 178 insertions, 0 deletions
diff --git a/src/Application.php b/src/Application.php
index 9413acb..bc01326 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -7,5 +7,6 @@ use CLIFramework\Application as BaseApplication;
 class Application extends BaseApplication {
     public function init () {
         parent::init();
+        $this->command('convert');
     }
 }
diff --git a/src/Command/ConvertCommand.php b/src/Command/ConvertCommand.php
new file mode 100644
index 0000000..5a9a484
--- /dev/null
+++ b/src/Command/ConvertCommand.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace YnabLedger\Command;
+
+use Generator;
+use SplFileObject;
+use LimitIterator;
+use SeekableIterator;
+use ArrayIterator;
+use DateTimeImmutable;
+use NumberFormatter;
+use CLIFramework\Command;
+use YnabLedger\Item\RegisterTransaction;
+use YnabLedger\Item\BudgetTransaction;
+
+function convertDate ($currencySymbol) {
+    $dateFormat;
+    return function ($dateString) use ($currencySymbol, &$dateFormat) {
+        if ($dateFormat === null) {
+            if (is_numeric(substr($dateString, -4))) {
+                if (ord($currencySymbol) == ord('£')) {
+                    $dateFormat = 'd/m/Y';
+                } else {
+                    $dateFormat = 'm/d/Y';
+                }
+            } else {
+                $dateFormat = false;
+            }
+        }
+        if ($dateFormat === false) {
+            return new DateTimeImmutable($dateString);
+        } else {
+            return DateTimeImmutable::createFromFormat(
+                $dateFormat,
+                $dateString
+            );
+        }
+    };
+}
+
+function readBudget (SplFileObject $budgetFile) {
+    $budgetFile->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
+    $file = new LimitIterator($budgetFile, 1);
+    $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
+
+    foreach ($file as $row) {
+        $txn = new BudgetTransaction;
+        $txn->date = DateTimeImmutable::createFromFormat('d F Y', '1 ' . $row[0])->setTime(0, 0, 0);
+        $txn->category = explode(':', $row[1], 2);
+        $txn->in = $fmt->parseCurrency($row[4], $curr);
+        $txn->out = $fmt->parseCurrency($row[5], $curr);
+        $txn->balance = $row[6];
+        if (!in_array($row[2], $txn->category, true)) {
+            array_unshift($txn->category, $row[2]);
+        }
+        yield $txn;
+    }
+}
+
+function readRegister (SplFileObject $registerFile, $locale) {
+    $registerFile->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
+
+    $iterator = new LimitIterator($registerFile, 1);
+    $iterator->rewind();
+    $convertDate = convertDate($iterator->current()[9]);
+
+    $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
+
+    $txns = [];
+    foreach ($iterator as $i => $row) {
+        // "Account","Flag","Check Number","Date","Payee","Category","Master Category","Sub Category","Memo","Outflow","Inflow","Cleared","Running Balance"
+        $txn = new RegisterTransaction();
+        $txn->account = $row[0];
+        $txn->date = $convertDate($row[3])->setTime(0, 0, 0);
+        $txn->payee = $row[4];
+        $txn->category = explode(':', $row[5]);
+        if (!in_array($row[6], $txn->category, true)) {
+            array_unshift($txn->category, $row[6]);
+        }
+        $txn->memo = $row[8];
+        $txn->out = $fmt->parseCurrency($row[9], $curr);
+        $txn->in = $fmt->parseCurrency($row[10], $curr);
+        $txn->cleared = ($row[11] !== 'U');
+        $txn->line = $i;
+        $txns[] = $txn;
+    }
+
+    // Sort by date, then promote income and fall back on line in file
+    usort($txns,
+          function ($a, $b) use ($convertDate) {
+              return strcmp($a->date->format('U'), $b->date->format('U'))
+                  ?: ($b->in < 0.01
+                      ? 0 : $a->line - $b->line);
+          }
+    );
+    foreach ($txns as $txn) yield $txn;
+}
+
+function multiRead (Generator $budgetFile, Generator $registerFile) {
+    $budgetFile->rewind();
+    foreach ($registerFile as $rTxn) {
+        if ($rTxn->payee !== 'Starting Balance' &&
+            $rTxn->category[1] !== 'Available this month') {
+            if ($budgetFile->valid()) {
+                $bTxn = $budgetFile->current();
+                $bDate = $bTxn->date;
+                if ($bTxn->date < $rTxn->date) {
+                    sleep(2);
+                    do {
+                        yield $bTxn;
+
+                        $budgetFile->next();
+                        $bTxn = $budgetFile->current();
+                    } while ($bTxn->date == $bDate);
+                }
+            }
+        }
+        yield $rTxn;
+    }
+}
+
+class ConvertCommand extends Command {
+    public function brief () {
+        return 'Convert budget & register exports';
+    }
+
+    public function arguments ($args) {
+        $args->add('budget')
+            ->desc('Budget export file')
+            ->isa('file')
+            ->glob('*-Budget.csv');
+
+        $args->add('register')
+            ->desc('Register export file')
+            ->isa('file')
+            ->glob('*-Register.csv');
+    }
+
+    public function execute ($budgetFile, $registerFile) {
+        $locale = 'en_GB';
+        $budget = readBudget(new SplFileObject($budgetFile));
+        $register = readRegister(new SplFileObject($registerFile), $locale);
+        foreach (multiRead($budget, $register) as $txn) {
+            printf("%-8s %-10s\t%-30s %s\n",
+                   $txn instanceof RegisterTransaction ? 'Register' : 'Budget',
+                   $txn->date->format('Y-m-d'),
+                   implode(':', $txn->category),
+                   isset($txn->payee) ? $txn->payee : ''
+            );
+        }
+    }
+}
diff --git a/src/Item/BudgetTransaction.php b/src/Item/BudgetTransaction.php
new file mode 100644
index 0000000..e4899f9
--- /dev/null
+++ b/src/Item/BudgetTransaction.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace YnabLedger\Item;
+
+class BudgetTransaction {
+    public $date;
+    public $category = [];
+    public $in;
+    public $out;
+}
diff --git a/src/Item/RegisterTransaction.php b/src/Item/RegisterTransaction.php
new file mode 100644
index 0000000..b676640
--- /dev/null
+++ b/src/Item/RegisterTransaction.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace YnabLedger\Item;
+
+class RegisterTransaction {
+    public $line;
+    public $account;
+    public $date;
+    public $payee;
+    public $category;
+    public $memo;
+    public $out;
+    public $in;
+    public $cleared = false;
+}