git.fiddlerwoaroof.com
multi-fun.lisp
a5e8afeb
 (defpackage :multi-fun
   (:shadow :rest)
   (:use :cl )
   (:export
    #:filter-functions
    #:number-recognizer
    #:delimited-field
    #:ignore-char
    #:whitespace
    #:iso-8601-timestamp
    #:parse-qs
    #:rest
    #:as
    #:splat-result
    #:defun
    #:month-recognizer
    #:take-field
    #:parse-format
    #:parse-file
    #:subformat
    #:treeify))
 (in-package :multi-fun)
 
 (defun filter-functions (funs list)
   (loop
      for fun in funs
      for results = (loop
                       for value in list
                       for (result . num) = (multiple-value-list (funcall fun value))
                       when result
                       collect (list (car num) result))
      when (and results (= (length results) (length list)))
      collect fun))
 
 (defun number-recognizer (range)
   (destructuring-bind (min max)
       (etypecase range
         (integer (list 0 range))
         (list range))
     (flet ((in-range-p (n)
              (< min n (1+ max))))
       (lambda (str)
         (loop for digits
            from (min (length str)
                      (ceiling (log max 10)))
            downto (if (<= min 0)
                       1
                       (max 1
                            (floor (log min 10))))
            for (result . rest) = (multiple-value-list (parse-integer str :end digits :junk-allowed t))
            for relevant-result = (and result (in-range-p result))
            until relevant-result
            finally (when relevant-result
                      (return (values-list
                               (cons result rest)))))))))
 
 (defun delimited-field (delimiter)
   (lambda (str)
     (alexandria:if-let ((val (position delimiter str)))
       (values (subseq str 0 val)
               (min (length str)
                    (1+ val)))
       (values str
               (length str)))))
 
 (defun ignore-char (char)
   (lambda (str)
     (when (char= (elt str 0) char)
       (values 'drop 1))))
 
 (defun whitespace ()
   (lambda (str)
     (values 'drop
             (position-if-not 'serapeum:whitespacep
                              str))))
 
 (defun iso-8601-timestamp ()
   (lambda (str)
     (values (local-time:parse-timestring (subseq str 0 32))
             32)))
 
 (defun parse-qs (str)
   (serapeum:mapply 'cons
                    (map 'list
                         (serapeum:op (coerce (fwoar.string-utils:split #\= _ :count 2) 'list))
                         (fwoar.string-utils:split #\& str))))
 
 (defun rest (&optional (as #'identity))
   (lambda (str)
     (values (funcall as str)
             (length str))))
 
 (defun as (fun transform)
   (flet ((do-transform (val) (funcall transform val)))
     (lambda (str)
       (fw.lu:transform-first-value (do-transform (funcall fun str))))))
 
 (defun splat-result (fun)
   (flet ((splatize (val) (list* 'splat (alexandria:ensure-list val))))
     (lambda (str)
       (fw.lu:transform-first-value (splatize (funcall fun str))))))
 
 (defun month-recognizer (str)
   (macrolet ((match-char (elt &body body)
                (let ((cases (mapcar (serapeum:op
                                       (if (listp _1)
                                           `(,(elt (car _1) elt) (match-char ,(1+ elt) ,@_1))
                                           `(,(elt _1 elt) (when (alexandria:starts-with-subseq ,_1 str)
                                                             ,_1))))
                                     body)))
                  `(when (> (length str) ,elt)
                     (case (elt str ,elt)
                       ,@cases)))))
     (alexandria:if-let ((value (match-char 0
                                            ("April" "August")
                                            "December"
                                            "February"
                                            ("January" ("July" "June"))
                                            (("March" "May"))
                                            "November"
                                            "October"
                                            "September")))
       (values value (length value))
       (values nil 0))))
 
 (defun take-field (fun strs)
   (loop for str in strs
      for (val chars-read) = (multiple-value-list (funcall fun str))
      do (format t "~&VAL: ~s CHARS-READ: ~s" val chars-read)
      collect (list val (subseq str chars-read))))
 
 (defun parse-format (funs str)
   (loop
      for line = str then (subseq line chars-read)
      for fun in funs
      for (val chars-read) = (multiple-value-list (funcall fun line))
      for total-chars = chars-read then (+ total-chars chars-read)
      if (and (consp val) (eql (car val) 'splat)) append (cdr val) into result
      else when (not (eql val 'drop)) collect val into result
      finally (return (values result
                              total-chars))))
 
 (defmacro subformat ((v-sym &rest parsers) &body transform)
   `(flet ((transform (,v-sym)
             ,@(if transform
                   transform
                   (list v-sym))))
      (lambda (str)
        (fw.lu:transform-first-value (transform (parse-format (list ,@parsers) str))))))
 
 (defun parse-file (format file &optional (get-record #'read-line))
   (let* ((records ())
          (cur records))
     (flet ((collect-record (record)
              (let ((new-cdr (list record)))
                (if cur
                    (setf (cdr cur) new-cdr)
                    (setf records new-cdr))
                (setf cur new-cdr))))
       (loop (let* ((line (funcall get-record file nil 'eof)))
               (when (eql line 'eof)
                 (return records))
               (with-simple-restart (skip-record "Skip line ~s" line)
                 (collect-record (coerce (parse-format format line)
                                         'vector))))))))
 
 (defun treeify (strings)
   (declare (optimize (speed 3))
            (inline data-lens:over data-lens:transform-tail data-lens:applicable-when data-lens:of-min-length
                    data-lens:on data-lens:over data-lens:slice data-lens:compress-runs
                    data-lens:combine-matching-lists data-lens:juxt data-lens:element data-lens:sorted))
   (let* ((strip-prefixes
           (alexandria:compose
            (data-lens:over
             (data-lens:transform-tail 
              (data-lens:over
               (data-lens:transform-head (data-lens:slice 1)))))
            (data-lens:compress-runs
             :collector 'data-lens:combine-matching-lists)))
          (extract-keys-and-sort
           (alexandria:compose (data-lens:over
                                (data-lens:juxt (alexandria:compose
                                                 (data-lens:element 0)
                                                 (data-lens:element 0))
                                                'identity))
                               (data-lens:sorted 'char<
                                                 :key (alexandria:compose
                                                       (data-lens:element 0)
                                                       (data-lens:element 0)))))
          (recurse
           (data-lens:over
            (data-lens:transform-tail
             (data-lens:applicable-when
              (lambda (x)
                (if (equal (caar x) "")
                    (cons (cons nil (cdar x))
                          (treeify (cdr x)))
                    (treeify x)))
              (data-lens:of-min-length 2)))))
          (step (data-lens:on strip-prefixes extract-keys-and-sort)))
     
     (funcall (alexandria:compose recurse step) strings)))
 
 
 
 
 (defparameter +months+
   '("January"
     "February"
     "March"
     "April"
     "May"
     "June"
     "July"
     "August"
     "September"
     "October"
     "November"
     "December"))