Make Nationwide statement processing data-driven.
Instead of different scripts for credit card and current account statements, define a profile for each that specifies the date and amount columns, and have this control the processing.
This commit is contained in:
parent
ef791b6be6
commit
98910d3306
1 changed files with 89 additions and 24 deletions
|
@ -1,11 +1,17 @@
|
||||||
#!/usr/bin/env -S guile -e main -s
|
#!/usr/bin/env -S guile -e main -s
|
||||||
!#
|
!#
|
||||||
|
|
||||||
(use-modules (ice-9 match)
|
;; Script for updating current account and credit card statements
|
||||||
(ice-9 getopt-long)
|
;; downloaded from Nationwide, who use a date and currency format
|
||||||
|
;; that Gnucash does not support.
|
||||||
|
|
||||||
|
(use-modules (ice-9 getopt-long)
|
||||||
|
((srfi srfi-1) #:select (drop))
|
||||||
(dsv))
|
(dsv))
|
||||||
|
|
||||||
|
;; Date appears in Nationwide statements in the format
|
||||||
|
;; "10 Jan 2024", but this is not understood by Gnucash
|
||||||
|
;; so we convert it to YYYY-MM-DD format.
|
||||||
(define date-input-format "%d %b %Y")
|
(define date-input-format "%d %b %Y")
|
||||||
(define date-output-format "%Y-%m-%d")
|
(define date-output-format "%Y-%m-%d")
|
||||||
|
|
||||||
|
@ -13,31 +19,84 @@
|
||||||
(strftime date-output-format
|
(strftime date-output-format
|
||||||
(car (strptime date-input-format d))))
|
(car (strptime date-input-format d))))
|
||||||
|
|
||||||
(define (read-statement path)
|
;; Characters we expect to see in a numeric amount field. The
|
||||||
(call-with-input-file path
|
;; Nationwide statements contain a non-ASCII currency character
|
||||||
(lambda (port)
|
;; that we want to delete.
|
||||||
(dsv->scm port #:format 'rfc4180))))
|
|
||||||
|
|
||||||
(define currency-charset (string->char-set "0123456789.-"))
|
(define currency-charset (string->char-set "0123456789.-"))
|
||||||
|
|
||||||
(define (format-amount s)
|
(define (format-amount s)
|
||||||
(string-filter currency-charset s))
|
(string-filter currency-charset s))
|
||||||
|
|
||||||
(define (process-row row)
|
;; Profiles for the different statemnets.
|
||||||
(match-let (((date description location paid-out paid-in) row))
|
;; skip: the number of leading rows to skip
|
||||||
(list (format-date date)
|
;; header: boolean indicating whether or not the first unskipped
|
||||||
description
|
;; row is a header
|
||||||
location
|
;; date-cols: list of columns containing dates
|
||||||
(format-amount paid-out)
|
;; amount-cols: list columns containing amounts
|
||||||
(format-amount paid-in))))
|
(define profiles
|
||||||
|
'(("credit-card" . ((skip . 4)
|
||||||
|
(header . #t)
|
||||||
|
(date-cols . (0))
|
||||||
|
(amount-cols . (3 4))))
|
||||||
|
("current-account" . ((skip . 4)
|
||||||
|
(header . #t)
|
||||||
|
(date-cols . (0))
|
||||||
|
(amount-cols . (3 4 5))))))
|
||||||
|
|
||||||
(define (process-statement input-path output-path)
|
;; Predicate for validating the profile option.
|
||||||
(match-let (((_ _ _ _ header . data) (read-statement input-path)))
|
(define (valid-profile? p)
|
||||||
(let ((updated (cons header (map process-row data))))
|
(if (assoc p profiles) #t #f))
|
||||||
(call-with-output-file output-path
|
|
||||||
|
;; Update a list by applying the given function to each of the
|
||||||
|
;; listed columns.
|
||||||
|
(define (update-list lst cols f)
|
||||||
|
(for-each (lambda (k)
|
||||||
|
(let ((v (list-ref lst k)))
|
||||||
|
(list-set! lst k (f v))))
|
||||||
|
cols))
|
||||||
|
|
||||||
|
;; Given a spec listing the date and amount columns, return a
|
||||||
|
;; function that will apply the corresponding formats to a row.
|
||||||
|
(define (process-row spec)
|
||||||
|
(let ((date-cols (assq-ref spec 'date-cols))
|
||||||
|
(amount-cols (assq-ref spec 'amount-cols)))
|
||||||
|
(lambda (row)
|
||||||
|
(when date-cols
|
||||||
|
(update-list row date-cols format-date))
|
||||||
|
(when amount-cols
|
||||||
|
(update-list row amount-cols format-amount)))))
|
||||||
|
|
||||||
|
;; Read a CSV from the given path.
|
||||||
|
(define (read-statement path)
|
||||||
|
(call-with-input-file path
|
||||||
(lambda (port)
|
(lambda (port)
|
||||||
(scm->dsv updated port #:format 'rfc4180))))))
|
(dsv->scm port #:format 'rfc4180))))
|
||||||
|
|
||||||
|
;; Write data to the given path in CSV format.
|
||||||
|
(define (write-statement data path)
|
||||||
|
(call-with-output-file path
|
||||||
|
(lambda (port)
|
||||||
|
(scm->dsv data port #:format 'rfc4180))))
|
||||||
|
|
||||||
|
|
||||||
|
(define (update-data spec data)
|
||||||
|
(let* ((data (drop data (or (assq-ref spec 'skip) 0)))
|
||||||
|
(header (if (assq-ref spec 'header) (car data) #f))
|
||||||
|
(data (if header (cdr data) data)))
|
||||||
|
(for-each (process-row spec) data)
|
||||||
|
(if header
|
||||||
|
(cons header data)
|
||||||
|
data)))
|
||||||
|
|
||||||
|
;; Apply the updates defined in `spec` to the statement read
|
||||||
|
;; from input-path and write the updated data to output-path.
|
||||||
|
(define (process-statement spec input-path output-path)
|
||||||
|
(let ((data (read-statement input-path)))
|
||||||
|
(write-statement (update-data spec data) output-path)))
|
||||||
|
|
||||||
|
;; Display a usage message and (optional) error message to STDERR
|
||||||
|
;; and exit. If an error message is given the exit code will be
|
||||||
|
;; non-zero.
|
||||||
(define* (usage #:optional errmsg)
|
(define* (usage #:optional errmsg)
|
||||||
(with-output-to-port (current-error-port)
|
(with-output-to-port (current-error-port)
|
||||||
(lambda ()
|
(lambda ()
|
||||||
|
@ -51,22 +110,28 @@ Usage: fix-credit-card-statement [options]
|
||||||
-i, --input=FILENAME Input file path.
|
-i, --input=FILENAME Input file path.
|
||||||
-o, --output=FILENAME Output file path. Required unless --overwrite is given.
|
-o, --output=FILENAME Output file path. Required unless --overwrite is given.
|
||||||
-w, --overwrite Overwrite the input file with the updated data.
|
-w, --overwrite Overwrite the input file with the updated data.
|
||||||
|
-p, --profile=PROFILE Profile name [credit-card|current-account].
|
||||||
")
|
")
|
||||||
(exit (if errmsg EXIT_FAILURE EXIT_SUCCESS)))))
|
(exit (if errmsg EXIT_FAILURE EXIT_SUCCESS)))))
|
||||||
|
|
||||||
|
;; Process command-line arguments and validate options.
|
||||||
|
;; If valid, run process-statement with the given options.
|
||||||
(define (main args)
|
(define (main args)
|
||||||
(let* ((option-spec '((help (single-char #\h) (value #f))
|
(let* ((option-spec `((help (single-char #\h) (value #f))
|
||||||
(input (single-char #\i) (value #t))
|
(input (single-char #\i) (value #t))
|
||||||
(output (single-char #\o) (value #t))
|
(output (single-char #\o) (value #t))
|
||||||
(overwrite (single-char #\w) (value #f))))
|
(overwrite (single-char #\w) (value #f))
|
||||||
|
(profile (single-char #\p) (value #t) (predicate ,valid-profile?))))
|
||||||
(options (getopt-long args option-spec))
|
(options (getopt-long args option-spec))
|
||||||
(help-wanted (option-ref options 'help #f))
|
(help-wanted (option-ref options 'help #f))
|
||||||
|
(profile (option-ref options 'profile #f))
|
||||||
(input (option-ref options 'input #f))
|
(input (option-ref options 'input #f))
|
||||||
(output (option-ref options 'output #f))
|
(output (option-ref options 'output #f))
|
||||||
(overwrite (option-ref options 'overwrite #f)))
|
(overwrite (option-ref options 'overwrite #f)))
|
||||||
(cond
|
(cond
|
||||||
(help-wanted (usage))
|
(help-wanted (usage))
|
||||||
|
((not profile) (usage "profile is required"))
|
||||||
((not input) (usage "input filename is required"))
|
((not input) (usage "input filename is required"))
|
||||||
((and overwrite output) (usage "output filename cannot be given with --overwrite"))
|
((and overwrite output) (usage "output filename cannot be given with --overwrite"))
|
||||||
((not (or overwrite output)) (usage "output filename is required without --overwrite")))
|
((not (or overwrite output)) (usage "output filename is required without --overwrite")))
|
||||||
(process-statement input (or output input))))
|
(process-statement (assoc-ref profiles profile) input (or output input))))
|
||||||
|
|
Loading…
Reference in a new issue