git.fiddlerwoaroof.com
Raw Blame History
(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)))