diff --git a/flan-utils.lisp b/flan-utils.lisp index ecb4992..7a3b841 100644 --- a/flan-utils.lisp +++ b/flan-utils.lisp @@ -120,6 +120,64 @@ (loop for line = (funcall read-line input nil) while line do (funcall fi line))) + + (defun map-lines (stream func &key + (ignore nil) + (applicator #'list) + (mapper #'mapcar) + (transform #'identity) + (read-line (lambda (stream) (read-line stream nil))) + (continue #'identity)) + "Maps over the lines in stream `stream', applying `applicator' to the result of `mapper' being called with `func' when called with the lines in-order. +To transform the line before processing, you can set the `transform' argument: This will be passed the raw input line, and the line used for the rest of the mapping is the result of that function. (If `transform' returns ``nil'', the line is treated as blank, otherwise, it must return a string.) +To ommit a certain kind of line from being sent to the mapping function, you can set `ignore' to: + - `:blank' Ignore blank lines + - `:whitespace-only' Ignore lines that contain just whitespaces + - Any functor that will take the line as an argument. If the call returns a non-truthy value, the line is ignored. +To use a custom line reader function, set `read-line' to a function that takes a stream and returns a string (or ``nil'', on EOF). +To stop on a specific line, `continue' can be set to a function that receives the line string; and if `nil' is returned from that function, the iteration stops. +The default behaviour (with `mapper' being `mapcar' and `applicator' being `list') works just like `mapcar'(). To emulate the behaviour of functions like `mapcan'(), set `applicator' to `nconc'(); then set `mapper' to `maplist'() for `mapcon'() behaviour." + ;(with-open-file (stream location :direction :input) + (let ((filter-single-line (switch ignore + ;; Specific `ignore' values + (:blank (lambda (line) (> (length line) 0))) + (:whitespace-only (lambda (line) (not (cl-ppcre:scan "^\s*$" line)))) + ;; Otherwise, the ignore function + ignore))) + (apply applicator ; apply `applicator' to the result of each iteration in `mapper'. + (funcall mapper func ; call the mapping function with `func' and the list of transformed and filtered lines + (mapcan (lambda (n) (when n (list n))) ; outputs a list of the lines + (loop for line = (funcall read-line stream) + while (and line (funcall continue line)) + collect (let ((line (funcall transform line))) + (when (funcall filter-single-line line) line)))))))) + (defmacro map-file-lines (location func &rest kvs &key &allow-other-keys) + "See `map-lines'(): Maps `func' over a file `location' instead of a stream." + (let ((stream (gensym))) + `(with-open-file (,stream ,location :direction :input) + ,(cons 'map-lines (append `(,stream ,func) kvs))))) + ;TODO: test map-lines -- (map-file-lines "file" #'identity :applicator #'nconc :ignore :blank) + + (defun mapcan-lines (stream func &rest kvs &key &allow-other-keys) + "See `map-lines'(): Uses `nconc'() as the applicator and `mapcar'() as the mapper, which produces an output you'd expect from `mapcan'() The other key arguments can be specified according to the signature of `map-lines'()." + (apply #'map-lines (append (list stream func :applicator #'nconc :mapper #'mapcar) kvs))) + + (defmacro mapcan-file-lines (location func &rest kvs &key &allow-other-keys) + "See `mapcan-lines'(): Maps `func' over a file `location' instead of a stream." + (let ((stream (gensym))) + `(with-open-file (,stream ,location :direction :input) + ,(cons 'mapcan-lines (append `(,stream ,func) kvs))))) + + (defun mapcon-lines (stream func &rest kvs &key &allow-other-keys) + "See `map-lines'(): Uses `nconc'() as the applicator and `maplist'() as the mapper, which produces an output you'd expect from `mapcon'(). The other key arguments can be specified according to the signature of `map-lines'()." + (apply #'map-lines (append (list stream func :applicator #'nconc :mapper #'maplist) kvs))) + + (defmacro mapcon-file-lines (location func &rest kvs &key &allow-other-keys) + "See `mapcon-lines'(): Maps `func' over a file `location' instead of a stream." + (let ((stream (gensym))) + `(with-open-file (,stream ,location :direction :input) + ,(cons 'mapcon-lines (append `(,stream ,func) kvs))))) + (defun strcat (&rest str) "Concat all strings, if item is not string it is written to one." (apply #'concatenate (cons 'string (mapcar #'(lambda (x)