Skip to content

Instantly share code, notes, and snippets.

@kolja
Created September 8, 2024 12:15
Show Gist options
  • Save kolja/47cbe101e3d84aebf1342195bf58ad38 to your computer and use it in GitHub Desktop.
Save kolja/47cbe101e3d84aebf1342195bf58ad38 to your computer and use it in GitHub Desktop.
Account data (csv) pretty printer
#!/usr/bin/env bb
(ns Konto
(:require [clojure.string :as str]
[babashka.process :refer [shell]]
[clojure.pprint :refer [cl-format]]
[babashka.cli :as cli]
[babashka.fs :as fs]
[clojure.data.csv :as csv]
[clojure.java.io :as io]))
(def cli-spec {:spec
{:help {:alias :h :desc "Show help"}
:info {:alias :i :desc "also print info (Verwendungszweck)"}
:color {:alias :c :desc "color output"}
:eingang {:alias :e :desc "show all incoming transactions"}
:ausgang {:alias :a :desc "show all outgoing transactions"}}})
(def key {:datum 0 ;; Buchungsdatum
:Wertstellung 1
:Status 2
:bezahler 3 ;; Zahlungspflichtige*r
:empfänger 4 ;; Zahlungsempfänger*in
:Verwendungszweck 5
:typ 6 ;; Umsatztyp
:IBAN 7
:betrag 8
:Gläubiger-ID 9
:Mandatsreferenz 10
:Kundenreferenz 11})
(defn usage [spec]
(->> [""
"Account data (CSV) pretty printer"
""
(format " %-7s %s <csv-file> <filter> <options>" "usage:" (fs/file-name *file*))
(format " %-7s cat <data.csv> | %s <options>" "or:" (fs/file-name *file*))
""
"If no filter is given, all transactions are printed."
"<csv-file> will be looked for in KONTO_DIR or (if that is not set) in the current directory."
""
" Options:"
""
(cli/format-opts spec)
""]
(str/join \newline)))
(defn exit [message code]
(if (= code 0)
(println message)
(binding [*out* *err*] (println message)))
(System/exit code))
(defn parse-csv [input]
(with-open [reader (io/reader input)]
(doall (csv/read-csv reader :separator \;))))
(defn compact-spaces [s]
(str/replace s #"\s{2,}" " "))
(defn to-cent [s]
(let [negative? (str/starts-with? s "-")
parts (-> s
(str/replace #"[-\.]" "")
(str/split #"\,"))
;; use Long/parseLong if you expect to be making more than EUR 21.474.836,47
integers (map #(Integer/parseInt %) parts)
cents (+ (* 100 (first integers))
(or (second integers) 0))]
(if negative? (* -1 cents) cents)
))
(defn bold [s] (str "\033[1m" s "\033[0m"))
(defn colored-text [color-code text]
(if color-code
(str "\033[" color-code "m" text "\033[0m")
text))
(def red (partial colored-text 31))
(def green (partial colored-text 32))
(def no-color (partial colored-text nil))
(defn format-euro [cents]
(let [neg (neg? cents)
euro (quot (abs cents) 100)
cent (mod (abs cents) 100)]
(if (zero? cent)
(format "%6d,--" (if neg (* -1 euro) euro) )
(format "%6d,%02d" (if neg (* -1 euro) euro) cent ))))
(defn env2path [env dir]
(let [path (System/getenv env)]
(if (nil? path)
dir
(if (fs/exists? path)
(fs/real-path path)
(exit (str (bold "Error:") "The path " env " = " path "does not exist.") 1)))))
(defn -main [args]
(let [parsed (cli/parse-args args cli-spec)
{[filename filterstring] :args {:keys [help eingang ausgang] :as opts} :opts} parsed]
(when help (exit (usage cli-spec) 0))
(let [
input-file (str (env2path "KONTO_DIR" ".") "/" filename)
input (cond
(.ready *in*) *in*
(fs/regular-file? input-file) (if filterstring
(:out (shell {:out :stream} "rg" "-i" filterstring input-file))
(io/reader input-file))
:else (exit (if filename
(str "no input via stdin and no file found at " input-file)
(str "no input via stdin and no csv-file specified")) 1))
data (filter #(= (get % (key :Status)) "Gebucht") (parse-csv input))
sum (reduce + (map (fn [line] (to-cent (line (key :betrag)))) data))]
(when eingang
(let [ein (->> data
(filter #(= (get % (key :typ)) "Eingang"))
(map #(get % (key :bezahler)))
distinct)]
(exit (str/join "\n" ein) 0)))
(when ausgang
(let [aus (->> data
(filter #(= (get % (key :typ)) "Ausgang"))
(map #(get % (key :empfänger)))
distinct)]
(exit (str/join "\n" aus) 0)))
(doseq [row data]
(let [
datum (row (key :datum))
typ (row (key :typ))
cent-betrag (to-cent (row (key :betrag)))
empfänger (compact-spaces (row (key :empfänger)))
bezahler (compact-spaces (row (key :bezahler)))
info (if (get opts :info) (str "\t" (row (key :Verwendungszweck))) "")
color-fn (if (get opts :color) (if (= typ "Ausgang") red green) no-color)]
(print (if (= (row (key :typ)) "Ausgang")
(cl-format nil "~@a ~@a -> ~@a ~@a\n" datum (color-fn (format-euro cent-betrag)) empfänger info)
(cl-format nil "~@a ~@a <- ~@a ~@a\n" datum (color-fn (format-euro cent-betrag)) bezahler info)
))))
(print (cl-format nil "---\nSumme:~12@a €\n" (format-euro sum))))))
(-main *command-line-args*)
;# vim:ft=clojure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment