AtlatestRepositorysigil-ledger

sigil-ledger / tree / src / ledgerreport.sgl

1;;; (ledger report) - hledger CLI Report Generation
2;;;
3;;; Run hledger CLI commands and parse their output for reports.
4;;; Uses `--output-format json` where available for structured data.
5;;;
6;;; ## Basic Usage
7;;;
8;;; ```scheme
9;;; (import (ledger report))
10;;;
11;;; ;; Get account balances
12;;; (hledger-balance "main.journal" "assets")
13;;;
14;;; ;; Get transaction register
15;;; (hledger-register "main.journal" "expenses" begin: "2026-01")
16;;;
17;;; ;; Income statement
18;;; (hledger-income-statement "main.journal" period: "monthly")
19;;;
20;;; ;; Run any hledger command
21;;; (hledger-command "accounts" file: "main.journal")
22;;; ```
24(define-library (ledger report)
25 (import (sigil string)
26 (sigil io)
27 (sigil process)
28 (sigil json))
29 (export
30 hledger-available?
31 hledger-command
32 hledger-balance
33 hledger-register
34 hledger-income-statement
35 hledger-print-json)
37 (begin
39 ;;; Check if hledger is available on PATH.
40 (define (hledger-available?)
41 (command-exists? "hledger"))
43 (define (ensure-hledger!)
44 (unless (hledger-available?)
45 (error "hledger is not installed or not on PATH")))
47 ;;; Run an arbitrary hledger command and return its output as a string.
48 ;;;
49 ;;; The `file:` keyword specifies the journal file.
50 ;;; Additional arguments are passed through to hledger.
51 ;;;
52 ;;; ```scheme
53 ;;; (hledger-command "accounts" file: "main.journal")
54 ;;; ; => "assets:checking\nassets:savings\n..."
55 ;;;
56 ;;; (hledger-command "bal" file: "main.journal" args: '("assets" "--tree"))
57 ;;; ; => balance report as text
58 ;;; ```
59 (define (hledger-command command (keys: (file #f) (args '())))
60 (ensure-hledger!)
61 (let ((cmd-args (if file
62 (cons command (cons "-f" (cons file args)))
63 (cons command args))))
64 (apply process-output->string "hledger" cmd-args)))
66 ;; Internal: run a report command with parsed query/option args
67 (define (run-report command file rest)
68 (ensure-hledger!)
69 (let* ((query-and-opts (parse-report-args rest))
70 (query (car query-and-opts))
71 (opts (cdr query-and-opts))
72 (args (build-report-args command file query opts)))
73 (apply process-output->string "hledger" args)))
75 ;;; Run `hledger bal` and return the output.
76 ;;;
77 ;;; ```scheme
78 ;;; (hledger-balance "main.journal")
79 ;;; (hledger-balance "main.journal" "assets")
80 ;;; (hledger-balance "main.journal" "expenses"
81 ;;; begin: "2026-01" end: "2026-04" depth: 2 tree: #t)
82 ;;; ```
83 (define (hledger-balance file . rest)
84 (run-report "bal" file rest))
86 ;;; Run `hledger reg` and return the output.
87 ;;;
88 ;;; ```scheme
89 ;;; (hledger-register "main.journal" "expenses:food")
90 ;;; (hledger-register "main.journal" begin: "2026-03")
91 ;;; ```
92 (define (hledger-register file . rest)
93 (run-report "reg" file rest))
95 ;;; Run `hledger is` (income statement) and return the output.
96 ;;;
97 ;;; ```scheme
98 ;;; (hledger-income-statement "main.journal")
99 ;;; (hledger-income-statement "main.journal" period: "monthly")
100 ;;; ```
101 (define (hledger-income-statement file . rest)
102 (run-report "is" file rest))
104 ;;; Run `hledger print -O json` and return parsed JSON.
105 ;;;
106 ;;; Returns the hledger JSON representation of transactions,
107 ;;; parsed into Sigil dicts/arrays.
108 ;;;
109 ;;; ```scheme
110 ;;; (define txns (hledger-print-json "main.journal"))
111 ;;; ```
112 (define (hledger-print-json file . rest)
113 (ensure-hledger!)
114 (let* ((query-and-opts (parse-report-args rest))
115 (query (car query-and-opts))
116 (opts (cdr query-and-opts))
117 (args (build-report-args "print" file query
118 (cons (cons output-format: "json") opts))))
119 (let ((output (apply process-output->string "hledger" args)))
120 (if (string-empty? (string-trim output))
121 #[]
122 (json-decode output)))))
124 ;; Parse variadic report arguments into (query . opts) pair
125 (define (parse-report-args args)
126 (let loop ((rest args) (query '()) (opts '()))
127 (if (null? rest)
128 (cons (reverse query) (reverse opts))
129 (let ((arg (car rest)))
130 (cond
131 ((keyword? arg)
132 (if (null? (cdr rest))
133 (cons (reverse query) (reverse opts))
134 (loop (cddr rest) query
135 (cons (cons arg (cadr rest)) opts))))
136 ((string? arg)
137 (loop (cdr rest) (cons arg query) opts))
138 (else
139 (loop (cdr rest) query opts)))))))
141 ;; Build hledger argument list from parsed report options
142 (define (build-report-args command file query opts)
143 (let ((base (list command "-f" file)))
144 (let ((with-opts (fold-right
145 (lambda (opt acc)
146 (let ((key (car opt))
147 (val (cdr opt)))
148 (cond
149 ((eq? key begin:)
150 (cons "-b" (cons val acc)))
151 ((eq? key end:)
152 (cons "-e" (cons val acc)))
153 ((eq? key period:)
154 (cons "-p" (cons val acc)))
155 ((eq? key depth:)
156 (cons "--depth" (cons (if (number? val) (number->string val) val) acc)))
157 ((eq? key tree:)
158 (if val (cons "--tree" acc) acc))
159 ((eq? key monthly:)
160 (if val (cons "-M" acc) acc))
161 ((eq? key quarterly:)
162 (if val (cons "-Q" acc) acc))
163 ((eq? key yearly:)
164 (if val (cons "-Y" acc) acc))
165 ((eq? key output-format:)
166 (cons "-O" (cons val acc)))
167 ((eq? key cost:)
168 (if val (cons "-B" acc) acc))
169 ((eq? key market:)
170 (if val (cons "-V" acc) acc))
171 ((eq? key exchange:)
172 (cons "-X" (cons val acc)))
173 (else acc))))
174 '()
175 opts)))
176 (append base with-opts query))))))