git.fiddlerwoaroof.com
parser.lisp
d2878c38
 (in-package #:tempores.parser)
9ba1ff14
 
 (defun .digit ()
   (.is #'digit-char-p))
 
 (defun .first-hour-char ()
   (.is (lambda (x)
          (member x '(#\0 #\1 #\2)))))
 
 (defun .first-minute-char ()
   (.is (lambda (x)
          (member x '(#\0 #\1 #\2 #\3 #\4 #\5)))))
 
 (defun .time-separator ()
   (.char= #\:))
 
34aff00c
 ; TODO: consider adding one-digit hours
9ba1ff14
 (defun .hour ()
   (.let* ((f (.first-hour-char))
           (s (.digit)))
     (if (or (char/= f #\2) (member s '(#\0 #\1 #\2 #\3))) ;; make sure we don't get 24
34aff00c
       (.identity (coerce (vector f s) 'string))
9ba1ff14
       (.fail))))
 
 (defun .minute-or-second ()
   (.let* ((f (.first-minute-char))
           (s (.digit)))
34aff00c
     (.identity (coerce (vector f s) 'string))))
9ba1ff14
 
 (defun .time-range-separator ()
   (.string= "--"))
 
34aff00c
 (define-condition invalid-time (parsing-error) ()
   (:report (lambda (condition stream)
              (format stream "Not a valid time part ~s"
                      (failed-chunk condition)))))
 
 (defun .valid-time ()
9ba1ff14
   (.let* ((hour (.hour))
           (_ (.time-separator))
           (minute (.minute-or-second))
           (second (.optional
                     (.progn (.time-separator)
                             (.minute-or-second)))))
34aff00c
     (.identity (list hour minute (if second second "0")))))
 
 (defun .invalid-time ()
   (flet ((.non-time-character ()
            (.map 'string
                  (.and (.not (.or (.time-separator)
                                   (.char= #\newline)
                                   (.char= #\space)))
                        (.item)))))
     (.let* ((hour (.or (.hour)
                        (.non-time-character)))
             (_ (.time-separator))
             (minute (.or (.minute-or-second)
                          (.non-time-character)))
             (second (.optional
                       (.progn (.time-separator)
                               (.or (.minute-or-second)
                                    (.non-time-character))))))
       (error 'invalid-time
         :failed-chunk (concatenate 'string
                                    (string hour) ":"
                                    (string minute)
                                    (if second
                                      (concatenate 'string ":" (string second))
                                      ""))))))
 
 (defun .time ()
   (.let* ((time (.or (.valid-time) (.invalid-time))))
     (.identity
       (apply #'make-time-obj
              (mapcar #'parse-integer
                      time)))))
9ba1ff14
 
 (defun .time-unit ()
   (.or
51faefd0
     (.string= "mins")
9ba1ff14
     (.string= "hrs")
     (.string= "min")
     (.string= "hr")))
 
 (defun .time-mod ()
   (.let* ((sign (.or (.char= #\+) (.char= #\-)))
           (num (.first (.map 'string (.digit))))
           (unit (.time-unit)))
     (.identity
       (make-time-mod
         (parse-integer
           (with-output-to-string (s)
             (princ sign s)
             (princ num s)))
         unit))))
 
 (defun .peek (parser)
   (lambda (input)
     (if (run parser input)
       (list (cons t input))
       (run (.fail) input))))
 
 (defun .time-range ()
   (.let* ((start (.time))
51faefd0
           (_ (.prog1
9ba1ff14
                (.time-range-separator)
                (.peek (.not (.char= #\,)))))
           (done (.optional (.time)))
           (mod (.optional (.time-mod))))
     (when (and mod (not done))
       (.fail))
     (if done
       (if mod
         (.identity
           (list start done mod))
         (.identity (list start done)))
       (.identity (list start)))))
 
 (defun .zero-or-more (parser)
   (.plus (.let* ((x parser)
                  (xs (.zero-or-more parser)))
            (.identity (cons x xs)))
          (.identity ())))
 
 (defun .range-list-separator ()
   (.char= #\,))
 
 (defun .range-list ()
   (.let*
     ((ranges (.prog1
                (.map 'list
                      (.prog1 (.time-range)
                              (.optional (.progn (.range-list-separator)
                                                 (.zero-or-more (.char= #\Space))))))
                (.char= #\Newline))))
     (let ((lengths (map 'vector #'length ranges)))
       (if (/= 0 (elt lengths (1- (length lengths))))
         (.identity ranges)
         (.fail)))))
 
34aff00c
 (defun .whitespace-char ()
   (.or (.char= #\tab) (.char= #\space)))
 
 (defun .whitespace ()
   (.map 'string (.whitespace-char)))
 
 (defun .valid-initial-space ()
   (.or (.string= (string #\tab))
        (.string= "   ")))
 
 (defun .extra-whitespace ()
   (.let* ((_ (.valid-initial-space))
           (extra-space (.optional (.whitespace))))
     (if extra-space
       (error 'invalid-whitespace :failed-chunk extra-space)
af9f5f80
       (.fail))))
34aff00c
 
9ba1ff14
 (defun .initial-space ()
34aff00c
   (.or (.extra-whitespace)
        (.valid-initial-space)))
9ba1ff14
 
 (defun .time-line-start ()
   (.progn (.initial-space)
           (.string= "start@")))
 
 (defun .time-line ()
   (.progn
     (.time-line-start)
     (.range-list)))
 
 (defun .client-separator ()
   (.char= #\:))
 
 (defun .client-name ()
   (.prog1
     (.map 'string (.and (.not (.client-separator)) (.item)) :at-least 0)
     (.client-separator)))
 
 (defun .memo ()
   (.prog2
     (.map 'string (.char= #\Space))
     (.first
       (.map 'string
           (.and (.not (.char= #\Newline))
                 (.item))))
     (.optional (.not (.item)))))
 
 (defun .memo-line ()
   (.progn
     (.initial-space)
     (.let* ((client (.client-name))
             (memo (.memo)))
       (.identity (list client memo)))))
 
 (defun .record ()
   (.let* ((time-line (.time-line))
           (memo-line (.memo-line)))
     (.identity (make-time-record time-line memo-line))))
 
 (defun .records ()
   (.first
     (.map 'list (.prog1 (.record)
                         (.or (.map 'string (.char= #\Newline))
                              (.progn
                                (.char= #\Newline)
                                (.not (.item)))
                              (.not (.item)))))))
 
 (defun .weekday ()
   (.or (.string-equal "Sunday")
        (.string-equal "Monday")
        (.string-equal "Tuesday")
        (.string-equal "Wednesday")
        (.string-equal "Thursday")
        (.string-equal "Friday")
        (.string-equal "Saturday")))
 
 (defun .year ()
   (.let* ((fi (.digit))
           (se (.digit))
           (th (.digit))
           (fo (.digit)))
     (.identity (coerce (list fi se th fo) 'string))))
 
 (defun .first-month-char ()
870680c2
   (.or (.char= #\0) (.char= #\1) (.char= #\2) (.char= #\3)))
9ba1ff14
 
 (defun .first-day-char ()
   (.or (.char= #\0) (.char= #\1) (.char= #\2) (.char= #\3)))
 
 (defun .month ()
   (.let* ((fi (.first-month-char))
           (se (.digit)))
870680c2
     (let ((res (when (char= fi #\3)
                  (unless (member se '(#\1 #\0))
                    (.fail)))))
       (if (not res)
         (.identity (coerce (list fi se) 'string))
         res))))
9ba1ff14
 
 (defun .day ()
   (.let* ((fi (.first-month-char))
           (se (.digit)))
     (when (and (char= fi #\3) (not (member se '(#\0 #\1))))
       (.fail))
     (.identity (coerce (list fi se) 'string))) )
 
 (defun .date-separator ()
b953500d
   (.or (.char= #\-)
        (.char= #\/)))
9ba1ff14
 
 (defun .date ()
   (.let* ((dow (.weekday))
b953500d
           (_ (.optional (.char= #\,)))
9ba1ff14
           (_ (.char= #\Space))
           (year (.year))
b953500d
           (sep1 (.date-separator))
9ba1ff14
           (month (.month))
b953500d
           (sep2 (.date-separator))
9ba1ff14
           (day (.day)))
2d700b03
     (let ((year (parse-integer year))
           (month (parse-integer month))
           (day (parse-integer day)))
       (.identity (make-date-obj dow year month day)))))
9ba1ff14
 
 (defun .date-start ()
   (.string= "-- "))
 
 (defun .date-line ()
   (.prog2 (.date-start)
           (.date)
           (.char= #\Newline)))
 
 (defun .date-record ()
   (.let* ((date (.date-line))
74adbb8b
           (records (.records)))
          (.identity (make-day-entry date records))))
9ba1ff14
 
 (defun .date-records ()
   (.first (.map 'list (.date-record))))
 
 (defun .parse-all-records ()
   (.prog1 (.date-records) (.not (.item))))
 
 ;; This will help make sure everything is consumed when
 ;; we don't care about the parser's output.
 (defun cdar-equal (a b) (== (cdar a) (cdar b)))