git.fiddlerwoaroof.com
ical-parser.lisp
6cf7e17c
 (defpackage :fwoar.lisp-sandbox.ical-parser
   (:use :cl )
   (:export ))
 (in-package :fwoar.lisp-sandbox.ical-parser)
 
 (defvar *data* (alexandria:read-file-into-string "~/test.ics"))
 
 (defun next-section (s &optional start)
   (if start
       (search "BEGIN" s :start2 start)
       (search "BEGIN" s)))
 
 (defmacro str->stream ((op arg &rest r))
   (alexandria:once-only (arg)
     (alexandria:with-gensyms (s)
       `(with-input-from-string (,s ,arg)
          (,op ,s ,@r)))))
 
 (defun get-line (stream)
   (loop for line = (read-line stream nil)
         while line
         collect line into results
         while (eql #\space (peek-char nil stream nil))
         finally (return (when results
                           (string-right-trim
                            '(#\newline #\return)
                            (serapeum:string-replace-all
                             #1=#.(coerce (list #\return #\space)
                                          'string)
                             (serapeum:string-join results "")
                             ""))))))
 
 (defgeneric handle-begin (client block)
   (:method (_ block)
     (format t "~&;; >>>~a~%" block)))
 (defgeneric handle-end (client block)
   (:method (_ block)
     (format t "~&;; <<<~a~%" block)))
 (defgeneric handle-line (client tag params content)
   (:method (_ tag params content)
     (format t "~&;; - ~s•~s•~s~%" tag params content)))
 (defun process-ics (client file)
   (let ((states '())
         (new-keywords '()))
     (unwind-protect
          (labels ((normalize (inp)
                     (multiple-value-bind (kw existing?)
                         (alexandria:make-keyword (string-upcase inp))
                       (unless existing?
                         (push kw new-keywords))
                       kw))
                   (%handle-block-delimiter (tag type)
                     (push type states)
                     (ecase tag
                       ((:begin) (handle-begin client type))
                       ((:end) (handle-end client type))))
                   (parse-params (inp)
                     (destructuring-bind (head params) (fwoar.string-utils:partition #\; inp)
                       (values head
                               (when params
                                 (map 'list
                                      (data-lens:• (data-lens:transform-head #'normalize)
                                                   (serapeum:op
                                                     (fwoar.string-utils:partition #\= _)))
                                      (fwoar.string-utils:split #\; params))))))
                   (parse-property (it)
                     (destructuring-bind (s e) (fwoar.string-utils:partition #\: it)
                       (multiple-value-bind (head params) (parse-params s)
                         (list (normalize head)
                               params
                               e))))
                   (%handle-line (it)
                     (apply 'handle-line client it)))
            (with-input-from-string (s file)
              (loop for line = (get-line s)
                    for (%tag tagged) = (if line
                                            (fwoar.string-utils:partition #\: line)
                                            '(nil nil))
                    for tag = (fw.lu:may (normalize %tag))
                    while line
                    do (case tag
                         ((:begin)
                          (%handle-block-delimiter tag (normalize tagged)))
                         ((:end)
                          (%handle-block-delimiter tag (normalize tagged)))
                         (t (%handle-line (parse-property line)))))))
       (mapc 'unintern new-keywords))))
 
 (fw.lu:defclass+ emit-sql ()
   ((%lines :accessor lines :initform ())
    (%tzid :accessor tzid :initform nil)
    (%db :reader db :initarg :db)))
 (defmethod handle-begin ((client emit-sql) block)
   (values))
 (defmethod handle-end :after ((client emit-sql) block)
   (setf (lines client) nil))
 (defmethod handle-end :before ((client emit-sql) block)
   (values))
 (defmethod handle-end ((client emit-sql) (block (eql :vevent)))
   (macrolet ((get-setter ()
                `(sxql:set= ,@(mapcan (lambda (it)
                                        (list it `(serapeum:assocadr
                                                   ,(alexandria:make-keyword
                                                     (substitute #\- #\_
                                                                 (string it)))
                                                   (lines client))))
                                      '(:X_APPLE_TRAVEL_ADVISORY_BEHAVIOR
                                        :VEVENT :VALARM :RECURRENCE_ID
                                        :ORGANIZER :LAST_MODIFIED
                                        :EXDATE :CREATED :ATTENDEE
                                        :ATTENDEE :ATTACH :CATEGORIES
                                        :DESCRIPTION :DTEND :DTSTAMP
                                        :DTSTART :GEO :LOCATION :RRULE
                                        :SEQUENCE :STATUS :SUMMARY
                                        :TRANSP :UID :URL
                                        :X_ALT_DESC)))))
     (multiple-value-bind (query params)
         (sxql:yield
          (sxql:insert-into :vevent
            (get-setter)))
       (apply 'sqlite:execute-single
              (db client)
              query
              params))))
 (defmethod handle-line ((client emit-sql) tag params content)
   (push (list tag content)
         (lines client)))
 
 (defparameter +datetime-scanner+
   (cl-ppcre:create-scanner
    '(:SEQUENCE
      :START-ANCHOR
      ;; date yyyy-mm-dd
      (:REGISTER (:GREEDY-REPETITION 4 4 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      #\T
      ;; time hh-mm-ss
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      ;; tz
      (:GREEDY-REPETITION 0 1 (:REGISTER #\Z))
      :END-ANCHOR)))
 (defparameter +date-scanner+
   (cl-ppcre:create-scanner
    '(:SEQUENCE
      :START-ANCHOR
      ;; date yyyy-mm-dd
      (:REGISTER (:GREEDY-REPETITION 4 4 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      :END-ANCHOR)))
 (defparameter +time-scanner+
   (cl-ppcre:create-scanner
    '(:SEQUENCE
      :START-ANCHOR
      ;; time hh-mm-ss
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      (:REGISTER (:GREEDY-REPETITION 2 2 :DIGIT-CLASS))
      ;; tz
      (:GREEDY-REPETITION 0 1 (:REGISTER #\Z))
      :END-ANCHOR)))
 
 (defparameter +sqlite-format+
   '((:YEAR 4) #\- (:MONTH 2) #\- (:DAY 2)
     #\space (:HOUR 2) #\: (:MIN 2) #\: (:SEC 2)
     :GMT-OFFSET-OR-Z))
 
 (defun parse-datetime (time timezone)
   (trivia:ematch (nth-value 1 (cl-ppcre:scan-to-strings +datetime-scanner+ time))
     (#(ye mo da ho mi se tz)
       (local-time:encode-timestamp
        0
        (fw.lu:if-let* ((se (parse-integer se))
                        (_ (= 60 se)))
          59
          se)
        (parse-integer mi) (parse-integer ho)
        (parse-integer da) (parse-integer mo) (parse-integer ye)
        :timezone (if (equal tz "Z")
                      local-time:+utc-zone+
                      timezone)))))
 
 (defun parse-date (time)
   (trivia:ematch (nth-value 1 (cl-ppcre:scan-to-strings +date-scanner+ time))
     (#(ye mo da)
       (local-time:encode-timestamp
        0 0 0 0
        (parse-integer da) (parse-integer mo) (parse-integer ye)))))
 
 ;;what do I do for the date here???
 #+(or)
 (defun parse-time (time)
   (trivia::match (nth-value 1 (cl-ppcre:scan-to-strings +time-scanner+ time))
     (#(ho mi se)
       (local-time:make-timestamp
        0 0 0 0
        (parse-integer da) (parse-integer mo) (parse-integer ye)))))
 
 (defun handle-ical-date (client tag params content)
   (push (list tag
               (local-time:format-timestring
                nil
                (case (alexandria:make-keyword
                       (string-upcase
                        (serapeum:assocadr :value params)))
                  (:date (parse-date content))
                  (t (parse-datetime content
                                     (or (local-time:find-timezone-by-location-name
                                          (serapeum:assocadr :tzid params))
                                         (tzid client)))))
                :format +sqlite-format+))
         (lines client)))
 
 (defmethod handle-line ((client emit-sql) (tag (eql :dtstart)) params content)
   (handle-ical-date client tag params content))
 (defmethod handle-line ((client emit-sql) (tag (eql :dtend)) params content)
   (handle-ical-date client tag params content))
 (defmethod handle-line ((client emit-sql) (tag (eql :created)) params content)
   (handle-ical-date client tag params content))
 (defmethod handle-line ((client emit-sql) (tag (eql :dtstamp)) params content)
   (handle-ical-date client tag params content))
 (defmethod handle-line :after ((client emit-sql) (tag (eql :dtstart)) params content)
   (format t "~&~s ~s~%~4t~s~%~4t~s~%" tag params
           content
           (parse-time content
                       (or (local-time:find-timezone-by-location-name
                            (serapeum:assocadr :tzid params))
                           (tzid client)))))
 
 (defun setup-sql ()
   (sxql:yield
    (sxql:create-table (:vevent :if-not-exists t)
        ((:ATTACH :type 'text)
         (:ATTENDEE :type 'text)
         (:CATEGORIES :type 'text)
         (:CREATED :type 'text)
         (:DESCRIPTION :type 'text)
         (:DTEND :type 'text)
         (:DTSTAMP :type 'text)
         (:DTSTART :type 'text)
         (:EXDATE :type 'text)
         (:GEO :type 'text)
         (:LAST_MODIFIED :type 'text)
         (:LOCATION :type 'text)
         (:ORGANIZER :type 'text)
         (:RECURRENCE_ID :type 'text)
         (:RRULE :type 'text)
         (:SEQUENCE :type 'text)
         (:STATUS :type 'text)
         (:SUMMARY :type 'text)
         (:TRANSP :type 'text)
         (:UID :type 'text)
         (:URL :type 'text)
         (:VALARM :type 'text)
         (:VEVENT :type 'text)
         (:X_ALT_DESC :type 'text)
         (:X_APPLE_TRAVEL_ADVISORY_BEHAVIOR :type 'text))
      (sxql:primary-key '(:sequence :uid :recurrence_id)))))
 
 (defun ics->sqlite (fn data)
   (sqlite:with-open-database (db fn)
     (sqlite:execute-non-query
      db (sxql:yield (sxql:drop-table :vevent :if-exists t)))
     (sqlite:execute-non-query
      db (setup-sql))
     (sqlite:with-transaction db
       (process-ics (emit-sql db) data))))
 
 
 (fw.lu:defclass+ build-tree ()
   ((%history :accessor history :initform ())))
 
 (defmethod handle-begin ((client build-tree) block)
   (push (list block) (history client)))
 (defmethod handle-end ((client build-tree) block)
   (progn (when (cdr (history client))
            (let ((last (pop (history client))))
              (push (nreverse last)
                    (car (history client)))))))
 (defmethod handle-line ((client build-tree) tag params content)
   (push (list tag params content)
         (car (history client))))
 
 (defun ics->tree (data)
   (let ((client (build-tree)))
     (process-ics client data)
     (nreverse (car (history client)))))
 
 (fw.lu:defclass+ one-vevent ()
   ((%started :accessor started :initform nil)
    (%lines :accessor lines :initform nil)))
 
 (defmethod handle-begin ((client one-vevent) block)
   (when (eql block :vevent)
     (push block (started client))))
 
 (defmethod handle-line ((client one-vevent) tag params content)
   (when (started client)
     (push (list tag params content)
           (lines client))))
 
 (defmethod handle-end ((client one-vevent) block)
   (when (eql block :vevent)
     (throw 'vevent
       (nreverse (lines client)))))
 
 (defun extract-one-vevent (data)
   (catch 'vevent
     (process-ics (one-vevent) data)))