| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- (in-package #:pta-ledger)
- (defvar *default-year* nil "Initialized to current year on parsing start. Could be set with directive")
- (defvar *default-commodity* "RUB" "Could be set with directive")
- (defparameter +day+ (* 60 60 24) "day in seconds")
- (defun get-date (universal-time)
- (nreverse (subseq (multiple-value-list (decode-universal-time universal-time)) 3 6)))
- (defun get-ut (year month day)
- (encode-universal-time 0 0 0 day month year))
- (defun strip-space (text)
- (subseq text (or (position #\Space text :test-not 'eql) 0)
- (1+ (or (position #\Space text :test-not 'eql :from-end t) -1))))
- (defun .date-delimiter ()
- (.is #'member '(#\/ #\- #\.)))
- (defun .chars (&rest chars)
- (.is #'member chars))
- (defun .true (value)
- (if value (.identity value) (.fail)))
- ;; optimized (.first (.map 'list parser at-least))
- (defun .while (parser &key result-type (at-least 1))
- (lambda (input)
- (loop
- for inp = input then (input-rest inp)
- for count from 0
- until (input-empty-p inp)
- while (funcall parser inp)
- finally (return (when (>= count at-least)
- (list (cons (when result-type
- (coerce (subseq input 0 count) result-type))
- inp)))))))
- (defun .until (parser &key result-type (at-least 1))
- (.while (.not parser) :result-type result-type :at-least at-least))
- (defun .mapcar* (parser &optional skip)
- (lambda (input)
- (loop
- with results
- until (input-empty-p input)
- for result = (run parser input)
- while result
- do
- (unless skip (push (caar result) results))
- (setf input (cdar result))
- finally (return (list (cons (unless skip (nreverse results)) input))))))
- (defun .list (&rest parsers)
- (if (not parsers)
- (.fail)
- (.let* ((first (first parsers))
- (rest (if (rest parsers)
- (apply #'.list (rest parsers))
- (.identity nil))))
- (.identity (cons first rest)))))
- (defun .number (&optional (at-least 1))
- (.let* ((digits (.while (.is #'digit-char-p) :result-type 'string :at-least at-least)))
- (.identity (parse-integer digits))))
- (defun .spaces (&key (at-least 1) result-type)
- (.while (.char= #\Space) :result-type result-type :at-least at-least))
- (defun .whitespace (&key (at-least 1) result-type)
- (.while (.chars #\Space #\Tab) :result-type result-type :at-least at-least))
- (defun .eol ()
- (.or (.char= #\Newline)
- (.and (.not (.item))
- (.identity '()))))
- (defun .line-comment ()
- (.let* ((_ (.whitespace :at-least 0))
- (_ (.chars #\; #\# #\*))
- (text (.until (.char= #\Newline) :at-least 0 :result-type 'string))
- (_ (.eol)))
- (.identity (strip-space text))))
- (defun .multi-line-comment ()
- (.let* ((_ (.string= "comment"))
- (text (.until (.progn (.eol) (.string= "end comment")) :result-type 'string))
- (_ (.optional (.progn (.eol) (.string= "end comment"))))
- (_ (.eol)))
- (.identity (strip-space text))))
- (defun .comment ()
- (.let* ((_ (.char= #\;))
- (text (.until (.char= #\Newline) :at-least 0 :result-type 'string)))
- (.identity (strip-space text))))
- (defun .empty-lines ()
- (.mapcar*
- (.or (.progn (.whitespace :at-least 0) (.eol))
- (.line-comment)
- (.multi-line-comment))
- t))
- (defun split (text &optional (delimiter (.char= #\Newline)))
- (when text
- (parse (.mapcar* (.prog1 (.until delimiter :result-type 'string :at-least 0)
- (.optional delimiter))) text)))
- (defun .simple-date (&optional default-year)
- (.let* ((year (.optional (.prog1 (.number 4) (.date-delimiter))))
- (month (.prog1 (.number) (.date-delimiter)))
- (day (.number)))
- (handler-case
- (.identity (get-ut (or year default-year *default-year*) month day))
- (error () (.fail)))))
- (defun parse-date (text &optional default-year)
- (parse (.prog1 (.simple-date default-year) (.eol)) text))
- (defun .status ()
- (.is 'member '(#\! #\*)))
- (defun .code ()
- (.prog2 (.char= #\()
- (.until (.char= #\)) :result-type 'string :at-least 0)
- (.char= #\))))
- (defun .description ()
- (.let* ((text (.until (.chars #\Newline #\;) :result-type 'string)))
- (.identity (strip-space text))))
- (defun .account ()
- (.let* ((account (.until (.or (.string= " ")
- (.chars #\) #\] #\Newline)) :result-type 'string)))
- (.identity (strip-space account))))
- (defun parse-account (text)
- (parse (.prog1 (.account) (.eol)) text))
- (defun .money-number (thousands-sep decimal-sep)
- (.let* ((first-part (.while (.is 'digit-char-p) :result-type 'string))
- (rest-parts (.mapcar* (.progn (.char= thousands-sep)
- (.while (.is 'digit-char-p) :result-type 'string))))
- (decimals (.optional (.progn (.char= decimal-sep)
- (.while (.is 'digit-char-p) :result-type 'list :at-least 0)))))
- ;; One thousands separator and no decimals - is other way around, fail for other to succeceed
- (if (and (= 1 (length rest-parts))
- (null decimals))
- (.fail)
- (.identity
- (+
- (parse-integer (apply 'concatenate 'string (cons first-part rest-parts)))
- (if decimals
- (* (parse-integer (coerce decimals 'string))
- (expt 10 (- (length decimals))))
- 0))))))
- (defun .money ()
- (.plus (.money-number #\, #\.)
- (.money-number #\. #\,)))
- (defun .commodity ()
- (.or (.until (.or (.chars #\. #\, #\Space #\" #\; #\Newline #\- #\+ #\@)
- (.is 'digit-char-p))
- :result-type 'string)
- (.prog2 (.char= #\")
- (.until (.char= #\") :result-type 'string)
- (.char= #\"))))
- (defun .amount ()
- (.or
- (.let* ((sign1 (.optional (.chars #\- #\+)))
- (commodity (.prog1 (.commodity) (.optional (.whitespace))))
- (sign2 (.optional (.chars #\- #\+)))
- (quantity (.money)))
- (if (and sign1 sign2)
- (.fail)
- (.identity
- (make-amount :quantity (if (or (equal sign1 #\-)
- (equal sign2 #\-))
- (- quantity)
- quantity)
- :commodity commodity))))
- (.let* ((sign (.optional (.chars #\- #\+)))
- (quantity (.money))
- (commodity (.optional (.progn (.optional (.whitespace))
- (.commodity)))))
- (.identity
- (make-amount :quantity (if (equal sign #\-)
- (- quantity)
- quantity)
- :commodity commodity)))))
- (defun parse-amount (text)
- (parse (.prog1 (.amount) (.eol)) text))
- (defun .posting ()
- (.let* ((_ (.whitespace))
- (status (.optional (.prog1 (.status) (.optional (.spaces)))))
- (virtual (.optional (.chars #\[ #\()))
- (account (.account))
- (_ (.optional (.chars #\] #\))))
- (amount (.optional (.prog2 (.whitespace :at-least 2) (.amount) (.optional (.whitespace)))))
- (unit-price (.optional (.progn (.char= #\@)
- (.not (.char= #\@))
- (.optional (.whitespace))
- (.amount))))
- (_ (.optional (.whitespace)))
- (total-price (.optional (.progn (.string= "@@")
- (.optional (.whitespace))
- (.amount))))
- (_ (.optional (.whitespace)))
- (comment (.optional (.comment)))
- (_ (.eol))
- (comments (.mapcar* (.prog2 (.whitespace) (.comment) (.eol)))))
- (if (and unit-price total-price) (.fail)
- (.identity (make-posting
- :status status
- :virtual virtual
- :account account
- :amount amount
- :unit-price unit-price
- :total-price total-price
- :comment (when (or comment comments)
- (format nil "~{~A~^~%~}" (remove nil (list* comment comments)))))))))
- (defun .entry ()
- (.let* ((date (.simple-date))
- (secondary-date (.optional (.progn (.char= #\=)
- (.simple-date (car (get-date date))))))
- (status (.optional (.progn (.spaces) (.status))))
- (code (.optional (.progn (.spaces) (.code))))
- (_ (.optional (.whitespace)))
- (description (.optional (.description)))
- (_ (.optional (.whitespace)))
- (comment (.optional (.comment)))
- (_ (.eol))
- (comments (.mapcar* (.prog2 (.whitespace) (.comment) (.eol))))
- (postings (.mapcar* (.posting))))
- (.identity (make-entry :date date
- :secondary-date secondary-date
- :status status
- :code code
- :description description
- :comment (when (or comment comments)
- (format nil "~{~A~^~%~}" (remove nil (list* comment comments))))
- :postings postings))))
- (defun .wrap (prefix parser)
- (.let* ((value parser))
- (.identity (cons prefix value))))
- (defun .journal ()
- (.prog1
- (.mapcar*
- (.progn
- (.empty-lines)
- (.or
- (.wrap :entry (.entry))
- (.prog1
- (.or
- (.wrap :market-price
- (.progn (.char= #\P)
- (.optional (.whitespace))
- (.let* ((date (.simple-date))
- (_ (.whitespace))
- (commodity (.commodity))
- (_ (.whitespace))
- (unit-price (.amount)))
- (.identity (list date commodity unit-price)))))
- (.wrap :commodity
- (.progn (.string= "commodity")
- (.whitespace)
- (.amount)))
- (.wrap :default-commodity
- (.progn (.char= #\D)
- (.optional (.whitespace))
- (.amount)))
- (.wrap :default-year
- (.progn (.char= #\Y)
- (.optional (.whitespace))
- (.number))))
- (.optional (.whitespace))
- (.eol)))))
- (.empty-lines)))
- (defun .query-coloned (type key-parser value-parser)
- (.let* ((key key-parser)
- (_ (.char= #\:))
- (value value-parser))
- (.identity (cons type
- #'(lambda (entry posting)
- (some #'(lambda (kv)
- (or (eql kv :t)
- (funcall value kv)))
- (funcall key entry posting)))))))
- (defun .query-entryp (prefix key)
- (.progn (.string= prefix) (.identity #'(lambda (e p) (declare (ignore p)) (list (funcall key e))))))
- (defun .query-postingp (prefix key)
- (.progn (.string= prefix) (.identity #'(lambda (e p) (declare (ignore e)) (list (funcall key p))))))
- (defun .query-bothp (prefix entry-key posting-key)
- (.progn (.string= prefix)
- (.identity #'(lambda (e p)
- (list (funcall entry-key e)
- (funcall posting-key p))))))
- (defun .query-value-regex ()
- (.let* ((regex (.until (.eol) :result-type 'string)))
- (handler-case
- (let ((matcher (cl-ppcre:create-scanner regex :case-insensitive-mode t)))
- (.identity #'(lambda (value)
- (cl-ppcre:scan matcher value))))
- (error ()
- (.fail)))))
- (defun .query-value-tag-regex ()
- (.let* ((tag-name (.until (.or (.char= #\:) (.eol)) :result-type 'string))
- (tag-value (.optional (.progn (.char= #\:)
- (.until (.eol) :result-type 'string :at-least 0)))))
- (handler-case
- (let ((matcher (cl-ppcre:create-scanner (format nil "~A:~@[\\s*~A~]" tag-name tag-value)
- :case-insensitive-mode t)))
- (.identity #'(lambda (value)
- (cl-ppcre:scan matcher value))))
- (error ()
- (.fail)))))
- (defun .query-value-amount ()
- (.let* ((op (.or (.string= ">=")
- (.string= "<=")
- (.char= #\=)
- (.char= #\<)
- (.char= #\>)
- (.identity #\=)))
- (value (.number)))
- (.identity #'(lambda (v)
- (funcall (find-symbol (string op)) (abs v) value)))))
- (defun .month ()
- (.or (.progn (.or (.string= "january") (.string= "jan")) (.identity 1))
- (.progn (.or (.string= "february") (.string= "feb")) (.identity 2))
- (.progn (.or (.string= "march") (.string= "mar")) (.identity 3))
- (.progn (.or (.string= "april") (.string= "apr")) (.identity 4))
- (.progn (.string= "may") (.identity 5))
- (.progn (.or (.string= "june") (.string= "jun")) (.identity 6))
- (.progn (.or (.string= "july") (.string= "jul")) (.identity 7))
- (.progn (.or (.string= "august") (.string= "aug")) (.identity 8))
- (.progn (.or (.string= "september") (.string= "sep")) (.identity 9))
- (.progn (.or (.string= "october") (.string= "oct")) (.identity 10))
- (.progn (.or (.string= "november") (.string= "nov")) (.identity 11))
- (.progn (.or (.string= "december") (.string= "dec")) (.identity 12))))
- (defun week-start-date (universal-time &optional offset)
- (get-date (- universal-time (* +day+ (+ (nth 6 (multiple-value-list (decode-universal-time universal-time)))
- (- (* 7 (or offset 0))))))))
- (defun month-start-date (year month &optional offset)
- (multiple-value-bind (year-offset month) (floor (+ month (or offset 0) -1) 12)
- (list (+ year year-offset) (1+ month) 1)))
- (defun .smart-range (parser &optional duration)
- (.let* ((date parser))
- (destructuring-bind (&optional year month day) date
- (handler-case
- (let* ((ut (get-ut (or year (car (get-date (get-universal-time))))
- (or month 1) (or day 1)))
- (date (get-date ut)))
- (.identity (cons ut
- (if duration (+ ut duration)
- (if day (+ ut +day+)
- (if month (apply #'get-ut (month-start-date (car date) (cadr date) 1))
- (get-ut (1+ year) 1 1)))))))
- (error () (.fail))))))
- (defun .smart-date ()
- (let* ((now (get-universal-time))
- (date-now (get-date now)))
- (.or (.smart-range (.list (.prog1 (.number 4) (.date-delimiter))
- (.prog1 (.number) (.date-delimiter))
- (.number)))
- (.smart-range (.list (.prog1 (.number 4) (.date-delimiter))
- (.number)
- (.identity nil)))
- (.smart-range (.list (.number 4) (.identity nil) (.identity nil)))
- (.smart-range (.list (.identity nil)
- (.prog1 (.number) (.date-delimiter))
- (.number)))
- (.smart-range (.list (.identity nil) (.month) (.identity nil)))
- (.smart-range (.progn (.string= "today") (.identity date-now)))
- (.smart-range (.progn (.string= "yesterday") (.identity (get-date (- now +day+)))))
- (.smart-range (.progn (.string= "tomorrow") (.identity (get-date (+ now +day+)))))
- (.let* ((offset (.or (.progn (.string= "this") (.identity 0))
- (.progn (.string= "last") (.identity -1))
- (.progn (.string= "next") (.identity 1))
- (.identity 0)))
- (_ (.optional (.whitespace))))
- (.or (.smart-range (.progn (.string= "year")
- (.identity (list (+ (car date-now) offset) nil nil))))
- (.smart-range (.progn (.string= "month")
- (.identity (subseq (month-start-date (car date-now) (cadr date-now) offset)
- 0 2))))
- (.smart-range (.progn (.string= "week")
- (.identity (week-start-date now offset)))
- (* +day+ 7))
- (.progn (.true (= offset -1)) ;; Only 'lastNdays'
- (.let* ((days (.number))
- (_ (.optional (.whitespace)))
- (_ (.optional (.or (.string= "days")
- (.string= "day")
- (.string= "d")))))
- (.smart-range (.identity (get-date (- now (* days +day+))))
- (* days +day+)))))))))
- (defun .query-value-period ()
- (.let* ((period (.or (.let* ((start (.optional (.smart-date)))
- (_ (.optional (.whitespace)))
- (_ (.or (.char= #\-) (.string= "to")))
- (_ (.optional (.whitespace)))
- (end (.optional (.smart-date))))
- (if (or start end)
- (.identity (cons (car start) (car end)))
- (.fail)))
- (.smart-date))))
- (destructuring-bind (start . end) period
- (.identity #'(lambda (value)
- (and
- (or (not start) (>= value start))
- (or (not end) (< value end))))))))
- (defun .query-coloned-option (option-parser value-parser)
- (.let* ((option option-parser)
- (_ (.char= #\:))
- (value value-parser))
- (.identity (cons option value))))
- (defun .query-option (option)
- (.progn (.string= (string-downcase (symbol-name option)))
- (.identity option)))
- (defun .query-value-boolean ()
- (.let* ((value (.optional (.chars #\t #\f))))
- (.identity (ecase value
- ((nil #\t) :t)
- (#\f :f)))))
- (defun .query-term ()
- (.or (.query-coloned-option (.query-option :cost) (.query-value-boolean))
- (.query-coloned-option (.query-option :tree) (.query-value-boolean))
- (.query-coloned-option (.query-option :real) (.query-value-boolean))
- (.query-coloned :acct (.query-postingp "acct" #'posting-account)
- (.query-value-regex))
- (.query-coloned :amt (.progn (.string= "amt")
- (.identity
- #'(lambda (e p)
- (mapcar #'amount-quantity
- (get-amounts p (entry-postings e))))))
- (.query-value-amount))
- (.query-coloned :code (.query-entryp "code" #'entry-code)
- (.query-value-regex))
- (.query-coloned :cur (.progn (.string= "cur")
- (.identity
- #'(lambda (e p)
- (mapcar #'amount-commodity
- (get-amounts p (entry-postings e))))))
- (.query-value-regex))
- (.query-coloned :desc (.query-entryp "desc" #'entry-description)
- (.query-value-regex))
- (.query-coloned :date (.query-entryp "date" #'entry-date)
- (.query-value-period))
- (.query-coloned :comment (.query-bothp "comment" #'entry-comment #'posting-comment)
- (.query-value-regex))
- (.query-coloned :tag (.query-bothp "tag" #'entry-comment #'posting-comment)
- (.query-value-tag-regex))
- (.let* ((value (.query-value-regex)))
- (.identity (cons :acct
- #'(lambda (entry posting)
- (declare (ignore entry))
- (let ((account (posting-account posting)))
- (funcall value account))))))))
- (defun .arg ()
- (.let* ((word (.until (.chars #\' #\" #\Space #\Tab) :result-type 'string :at-least 0))
- (quoted (.optional (.let* ((quote (.chars #\' #\"))
- (text (.until (.char= quote) :result-type 'string :at-least 0))
- (_ (.char= quote)))
- (.identity text)))))
- (.identity (if quoted (concatenate 'string word quoted)
- word))))
- (defun make-options (terms)
- (let (options desc acct status other)
- (loop for (type . f) in terms
- do (case type
- (:desc (push f desc))
- (:comment (push f desc))
- (:acct (push f acct))
- (:status (push f status))
- ((:cost :real :tree) (setf (getf options type) f))
- (otherwise (push f other))))
- (append options
- (list :predicate
- #'(lambda (entry posting)
- (labels ((any (predicates)
- (or (not predicates)
- (find-if #'(lambda (p) (funcall p entry posting)) predicates)))
- (all (predicates)
- (not (find-if-not #'(lambda (p) (funcall p entry posting)) predicates))))
- (and (any desc) (any acct) (any status) (all other))))))))
- (defun .query ()
- (.let* ((args (.mapcar* (.prog1 (.arg) (.optional (.whitespace))))))
- (loop for arg in args with terms
- do (multiple-value-bind (pred left)
- (parse (.query-term) arg)
- (if (and pred (input-empty-p left))
- (push pred terms)
- (return (.fail))))
- finally (return (.identity (make-options terms))))))
|