payroll-import/src/parse.clj

85 lines
2.6 KiB
Clojure
Raw Normal View History

(ns parse
(:require [clojure.spec.alpha :as s]
[clojure.walk :as walk]))
2024-02-20 18:26:45 +11:00
(s/def ::token (s/+ (set "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890-:")))
(s/def ::number (s/+ (set "01234567890-.")))
(s/def ::quoted-token (s/cat
:_ #{\"}
:token (s/+ (set "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890-:./% ()"))
:_ #{\"}))
(s/def ::whitespace (s/+ #{\space}))
(s/def ::meta
(s/cat :_ ::whitespace
:key ::token
:_ #{\:}
:_ ::whitespace
:value ::quoted-token
:_ #{\newline}))
(s/def ::posting
(s/cat :_ ::whitespace
:account ::token
:_ ::whitespace
2024-02-20 18:26:45 +11:00
:amount ::number
:_ ::whitespace
:currency ::token
:_ #{\newline}
:meta (s/* ::meta)))
(s/def ::transaction
(s/cat
:date ::token
:_ ::whitespace
:_ ::token
:_ ::whitespace
:payee (s/? ::quoted-token)
:_ (s/? ::whitespace)
:desc ::quoted-token
:_ #{\newline}
:meta (s/* ::meta)
:postings (s/* ::posting)
:_ (s/* #{\newline})))
2024-02-20 14:55:14 +11:00
(s/def ::transactions (s/+ ::transaction))
(defn kv->map
2024-02-20 14:55:14 +11:00
"Convert vector [{:key \"x\" :value \"y\"}] to {:x y}"
[vec]
(into {} (for [{:keys [key value]} vec]
[(keyword key) value])))
(defn convert-parse-tree
"Reformat the parse tree into a similar data structure as used during import."
[tree]
(walk/postwalk #(cond
;; vector of chars to string
(and (vector? %)
(= (type (first %)) java.lang.Character)) (apply str %)
;; posting amount to bigdec
(and (= (type %) clojure.lang.PersistentArrayMap)
(contains? % :amount)) (dissoc (update % :amount bigdec) :_)
;; flatten quoted-tokens
(and (= (type %) clojure.lang.PersistentArrayMap)
(contains? % :token)) (:token %)
;; drop :_ keys
(= (type %) clojure.lang.PersistentArrayMap) (dissoc % :_)
2024-02-20 14:55:14 +11:00
;; convert vector of :key/:value maps to a map
(and (vector? %)
(= (type (first %)) clojure.lang.PersistentArrayMap)
(contains? (first %) :key)) (kv->map %)
:else %)
tree))
(defn parse
"Parse a Beancount transaction.
Use so we can convert the data generated during import with the hand-generated
Beancount transactions."
[text]
(let [tree (s/conform ::transactions (conj (vec text) \newline))]
(convert-parse-tree tree)))