Created
September 8, 2024 12:15
-
-
Save kolja/47cbe101e3d84aebf1342195bf58ad38 to your computer and use it in GitHub Desktop.
Account data (csv) pretty printer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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