;;;; utils.lisp -- some random utils I collect. might be useful, probably won't be. (defpackage :flan-utils (:use :cl) (:nicknames :fu)) (in-package :flan-utils) (defmacro export* (&rest syms) "Export all symbols" (cons 'progn (mapcan #'(lambda (x) `((export ',x))) syms))) (defmacro eval-all (syms) "Eval all statements, return in list." `(mapcar #'eval '(,@syms))) (export 'eval-all) (defmacro export*! (&rest syms) "Eval all statements export return values" `(mapc #'export (eval-all ,syms))) (export 'export*) (export 'export*!) (defmacro defexport (&rest val) "Same as export*! for some reason" `(export*! ,@val)) ; TODO: Make this filter out non-defun/defparameter/defmacro statements from `val` before passing to `export*!` (export 'defexport) (defun file-get-contents (filename) "Read file into string" (with-open-file (stream filename) (let ((contents (make-string (file-length stream)))) (read-sequence contents stream) contents))) (export 'file-get-contents) (defparameter *old-readtables* nil) (defexport ;;; --- actual (exported) code goes here -- (defun copy-stream (from to &key (buffer-size 4096)) "Block copy byte streams" (let ((buffer (make-array buffer-size :element-type '(unsigned-byte 8)))) (loop for bytes-read = (read-sequence buffer from) while (plusp bytes-read) do (write-sequence buffer to :end bytes-read)))) (defun real-copy-file (from to) "Copy a file" (with-open-file (from from :direction :input :element-type '(unsigned-byte 8)) (with-open-file (to to :direction :output :if-exists :supersede :element-type '(unsigned-byte 8)) (copy-stream from to)))) (defun real-move-file (from to) "Actually move a file" (real-copy-file from to) (delete-file from)) (defmacro errors (stmt) `(let ((ret (handler-case (cons ,stmt nil) (t (c) (cons nil c))))) (values (car ret) (cdr ret)))) (defmacro try-catch (try &body catch) (let ((errnm (gensym))) `(let ((,errnm (handler-case (cons ,try nil) (t (e) (let ((e e)) (cons (progn ,@catch) e)))))) (if (cdr ,errnm) (values (car ,errnm) (cdr ,errnm)) (values (car ,errnm) nil))))) (defmacro try (&body body) `(try-catch (progn ,@body) nil)) (defmacro try-catch-finally (try catch &body finally) (let ((ret (gensym)) (err (gensym))) `(multiple-value-bind (,ret ,err) (try-catch ,try ,catch) (values (progn ,@finally) ,ret ,err)))) (defmacro try-finally (try &body finally) `(try-catch-finally ,try nil ,@finally)) (defmacro val-if-or (val test or) "(if (test val) val or)" `(let ((vv ,val)) (if (funcall ,test vv) vv ,or))) (defun where (expr items) (mapcan #'(lambda (x) (when (funcall expr x) (list x))) items)) (defun true (f) (not (null f))) ; Functional (defun nop () "Do nothing" nil) (defun yep () "Do nothing" t) ; Combinators (defun deatomise (list) "If `list' is a list, pass it through; if it is a non-nil atom, wrap it in a single-element list" (if (and list (atom list)) (cons list nil) list)) (defun deatomise! (list) "Ensure `list' is a list" (cons list nil)) (defun combine (fa fb &key (pass #'deatomise!)) "Returns an applicative lambda that runs (`fb' (`fa' args...)...) NOTE: This function *applies* the result of `fa' to `fb', therefore, if the result of `fa' is a list, the elements of said list are applied as the sequential arguments for `fb', if the result is a single element, it is passed as argument 1 only (this includes ``nil''). If you want to pass the result of `fa' to `fb' verbatim through the first argument only, use ``combine1''(). WARNING: By default, if `fa' returns ``nil'', the ``nil'' is passed as argument 1 to `fb'. Therefore, no function `fa' will ever produce 0 arguments for `fb'; if you wish to override this behaviour and allow a nil return to mean 0 arguments, set `pass' to ``deatomise''() (to still ensure the return of `fa' is contained list; you can use ``combine!''() for behaviour instead too), or the ``identity''() function, if you know `fa' returns a list." (lambda (&rest args) (apply fb (funcall pass (apply fa args))))) (defun combine! (fa fb) "Returns an applicative lambda that runs (`fb' [(`fa' args...)...]) NOTE: This is the same as calling `combine' with `pass' as ``deatomise''()." (combine fa fb :pass #'deatomise)) (defun combine1 (fa fb) "Returns a lambda that runs (`fb' (`fa' args...)) NOTE: The difference between this an ``combine''() is that `combine' *applies* the result of `fa' to `fb', whereas `combine1'() simply calls `fb' with the result of `fa'." (lambda (&rest args) (funcall fb (apply fa args)))) (defun inverse (func) "Returns a lambda that resolves ¬(`func' args...)" (lambda (&rest n) (not (apply func n)))) (defun inverse* (&rest functions) "Returns a list of `inverse'()d functions from `functions'" (mapcar #'inverse functions)) ;; Mapping (defun mapline (input fi &key (read-line #'read-line)) "Map lines from stream" (loop for line = (funcall read-line input nil) while line do (funcall fi line))) (defun map-lines (func stream &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 (cond ;; Specific `ignore' values ((eql ignore :blank) (lambda (line) (> (length line) 0))) ((eql ignore :whitespace-only) (lambda (line) (not (cl-ppcre:scan "^\\s*$" line)))) ;; Otherwise, the ignore function (or pass all, if nil) (t (or ignore (lambda (--n) (declare (ignore --n)) t)))))) (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 (func location &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 `(,func ,stream) kvs))))) (defun mapcan-lines (func stream &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 func stream :applicator #'nconc :mapper #'mapcar) kvs))) (defmacro mapcan-file-lines (func location &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 `(,func ,stream) kvs))))) (defun mapcon-lines (func stream &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 func stream :applicator #'nconc :mapper #'maplist) kvs))) (defmacro mapcon-file-lines (func location &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 `(,func ,stream) 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) (typecase x (string x) (character (string x)) (t (write-to-string x)))) str)))) (defmacro strcat-fast (&rest strings) "Concat all strings, they need to be strings. Use `strcat' instead unless you can guarantee you won't violate that." `(concatenate 'string ,@strings)) (defmacro until (stmt) "Repeat stmt until its return is not NIL, then return that value." `(let ((ret nil)) (loop while (null (setf ret ,stmt))) ret)) (defmacro popor (li or) "If li is list, pop it, else return or" (let ((liname (gensym))) `(let ((,liname ,li)) (if (atom ,liname) ,or (pop ,liname))))) (defun rand-in (l &key (random #'random) ) "Random member of, slide right if nil" "Pretty sure this doesn't work" (let ((rng (funcall random (list-length l)))) (let ((nl (nthcdr rng l))) (until (pop nl))))) (defun regex-replace-many (str matches replwith) "Replace list of regexes with list of new string" (let ((ret str)) (loop for match in matches for repl in replwith do (setf ret (cl-ppcre:regex-replace-all match ret repl))))) (defun in-range(num r s) (and (>= num r) (<= num s))) (defun format-string (fmt &rest r) "Like sprintf I guess" (with-output-to-string (stream) (apply #'format `(,stream ,fmt . ,r)))) (defun get-all-symbols () "Gets all symbols" (let ((lst '())) (do-all-symbols (s lst) (push s lst)) lst)) (defun symbol-match (scan &optional (symbols nil)) "Return all symbols whose names match regex `scan'. If symbols are not provided, get them all." (let ((symbols (val-if-or symbols #'true (get-all-symbols)))) (where #'(lambda (x) (cl-ppcre:scan scan (write-to-string x))) symbols))) (defun index (i max) "Looping index" (if (< i 0) (index (+ max i) max) (mod i max))) (defun slice-list (seq start &optional (end -1)) "Like JS slice() for list" (let ((start (index start (length seq))) (end (index end (length seq)))) (rplacd (nthcdr end seq) nil) (nthcdr start seq))) (defun slice (seq start &optional (end -1)) "Like JS slice()?" (let ((start (index start (length seq))) (end (index end (1+ (length seq))))) (subseq seq start end))) (defun flatten-top-level (li) "'( (1 2) 3 4 (5 6 7) (8) ((9 10))) -> ( 1 2 3 4 5 6 7 8 (9 10))" (mapcan #'(lambda (x) (if (atom x) (list x) x)) li)) (defun flatten (li) "'( (1 2) 3 4 (5 6 7) (8) ((9 10))) -> ( 1 2 3 4 5 6 7 8 9 10)" (mapcan #'(lambda (x) (if (atom x) (list x) (flatten x))) li)) (defun strjoin (delim &rest strs) "Join strings with deliminator" (let ((strs (flatten-top-level strs))) (apply #'strcat (slice (mapcan #'(lambda (x) (list x delim)) strs) 0 -2)))) (defun export-struct (struct &optional (symbols nil)) ;;XXX: Exporting make-* does not check for $ "Export all symbols relating to `struct'" (mapc #'export (symbol-match (strcat "^(MAKE-)?" (write-to-string struct) "-?") symbols))) (defun -export*-struct (structs &optional (symbols nil)) ;;XXX (mapc #'export (symbol-match (strcat "^(MAKE-)?(" (strjoin "|" (mapcar #'write-to-string structs)) ")-?") symbols))) (defmacro export*-struct (&rest structs) "Export all symbols relating to multiple structs" `(-export*-struct '(,@structs))) (defun mapn (lam &rest lists) "Map and return last" (let ((ret '())) (mapc #'(lambda (x) (setf ret (funcall lam x))) (flatten-top-level lists)) ret)) (defun cyclic (list) "Simple circular list" (cdr (rplacd (last list) list))) (defun map1 (lam &rest lists) "Map and return first" (let ((ret '()) (change t)) (mapc #'(lambda (x) (if change (progn (setf ret (funcall lam x)) (setf change nil)) (funcall lam x))) (flatten-top-level lists)) ret)) (defun mapnth (lam n &rest args) "Map and return nth or nil (second value t if match found)" (let ((index 0) (ret '()) (match nil)) (mapc #'(lambda (x) (if (= n index) (progn (setf ret (funcall lam x)) (setf match t)) (funcall lam x)) (incf index)) (flatten-top-level args)) (values ret match))) (defun mapv (lam &rest args) "Map and return values()" (apply #'values (mapcar lam (flatten-top-level args)))) (defmacro lexical-boundp (lex) "XXX: Use (declare (special ...)) instead" `(when (ignore-errors ,lex) t)) ;; --- progressive evals (defmacro progex (funcs expr) "run funcs on expr, return specified" (let ((name (gensym))) `(let ((,name ,expr)) (mapcar #'(lambda (x) (funcall x ,name)) ,funcs) ,name))) (defmacro progen (&rest things) "mapn eval things" `(mapn #'eval '(,@things))) (defmacro proge1 (&rest things) "map1 eval things" `(map1 #'eval '(,@things))) (defmacro progenth (n &rest things) "mapnth eval n things" `(mapnth #'eval ,n '(,@things))) (defmacro progev (&rest things) "mapv eval things" `(mapv #'eval '(,@things))) (defmacro progel (&rest things) "mapcar eval things" `(mapcar #'eval '(,@things))) (defmacro progenc (&rest things) "mapcan eval things" `(mapcan #'eval '(,@things))) (defmacro yield-return (&rest things) "Create yield block, returns last of push()ed outputs of yield() (so, last is first etc)" `(let ((--yield-vars '()) (--yield-var nil)) (declare (special --yield-vars --yield-var)) ,@things (apply #'values --yield-var))) (defmacro yield (value) "Yield return value `value' if in yield-return(...) block." `(when (boundp '--yield-vars) (setf --yield-var (multiple-value-list ,value)) (push (car --yield-var) --yield-vars))) (defparameter *yield-global-vars* '()) (defmacro yield-return-global (name &rest things) "Like yield-return but stores values by key in alist and is accessable outside of current lexenv" `(car (yield-return (push (cons ,name '()) *yield-global-vars*) ,@things (yield (cdr (assoc ,name *yield-global-vars*))) (setf *yield-global-vars* (remove ,name *yield-global-vars* :key #'car :count 1))))) (defmacro yield-global (name value) `(when (assoc ,name *yield-global-vars*) (push ,value (cdr (assoc ,name *yield-global-vars*))))) ;; --- (defmacro push-unique (thing things &key (key nil) (test #'eql)) "Push if not member" `(let ((lex-thing ,thing)) (if (member lex-thing ,things :key ,key :test ,test) (values ,things nil) (values (push lex-thing ,things) t)))) (defmacro import* (&rest args) "Import all symbols" (cons 'progn (mapcan #'(lambda (x) `((import ',x))) args))) (defmacro shadowing-import* (&rest args) "Import all symbols (shadowing)" (cons 'progn (mapcan #'(lambda (x) `((shadowing-import ',x))) args))) (defmacro import*! (&rest args) "Map eval then import all returns" `(mapc #'import (progel ,args))) (defmacro shadowing-import*! (&rest args) "Map eval then import all returns (shadowing)" `(mapc #'shadowing-import (progel ,args))) (defmacro import*-from (package &rest args) "Import all symbols from package. NOTE: You shouldn't prefix the symbols with the package name" (let ((ret (mapcar #'(lambda (x) (read-from-string (strcat (subseq (write-to-string package) 1) ":" (write-to-string x)))) args))) `(shadowing-import* @,ret))) ;(defun restrain-index (i max) ; ; (if nil ; (if (>= i max) (1- max) ; (if (< i 0) 0 ; i)) ; i)) (defun many-equals (items &key (test #'eql)) "For some reason equality comparers don't like more than 2 args" (let ((comp (car items))) (labels ((compare (x to) (when (null x) t) (when (funcall test (car x) to) (compare (cdr x) to)))) (compare (cdr items) comp)))) (defun many-eql (&rest items) (many-equals items :test #'eql)) ; --- async stuff (defstruct async-info thread handlers lock) (defmacro push-handler (name lam) "Push new handler to current async-info with name" "NOTE: Should only be used inside an async() body." `(bt:with-lock-held ((async-info-lock current-async-info)) (push (cons ,name ,lam) (async-info-handlers current-async-info)))) (defmacro async (&rest form) "Run form(s) async" `(let ((current-async-info (make-async-info))) (setf (async-info-handlers current-async-info) nil) (setf (async-info-lock current-async-info) (bt:make-lock)) (setf (async-info-thread current-async-info) (bt:make-thread #'(lambda () ,@form))) current-async-info)) (defun async-info-handler (async name &key (test 'eql)) "Get handler from async-info of name" (bt:with-lock-held ((async-info-lock async)) (let ((as (assoc name (async-info-handlers async) :test test ))) (and as (cdr as))))) (defun wait (handle) "Wait on async()" (if (async-info-p handle) (wait (async-info-thread handle)) (bt:join-thread handle))) (defun async-kill (handle) "Kill async()" (if (async-info-p handle) (async-kill (async-info-thread handle)) (bt:destroy-thread handle))) (defun async-alive (handle) "Is async() alive" (if (async-info-p handle) (async-alive (async-info-thread handle)) (bt:thread-alive-p handle))) (defun val (v) v) (defun groupn (n list) "Group list into sublists every `n' items." (let ((last nil) (ret nil)) (loop for x in list for y from 0 below (length list) do (if (= 0 (mod y n)) (push x last) (progn (push x last) (setf ret (cons (reverse last) ret)) (setf last nil)))) (reverse ret))) ; --- reader macros (defun sexpr-reader (stream char &key (func 'val) (unset t) (keep-char t)) "Read next token only if S expression, else return as is" (if (char= (peek-char t stream t nil t) #\() (values (funcall func (read stream t nil t)) t) (let ((*readtable* (copy-readtable))) (and unset (set-macro-character char nil)) (if keep-char (values (read-from-string (strcat (string char) (write-to-string (read stream t nil t)))) nil) (values (read stream t nil t) nil))))) (defun not-reader (stream char) (declare (ignore char)) (list (quote not) (read stream t nil t))) (defun read-delimiter (stream char) (declare (ignore stream char))) (defun read-next-until (stream char) (if (char= (peek-char t stream t nil t) char) (progn (read-char stream t nil t) nil) (read stream t nil t))) (defun export-reader (stream char) (declare (ignore char)) (loop for next = (read-next-until stream #\]) while next collect next into objects finally (return `(defexport ,@objects)))) (defun top-level-reader (stream char) (multiple-value-bind (thing okay) (sexpr-reader stream char) (if okay (append (list 'eval-when '(:compile-toplevel :load-toplevel :execute)) (list thing)) thing))) (defun async-reader (stream char) (multiple-value-bind (thing okay) (sexpr-reader stream char) (if okay (cons 'async (list thing)) thing))) (defun lambda-reader (stream char) (declare (ignore char)) `(lambda () ,(read stream t nil t))) (defmacro enable-all-readers () "Turn on reader macros" '(eval-when (:compile-toplevel :load-toplevel :execute) (push *readtable* *old-readtables*) (setq *readtable* (copy-readtable)) (set-macro-character #\[ 'export-reader) ;"Exports all in brackets []" (set-macro-character #\] 'read-delimiter) (set-macro-character #\$ 'async-reader) ;"Run statement in seperate thread" (set-macro-character #\¬ 'not-reader) ;"Negates next statement" (set-macro-character #\£ 'lambda-reader) ;"Wrap statement in lambda" (set-macro-character #\€ 'top-level-reader))) ;"Run at compile,load and execute" (defmacro disable-all-readers() "Turn off reader macros" '(eval-when (:compile-toplevel :load-toplevel :execute) (setq *readtable* (pop *old-readtables*)))) ; ((defun map-parallel (func seq &key (map #'mapcar) (split 0.25) (debug nil)) ;;; TODO: make this work lol ; (flet ((dprint (x) (when debug (format t "~S~%" x)) x )) ; "Map over list in parallel" ; (let* ((step (floor (* split (list-length seq)))) ; (threads (val-if-or (ceiling (/ 1 split)) ; #'(lambda (val) ; (< val step)) ; 0))) ; (if (< threads 2) ; (funcall map func seq) ;; no splits ; (let ((threads '()) ; (no-threads threads) ; (outputs (make-list threads)) ; (left '())) ; (loop for i from 0 below no-threads ; for start from 0 below (list-length seq) by step ; do (progn ; (push (bt:make-thread ; #'(lambda () ; ;(dprint (list i start (restrain-index (+ step start) (list-length seq) ))) ; (rplaca (nthcdr i outputs) ; (funcall map func (slice seq start (+ (1- step) start)))))) ; threads))) ; (setf left ; (when (> (mod (list-length seq) step) 0) ; (funcall map func (slice seq (* no-threads step) (+ (* no-threads step) (mod (list-length seq) step)))))) ; ; (loop while (apply #'= (cons 0 (mapcar #'(lambda (x) (if (bt:thread-alive-p x) 1 0)) threads))) do ; (dprint ; (mapcar #'bt:thread-alive-p ; threads))) ; (apply #'nconc (append outputs left))))))) ; ;(defun map-parallel-test (&key (length 10001)) ; (map-parallel ; #'(lambda (x) ; x) ; (make-list length :initial-element 1) ; :debug t)) ; --- others (defmacro switch (value &body conds) "Cond `eql' for value to each first element of `conds', with the result as the 2nd element. If `conds' is an atom, it is treated as the default condition" (let* ((value-name (gensym)) (exprs (mapcar #'(lambda (pair) (if (atom pair) `(t ,pair) `((eql ,value-name ,(car pair)) ,(cadr pair)))) conds))) `(let ((,value-name ,value)) ,(cons 'cond exprs)))) (defun split-string (string &optional sep) "Split a string by this seperator (or whitespace, if not provided)" (let* ((sep (or (switch sep (#\Newline "[\\n\\f\\r]") (#\Space " ") (#\Backspace "\\b") (#\Tab "\\t") (#\Linefeed "\\n") (#\Page "\\f") ; ??? (#\Return "\\r") ;(#\Rubout wtf even is this??? (nil "\\s") nil) (cl-ppcre:quote-meta-chars sep))) (lst (cl-ppcre:split sep string))) (values (where #'(lambda (str) (> (length str) 0)) lst) sep))) (defmacro popto (li val &key (test #'eql)) "pop() list
  • until (car li) is equal to , return elements pop()ed in new list" `(loop while (not (funcall ,test (car ,li) ,val)) collect (pop ,li))) (defmacro popn (li n) "pop() list
  • times, return elements pop()ed in a new list." (if (numberp n) (list 'let '((tmp 'nil)) (apply #'list 'progn (loop for x from 1 to n collect `(setf tmp (cons (pop ,li) tmp)))) '(reverse tmp)) `(loop for x from 1 to ,n collect (pop ,li)))) (defun make-paged-vector (blocksize &key (element-type 'integer)) "Vector that increases size in blocks" (list (make-array blocksize :element-type element-type :fill-pointer 0 :adjustable t) blocksize 0 1)) (defun paged-vector<-size (vec) (caddr vec)) (defun paged-vector<-blocksize (vec) (cadr vec)) (defun paged-vector<-blocks (vec) (cadddr vec)) (defmacro paged-vector->push (vec elem) "add to end, extending if needed" `(if (>= (1+ (mod (paged-vector<-size ,vec) (paged-vector<-blocksize ,vec))) (paged-vector<-blocksize ,vec)) (progn (adjust-array (car ,vec) (* (1+ (paged-vector<-blocks ,vec)) (paged-vector<-blocksize ,vec))) (incf (cadddr ,vec)) (incf (caddr ,vec)) (vector-push ,elem (car ,vec)) ,vec) (progn (incf (caddr ,vec)) (vector-push ,elem (car ,vec)) ,vec))) (defun make-paged-vector-s (elements blocksize) "make-paged-vector with default elements" (let ((out (make-paged-vector blocksize))) (mapc #'(lambda (x) (paged-vector->push out x)) elements) out)) (defmacro paged-vector<- (vec) `(car ,vec)) (defun status-bar-string (percs &optional (sz (length percs)) (max nil) (default #\-)) "Create a bar string of multiple characters at a ratio. The most simple variant of this uses 2 chars at a ratio to create a text-based progress bar. (see `progress-bar-string') `percs': a list of (fract . char) `sz': the length of the bar (default: length of `percs') `max': the max for `fract' (default: sum of all `fract's in `percs') `default': the char to print if we run above `max' while still under `sz' (default #\-) (note: `default' will never appear in the output string unless `max' is user-provided; otherwise `percs' will be stretched to always completely fill `sz')" (let ((max (or max (apply #'+ (mapcar #'car percs))))) (flet ((write-single (stream fract char cur) (let ((sml (* (/ fract max) sz))) (length (loop for i from cur to (- sz 1) for j from 0 to (- sz 1) while (< j sml) collect i ; XXX: This is very inefficient, come on... do (write-char char stream)))))) (with-output-to-string (stream) (let ((i 0)) (loop while (< i sz) do (let ((this (pop percs))) (incf i (if this (destructuring-bind (fract . char) this (funcall #'write-single stream fract char i)) (prog1 1 (write-char default stream))))))))))) ;(print (status-bar-string '((2 . #\: ) (8 . #\|) (3 . #\_) (1 . #\!)) 60)) (defun progress-bar-string (perc len &optional (char #\#) (default #\-)) "Create a progress bar string of `len' size containing `perc'% `char', with the rest `default'" (status-bar-string `((,perc . ,char)) len 100 default)) ;(print (progress-bar-string 10 60)) ) ;; -- end export