pta-ledger.lisp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. (in-package #:pta-ledger)
  2. (defstruct amount quantity commodity)
  3. (defstruct posting
  4. status account amount unit-price total-price comment)
  5. (defstruct entry
  6. date secondary-date status code description comment postings)
  7. (defvar *default-year* nil "Initialized to current year on parsing start. Could be set with directive")
  8. (defvar *default-commodity* "RUB" "Could be set with directive")
  9. (defparameter +day+ (* 60 60 24) "day in seconds")
  10. (defun get-date (universal-time)
  11. (nreverse (subseq (multiple-value-list (decode-universal-time universal-time)) 3 6)))
  12. (defun get-ut (year month day)
  13. (encode-universal-time 0 0 0 day month year))
  14. (defun strip-space (text)
  15. (subseq text (or (position #\Space text :test-not 'eql) 0)
  16. (1+ (or (position #\Space text :test-not 'eql :from-end t) -1))))
  17. (defun .date-delimiter ()
  18. (.is #'member '(#\/ #\- #\.)))
  19. (defun .chars (&rest chars)
  20. (.is #'member chars))
  21. ;; optimized (.first (.map 'list parser at-least))
  22. (defun .while (parser &key result-type (at-least 1))
  23. (lambda (input)
  24. (loop
  25. for inp = input then (input-rest inp)
  26. for count from 0
  27. until (input-empty-p inp)
  28. while (funcall parser inp)
  29. finally (return (when (>= count at-least)
  30. (list (cons (when result-type
  31. (coerce (subseq input 0 count) result-type))
  32. inp)))))))
  33. (defun .until (parser &key result-type (at-least 1))
  34. (.while (.not parser) :result-type result-type :at-least at-least))
  35. (defun .mapcar* (parser &optional skip)
  36. (lambda (input)
  37. (loop
  38. with results
  39. until (input-empty-p input)
  40. for result = (run parser input)
  41. while result
  42. do
  43. (unless skip (push (caar result) results))
  44. (setf input (cdar result))
  45. finally (return (list (cons (unless skip (nreverse results)) input))))))
  46. (defun .list (&rest parsers)
  47. (if (not parsers)
  48. (.fail)
  49. (.let* ((first (first parsers))
  50. (rest (if (rest parsers)
  51. (apply #'.list (rest parsers))
  52. (.identity nil))))
  53. (.identity (cons first rest)))))
  54. (defun .number (&optional (at-least 1))
  55. (.let* ((digits (.while (.is #'digit-char-p) :result-type 'string :at-least at-least)))
  56. (.identity (parse-integer digits))))
  57. (defun .spaces (&key (at-least 1) result-type)
  58. (.while (.char= #\Space) :result-type result-type :at-least at-least))
  59. (defun .whitespace (&key (at-least 1) result-type)
  60. (.while (.chars #\Space #\Tab) :result-type result-type :at-least at-least))
  61. (defun .eol ()
  62. (.or (.char= #\Newline)
  63. (.and (.not (.item))
  64. (.identity '()))))
  65. (defun .line-comment ()
  66. (.let* ((_ (.whitespace :at-least 0))
  67. (_ (.chars #\; #\# #\*))
  68. (text (.until (.char= #\Newline) :at-least 0 :result-type 'string))
  69. (_ (.eol)))
  70. (.identity (strip-space text))))
  71. (defun .multi-line-comment ()
  72. (.let* ((_ (.string= "comment"))
  73. (text (.until (.progn (.eol) (.string= "end comment")) :result-type 'string))
  74. (_ (.optional (.progn (.eol) (.string= "end comment"))))
  75. (_ (.eol)))
  76. (.identity (strip-space text))))
  77. (defun .comment ()
  78. (.let* ((_ (.char= #\;))
  79. (text (.until (.char= #\Newline) :at-least 0 :result-type 'string)))
  80. (.identity (strip-space text))))
  81. (defun .empty-lines ()
  82. (.mapcar*
  83. (.or (.progn (.whitespace :at-least 0) (.eol))
  84. (.line-comment)
  85. (.multi-line-comment))
  86. t))
  87. (defun .simple-date (&optional default-year)
  88. (.let* ((year (.optional (.prog1 (.number 4) (.date-delimiter))))
  89. (month (.prog1 (.number) (.date-delimiter)))
  90. (day (.number)))
  91. (handler-case
  92. (.identity (get-ut (or year default-year *default-year*) month day))
  93. (error () (.fail)))))
  94. (defun .status ()
  95. (.is 'member '(#\! #\*)))
  96. (defun .code ()
  97. (.prog2 (.char= #\()
  98. (.until (.char= #\)) :result-type 'string :at-least 0)
  99. (.char= #\))))
  100. (defun .description ()
  101. (.let* ((text (.until (.chars #\Newline #\;) :result-type 'string)))
  102. (.identity (strip-space text))))
  103. (defun .account ()
  104. (.let* ((account (.until (.or (.string= " ")
  105. (.char= #\Newline)) :result-type 'string)))
  106. (.identity (strip-space account))))
  107. (defun .money-number (thousands-sep decimal-sep)
  108. (.let* ((first-part (.while (.is 'digit-char-p) :result-type 'string))
  109. (rest-parts (.mapcar* (.progn (.char= thousands-sep)
  110. (.while (.is 'digit-char-p) :result-type 'string))))
  111. (decimals (.optional (.progn (.char= decimal-sep)
  112. (.while (.is 'digit-char-p) :result-type 'list :at-least 0)))))
  113. ;; One thousands separator and no decimals - is other way around, fail for other to succeceed
  114. (if (and (= 1 (length rest-parts))
  115. (null decimals))
  116. (.fail)
  117. (.identity
  118. (+
  119. (parse-integer (apply 'concatenate 'string (cons first-part rest-parts)))
  120. (if decimals
  121. (* (parse-integer (coerce decimals 'string))
  122. (expt 10 (- (length decimals))))
  123. 0))))))
  124. (defun .money ()
  125. (.plus (.money-number #\, #\.)
  126. (.money-number #\. #\,)))
  127. (defun .commodity ()
  128. (.or (.until (.or (.chars #\. #\, #\Space #\" #\; #\Newline #\- #\+ #\@)
  129. (.is 'digit-char-p))
  130. :result-type 'string)
  131. (.prog2 (.char= #\")
  132. (.until (.char= #\") :result-type 'string)
  133. (.char= #\"))))
  134. (defun .amount ()
  135. (.or
  136. (.let* ((sign1 (.optional (.chars #\- #\+)))
  137. (commodity (.prog1 (.commodity) (.optional (.whitespace))))
  138. (sign2 (.optional (.chars #\- #\+)))
  139. (quantity (.money)))
  140. (if (and sign1 sign2)
  141. (.fail)
  142. (.identity
  143. (make-amount :quantity (if (or (equal sign1 #\-)
  144. (equal sign2 #\-))
  145. (- quantity)
  146. quantity)
  147. :commodity commodity))))
  148. (.let* ((sign (.optional (.chars #\- #\+)))
  149. (quantity (.money))
  150. (commodity (.optional (.progn (.optional (.whitespace))
  151. (.commodity)))))
  152. (.identity
  153. (make-amount :quantity (if (equal sign #\-)
  154. (- quantity)
  155. quantity)
  156. :commodity commodity)))))
  157. (defun .posting ()
  158. (.let* ((_ (.whitespace))
  159. (status (.optional (.prog1 (.status) (.optional (.spaces)))))
  160. (account (.account))
  161. (amount (.optional (.prog2 (.whitespace :at-least 2) (.amount) (.optional (.whitespace)))))
  162. (unit-price (.optional (.progn (.char= #\@)
  163. (.not (.char= #\@))
  164. (.optional (.whitespace))
  165. (.amount))))
  166. (_ (.optional (.whitespace)))
  167. (total-price (.optional (.progn (.string= "@@")
  168. (.optional (.whitespace))
  169. (.amount))))
  170. (_ (.optional (.whitespace)))
  171. (comment (.optional (.comment)))
  172. (_ (.eol))
  173. (comments (.mapcar* (.prog2 (.whitespace) (.comment) (.eol)))))
  174. (if (and unit-price total-price) (.fail)
  175. (.identity (make-posting
  176. :status status
  177. :account account
  178. :amount amount
  179. :unit-price unit-price
  180. :total-price total-price
  181. :comment (when (or comment comments)
  182. (format nil "~{~A~^~%~}" (remove nil (list* comment comments)))))))))
  183. (defun .entry ()
  184. (.let* ((date (.simple-date))
  185. (secondary-date (.optional (.progn (.char= #\=)
  186. (.simple-date (car (get-date date))))))
  187. (status (.optional (.progn (.spaces) (.status))))
  188. (code (.optional (.progn (.spaces) (.code))))
  189. (_ (.optional (.whitespace)))
  190. (description (.optional (.description)))
  191. (_ (.optional (.whitespace)))
  192. (comment (.optional (.comment)))
  193. (_ (.eol))
  194. (comments (.mapcar* (.prog2 (.whitespace) (.comment) (.eol))))
  195. (postings (.mapcar* (.posting))))
  196. (.identity (make-entry :date date
  197. :secondary-date secondary-date
  198. :status status
  199. :code code
  200. :description description
  201. :comment (when (or comment comments)
  202. (format nil "~{~A~^~%~}" (remove nil (list* comment comments))))
  203. :postings postings))))
  204. (defun .wrap (prefix parser)
  205. (.let* ((value parser))
  206. (.identity (cons prefix value))))
  207. (defun .journal ()
  208. (.prog1
  209. (.mapcar*
  210. (.progn
  211. (.empty-lines)
  212. (.or
  213. (.wrap :entry (.entry))
  214. (.prog1
  215. (.or
  216. (.wrap :market-price
  217. (.progn (.char= #\P)
  218. (.optional (.whitespace))
  219. (.let* ((date (.simple-date))
  220. (_ (.whitespace))
  221. (commodity (.commodity))
  222. (_ (.whitespace))
  223. (unit-price (.amount)))
  224. (.identity (list date commodity unit-price)))))
  225. (.wrap :commodity
  226. (.progn (.string= "commodity")
  227. (.whitespace)
  228. (.amount)))
  229. (.wrap :default-commodity
  230. (.progn (.char= #\D)
  231. (.optional (.whitespace))
  232. (.amount)))
  233. (.wrap :default-year
  234. (.progn (.char= #\Y)
  235. (.optional (.whitespace))
  236. (.number))))
  237. (.optional (.whitespace))
  238. (.eol)))))
  239. (.empty-lines)))
  240. (define-condition journal-failed (error)
  241. ((position :initarg :position :reader parse-error-pos)
  242. (left-data :initarg :left-data :reader parse-error-left))
  243. (:report (lambda (condition stream)
  244. (with-slots (position left-data) condition
  245. (format stream "Ledger parse failed at position ~A~@[, starting from ~A~]" position left-data)))))
  246. (defun parse-journal (str)
  247. (let ((*default-year* (car (get-date (get-universal-time)))))
  248. (multiple-value-bind (result left rest)
  249. (parse (.journal) str)
  250. (cond
  251. ((null result) (error 'journal-failed :left-data (subseq str 0 (min 200 (length str)))))
  252. ((not (string= "" left)) (error 'journal-failed
  253. :position (- (length str)
  254. (length left))
  255. :left-data (subseq left 0 (min 200 (length left)))))
  256. ((not (null rest)) (error 'journal-failed :position (length str)))
  257. (:otherwise result)))))
  258. (defun .query-coloned (type key-parser value-parser)
  259. (.let* ((key key-parser)
  260. (_ (.char= #\:))
  261. (value value-parser))
  262. (.identity (cons type
  263. #'(lambda (entry posting)
  264. (let ((key-value (funcall key entry posting)))
  265. (or (eql key-value :t)
  266. (funcall value key-value))))))))
  267. (defun .query-entryp (prefix key)
  268. (.progn (.string= prefix) (.identity #'(lambda (e p) (declare (ignore p)) (funcall key e)))))
  269. (defun .query-postingp (prefix key)
  270. (.progn (.string= prefix) (.identity #'(lambda (e p) (declare (ignore e)) (funcall key p)))))
  271. (defun .query-value-regex ()
  272. (.let* ((regex (.until (.eol) :result-type 'string)))
  273. (handler-case
  274. (let ((matcher (cl-ppcre:create-scanner regex :case-insensitive-mode t)))
  275. (.identity #'(lambda (value)
  276. (cl-ppcre:scan matcher value))))
  277. (error ()
  278. (.fail)))))
  279. (defun .query-value-amount ()
  280. (.let* ((op (.or (.string= ">=")
  281. (.string= "<=")
  282. (.char= #\=)
  283. (.char= #\<)
  284. (.char= #\>)
  285. (.identity #\=)))
  286. (value (.number)))
  287. (.identity #'(lambda (v)
  288. (funcall (find-symbol (string op)) v value)))))
  289. (defun .month ()
  290. (.or (.progn (.or (.string= "january") (.string= "jan")) (.identity 1))
  291. (.progn (.or (.string= "february") (.string= "feb")) (.identity 2))
  292. (.progn (.or (.string= "march") (.string= "mar")) (.identity 3))
  293. (.progn (.or (.string= "april") (.string= "apr")) (.identity 4))
  294. (.progn (.string= "may") (.identity 5))
  295. (.progn (.or (.string= "june") (.string= "jun")) (.identity 6))
  296. (.progn (.or (.string= "july") (.string= "jul")) (.identity 7))
  297. (.progn (.or (.string= "august") (.string= "aug")) (.identity 8))
  298. (.progn (.or (.string= "september") (.string= "sep")) (.identity 9))
  299. (.progn (.or (.string= "october") (.string= "oct")) (.identity 10))
  300. (.progn (.or (.string= "november") (.string= "nov")) (.identity 11))
  301. (.progn (.or (.string= "december") (.string= "dec")) (.identity 12))))
  302. (defun week-start-date (universal-time &optional offset)
  303. (get-date (- universal-time (* +day+ (+ (nth 6 (multiple-value-list (decode-universal-time universal-time)))
  304. (- (* 7 (or offset 0))))))))
  305. (defun month-start-date (year month &optional offset)
  306. (multiple-value-bind (year-offset month) (floor (+ month (or offset 0) -1) 12)
  307. (list (+ year year-offset) (1+ month) 1)))
  308. (defun .smart-range (parser &optional duration)
  309. (.let* ((date parser))
  310. (destructuring-bind (&optional year month day) date
  311. (handler-case
  312. (let* ((ut (get-ut (or year (car (get-date (get-universal-time))))
  313. (or month 1) (or day 1)))
  314. (date (get-date ut)))
  315. (.identity (cons ut
  316. (if duration (+ ut duration)
  317. (if day (+ ut +day+)
  318. (if month (apply #'get-ut (month-start-date (car date) (cadr date) 1))
  319. (get-ut (1+ year) 1 1)))))))
  320. (error () (.fail))))))
  321. (defun .smart-date ()
  322. (let* ((now (get-universal-time))
  323. (date-now (get-date now)))
  324. (.or (.smart-range (.list (.prog1 (.number 4) (.date-delimiter))
  325. (.prog1 (.number) (.date-delimiter))
  326. (.number)))
  327. (.smart-range (.list (.prog1 (.number 4) (.date-delimiter))
  328. (.number)
  329. (.identity nil)))
  330. (.smart-range (.list (.number 4) (.identity nil) (.identity nil)))
  331. (.smart-range (.list (.identity nil)
  332. (.prog1 (.number) (.date-delimiter))
  333. (.number)))
  334. (.smart-range (.list (.identity nil) (.month) (.identity nil)))
  335. (.smart-range (.progn (.string= "today") (.identity date-now)))
  336. (.smart-range (.progn (.string= "yesterday") (.identity (get-date (- now +day+)))))
  337. (.smart-range (.progn (.string= "tomorrow") (.identity (get-date (+ now +day+)))))
  338. (.let* ((offset (.or (.progn (.string= "this") (.identity 0))
  339. (.progn (.string= "last") (.identity -1))
  340. (.progn (.string= "next") (.identity 1))
  341. (.identity 0)))
  342. (_ (.optional (.whitespace))))
  343. (.or (.smart-range (.progn (.string= "year")
  344. (.identity (list (+ (car date-now) offset) nil nil))))
  345. (.smart-range (.progn (.string= "month")
  346. (.identity (subseq (month-start-date (car date-now) (cadr date-now) offset)
  347. 0 2))))
  348. (.smart-range (.progn (.string= "week")
  349. (.identity (week-start-date now offset)))
  350. (* +day+ 7)))))))
  351. (defun .query-value-period ()
  352. (.let* ((period (.or (.let* ((start (.optional (.smart-date)))
  353. (_ (.optional (.whitespace)))
  354. (_ (.or (.char= #\-) (.string= "to")))
  355. (_ (.optional (.whitespace)))
  356. (end (.optional (.smart-date))))
  357. (if (or start end)
  358. (.identity (cons (car start) (car end)))
  359. (.fail)))
  360. (.smart-date))))
  361. (destructuring-bind (start . end) period
  362. (.identity #'(lambda (value)
  363. (and
  364. (or (not start) (>= value start))
  365. (or (not end) (< value end))))))))
  366. (defun .query-term ()
  367. (.or (.query-coloned :acct (.query-postingp "acct" #'posting-account)
  368. (.query-value-regex))
  369. (.query-coloned :amt (.progn (.string= "amt")
  370. (.identity
  371. #'(lambda (e p)
  372. (let ((amounts (get-amounts p (entry-postings e))))
  373. (if (> (length amounts) 1) :t
  374. (amount-quantity (car amounts)))))))
  375. (.query-value-amount))
  376. (.query-coloned :code (.query-entryp "code" #'entry-code)
  377. (.query-value-regex))
  378. (.query-coloned :cur (.progn (.string= "cur")
  379. (.identity
  380. #'(lambda (e p)
  381. (get-amounts p (entry-postings e)))))
  382. (.let* ((regexp (.query-value-regex)))
  383. (.identity #'(lambda (amounts)
  384. (find-if regexp amounts :key #'amount-commodity)))))
  385. (.query-coloned :desc (.query-entryp "desc" #'entry-description)
  386. (.query-value-regex))
  387. (.query-coloned :date (.query-entryp "date" #'entry-date)
  388. (.query-value-period))
  389. (.let* ((value (.query-value-regex)))
  390. (.identity (cons :acct
  391. #'(lambda (entry posting)
  392. (declare (ignore entry))
  393. (let ((account (posting-account posting)))
  394. (funcall value account))))))))
  395. (defun .arg ()
  396. (.let* ((word (.until (.chars #\' #\" #\Space #\Tab) :result-type 'string :at-least 0))
  397. (quoted (.optional (.let* ((quote (.chars #\' #\"))
  398. (text (.until (.char= quote) :result-type 'string :at-least 0))
  399. (_ (.char= quote)))
  400. (.identity text)))))
  401. (.identity (if quoted (concatenate 'string word quoted)
  402. word))))
  403. (defun make-query-predicate (terms)
  404. (let (desc acct status other)
  405. (loop for (type . f) in terms
  406. do (case type
  407. (:desc (push f desc))
  408. (:acct (push f acct))
  409. (:status (push f status))
  410. (otherwise (push f other))))
  411. #'(lambda (entry posting)
  412. (labels ((any (predicates)
  413. (or (not predicates)
  414. (find-if #'(lambda (p) (funcall p entry posting)) predicates)))
  415. (all (predicates)
  416. (not (find-if-not #'(lambda (p) (funcall p entry posting)) predicates))))
  417. (and (any desc) (any acct) (any status) (all other))))))
  418. (defun .query ()
  419. (.let* ((args (.mapcar* (.prog1 (.arg) (.optional (.whitespace))))))
  420. (loop for arg in args with terms
  421. do (multiple-value-bind (pred left)
  422. (parse (.query-term) arg)
  423. (if (and pred (input-empty-p left))
  424. (push pred terms)
  425. (return (.fail))))
  426. finally (return (.identity (make-query-predicate terms))))))
  427. (defun parse-query (query)
  428. (let ((predicate (parse (.query) query)))
  429. (unless predicate
  430. (error "bad query: ~A" query))
  431. predicate))
  432. (defun get-commodity (amount)
  433. (let ((com (amount-commodity amount)))
  434. (cond
  435. ((null com) *default-commodity*)
  436. ((equal com "$") "USD")
  437. ((equal com "€") "EUR")
  438. (t com))))
  439. (defun add-amounts! (sum amounts)
  440. (dolist (a amounts sum)
  441. (let* ((commodity (get-commodity a))
  442. (same (find commodity sum :key #'amount-commodity :test #'equal)))
  443. (unless same (setf same (car (push (make-amount :quantity 0 :commodity commodity)
  444. sum))))
  445. (incf (amount-quantity same)
  446. (amount-quantity a)))))
  447. (defun complement-amounts (postings &optional cost)
  448. (let ((sum
  449. (reduce #'add-amounts! postings :initial-value nil
  450. :key #'(lambda (p) (when (posting-amount p)
  451. (get-amounts p cost))))))
  452. (dolist (a sum sum)
  453. (setf (amount-quantity a)
  454. (- (amount-quantity a))))))
  455. (defun get-amounts (posting postings &optional cost)
  456. (let ((amount (posting-amount posting))
  457. (unit-price (posting-unit-price posting))
  458. (total-price (posting-total-price posting)))
  459. (if amount
  460. (if cost
  461. (if total-price (list total-price)
  462. (if unit-price
  463. (list (make-amount :commodity (amount-commodity unit-price)
  464. :quantity (* (amount-quantity amount)
  465. (amount-quantity unit-price))))
  466. (list amount)))
  467. (list amount))
  468. (complement-amounts postings cost))))
  469. (defun entries (journal)
  470. (mapcar #'cdr (remove :entry journal :key #'car :test-not #'eql)))
  471. (defun balance (entries &key predicate cost)
  472. (let ((balance (make-hash-table :test #'equal)))
  473. (dolist (entry entries balance)
  474. (dolist (posting (entry-postings entry))
  475. (when (or (null predicate)
  476. (funcall predicate entry posting))
  477. (let* ((account (posting-account posting))
  478. (amounts (get-amounts posting (entry-postings entry) cost)))
  479. (setf (gethash account balance)
  480. (add-amounts! (gethash account balance) amounts)
  481. (gethash "TOTALS" balance)
  482. (add-amounts! (gethash "TOTALS" balance) amounts))))))))
  483. (defun format-balance (balance)
  484. (let* ((accounts (alexandria:hash-table-keys balance))
  485. (max-account-length (apply #'max (mapcar #'length accounts))))
  486. (with-output-to-string (s)
  487. (loop for account in (sort accounts #'string<)
  488. for amounts = (gethash account balance)
  489. when (find 0 amounts :test-not #'= :key #'amount-quantity)
  490. do (format s "~vA ~$ ~A~{~%~va ~$ ~A~}~%" max-account-length account
  491. (amount-quantity (car amounts)) (amount-commodity (car amounts))
  492. (apply #'append (mapcar #'(lambda (a)
  493. (list max-account-length ""
  494. (amount-quantity a)
  495. (amount-commodity a)))
  496. (rest amounts))))))))
  497. (defun journal-balance (journal &optional query)
  498. (format-balance (balance (entries journal)
  499. :predicate (when query (parse-query query)))))