summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2017-09-21 01:06:12 -0300
committerSean Whitton <spwhitton@spwhitton.name>2017-09-21 01:06:12 -0300
commitab1f47b0bdefe640dd16a185da5ffef2930a7ebe (patch)
tree33f47d6940bf7f62016226bab9429b6c72ed4474
Import emacs-kv_0.0.19+git20140108.7211484.orig.tar.xz
[dgit import orig emacs-kv_0.0.19+git20140108.7211484.orig.tar.xz]
-rw-r--r--README.creole155
-rw-r--r--kv-tests.el256
-rw-r--r--kv.el462
3 files changed, 873 insertions, 0 deletions
diff --git a/README.creole b/README.creole
new file mode 100644
index 0000000..1dfd1aa
--- /dev/null
+++ b/README.creole
@@ -0,0 +1,155 @@
+A collection of tools for dealing with key/value data structures such
+as plists, alists and hash-tables.
+
+=== kvalist->filter-keys alist &rest keys ===
+
+Return the //alist// filtered to the //keys// list.
+
+Only pairs where the car is a [[member]] of //keys// will be returned.
+
+
+=== kvalist->hash alist &rest hash-table-args ===
+
+Convert //alist// to a HASH.
+
+//hash-table-args// are passed to the hash-table creation.
+
+
+=== kvalist->keys alist ===
+
+Get just the keys from the alist.
+
+
+=== kvalist->plist alist ===
+
+Convert an alist to a plist.
+
+
+=== kvalist->values alist ===
+
+Get just the values from the alist.
+
+
+=== kvalist-keys->* alist fn ===
+
+Convert the keys of //alist// through //fn//.
+
+
+=== kvalist-keys->symbols alist ===
+
+Convert the keys of //alist// into symbols.
+
+
+=== kvalist-sort alist pred ===
+
+Sort //alist// (by key) with //pred//.
+
+
+=== kvalist-sort-by-value alist pred ===
+
+Sort //alist// by value with //pred//.
+
+
+=== kvalist2->alist alist2 car-key cdr-key &optional proper ===
+
+Reduce the //alist2// (a list of alists) to a single alist.
+
+//car-key// is the key of each alist to use as the resulting key and
+//cdr-key// is the key of each alist to user as the resulting cdr.
+
+If //proper// is [[t]] then the alist is a list of proper lists, not
+cons cells.
+
+
+=== kvalist2->filter-keys alist2 &rest keys ===
+
+Return the //alist2// (a list of alists) filtered to the //keys//.
+
+
+=== kvalist2->plist alist2 ===
+
+Convert a list of alists too a list of plists.
+
+
+=== kvcmp a b ===
+
+Do a comparison of the two values using printable syntax.
+
+Use this as the function to pass to [[sort]].
+
+
+=== kvdotassoc expr table ===
+
+Dotted expression handling with [[assoc]].
+
+
+=== kvdotassoc-fn expr table func ===
+
+Use the dotted //expr// to access deeply nested data in //table//.
+
+//expr// is a dot separated expression, either a symbol or a string.
+For example:
+
+{{{
+ "a.b.c"
+}}}
+
+or:
+
+{{{
+ 'a.b.c
+}}}
+
+If the //expr// is a symbol then the keys of the alist are also
+expected to be symbols.
+
+//table// is expected to be an alist currently.
+
+//func// is some sort of [[assoc]] like function.
+
+
+=== kvdotassq expr table ===
+
+Dotted expression handling with [[assq]].
+
+
+=== kvhash->alist hash ===
+
+Convert //hash// to an ALIST.
+
+
+=== kvmap-bind args sexp seq ===
+
+A hybrid of [[destructuring-bind]] and [[mapcar]]
+//args// shall be of the form used with [[destructuring-bind]]
+
+Unlike most other mapping forms this is a macro intended to be
+used for structural transformations, so the expected usage will
+be that //args// describes the structure of the items in //seq//, and
+//sexp// will describe the structure desired.
+
+
+=== kvplist->alist plist ===
+
+Convert //plist// to an alist.
+
+The keys are expected to be :prefixed and the colons are removed.
+The keys in the resulting alist are symbols.
+
+
+=== kvplist->filter-keys plist &rest keys ===
+
+Filter the plist to just those matching //keys//.
+
+//keys// must actually be :-less symbols.
+
+[[kvalist->filter-keys]] is actually used to do this work.
+
+=== kvplist->merge &rest plists ===
+
+Merge the 2nd and subsequent plists into the first, clobbering values set
+by lists to the left.
+
+=== kvplist2->filter-keys plist2 &rest keys ===
+
+Return the //plist2// (a list of plists) filtered to the //keys//.
diff --git a/kv-tests.el b/kv-tests.el
new file mode 100644
index 0000000..1713e5e
--- /dev/null
+++ b/kv-tests.el
@@ -0,0 +1,256 @@
+(require 'kv)
+(require 'ert)
+
+(ert-deftest kvhash->alist ()
+ "Test making alists from hashes."
+ (should
+ (equal
+ (sort
+ (kvhash->alist
+ (kvalist->hash '((name1 . value1)
+ (name2 . value2))))
+ (lambda (a b)
+ (string-lessp (symbol-name (car a))
+ (symbol-name (car b)))))
+ '((name1 . value1)
+ (name2 . value2))))
+ (should
+ (equal
+ (sort '((a . 1)
+ (c . 3)) 'kvcmp)
+ (sort (kvhash->alist
+ (kvalist->hash '((a . 1)(b . 2)(c . 3)))
+ (lambda (k v) (and (memq k '(a c)) v))) 'kvcmp))))
+
+(ert-deftest kvalist-sort ()
+ (should
+ (equal
+ (kvalist-sort
+ (list '("z" . 20)
+ '("a" . 20)
+ '("b" . 17))
+ 'string-lessp)
+ '(("a" . 20)
+ ("b" . 17)
+ ("z" . 20)))))
+
+(ert-deftest kvalist-sort-by-value ()
+ (should
+ (equal
+ (kvalist-sort-by-value
+ (list '("z" . 20)
+ '("a" . 20)
+ '("b" . 17))
+ '<)
+ '(("b" . 17)
+ ("z" . 20)
+ ("a" . 20)))))
+
+(ert-deftest kvcmp ()
+ "Test the general cmp function."
+ (should
+ (equal
+ '((a . 10)(b . 20)(c . 5))
+ (sort '((a . 10)(b . 20)(c . 5)) 'kvcmp)))
+ (should
+ (equal
+ '((a . 10)(b . 20)(c . 5))
+ (sort '((b . 20)(c . 5)(a . 10)) 'kvcmp))))
+
+(ert-deftest kvalist-keys->symbols ()
+ "Test the key transformation."
+ (should
+ (equal
+ '((a . 10)(\10 . 20)(\(a\ b\ c\) . 30))
+ (kvalist-keys->symbols
+ '(("a" . 10)(10 . 20)((a b c) . 30)))))
+ (should
+ (equal
+ '((a . 10)(\10 . 20)(\(a\ b\ c\) . 30))
+ (kvalist-keys->symbols
+ '(("A" . 10)(10 . 20)((a b c) . 30))
+ :first-fn 'downcase))))
+
+(ert-deftest kvfa ()
+ "Destructuring kva through functions."
+ (should
+ (equal '("b")
+ (kvfa "a" '((:a :b)("a" "b"))
+ (lambda (key &rest result) result))))
+ (should
+ (equal "b"
+ (kvfa "a" '((:a :b)("a" "b"))
+ (lambda (k v &rest any) v))))
+ (should
+ (equal "b"
+ (kvfa "a" '((:a . :b)("a" . "b"))
+ (cl-function
+ (lambda (k v &rest any) v)))))
+ (should
+ (equal 1
+ (kvfa "a" '((:a :b :c 1)("a" "b" :a 1))
+ (cl-function
+ (lambda (k v &key a) a))))))
+
+(ert-deftest kva ()
+ "Test the simple assoc."
+ (should (equal :b (kva :a '((:a . :b)("a" . "b")))))
+ (should (equal "b" (kva "a" '((:a . :b)("a" . "b")))))
+ (should-not (kva "b" '((:a . :b)("a" . "b")))))
+
+(ert-deftest kvaq ()
+ "Test the simple assq."
+ (should (equal :b (kvaq :a '((:a . :b)("a" . "b")))))
+ (should (equal 2 (kvaq 1 '((1 . 2)("a" . "b")))))
+ (should-not (equal "b" (kvaq "a" '((:a . :b)("a" . "b")))))
+ (should-not (kvaq "b" '((:a . :b)("a" . "b")))))
+
+(ert-deftest kvaq ()
+ "Test the simple assq."
+ (should (equal :b (kvaq :a '((:a . :b)("a" . "b")))))
+ (should (equal 2 (kvaq 1 '((1 . 2)("a" . "b")))))
+ (should-not (equal "b" (kvaq "a" '((:a . :b)("a" . "b")))))
+ (should-not (kvaq "b" '((:a . :b)("a" . "b")))))
+
+(ert-deftest kvaqc ()
+ "Test the simple assq."
+ (should (equal :b (kvaqc :a '((:a . :b)("a" . "b")))))
+ (should (equal 2 (kvaqc 1 '((1 . 2)("a" . "b")))))
+ (should (equal "b" (kvaqc "a" '((:a . :b)("a" . "b")))))
+ (should-not (kvaqc "b" '((:a . :b)("a" . "b")))))
+
+(ert-deftest kvassoc= ()
+ (should
+ (equal
+ '("testkey" . "testvalue")
+ (kvassoc= "testkey" "testvalue" '(("testkey" . "testvalue"))))))
+
+(ert-deftest kvassoq= ()
+ (should
+ (equal
+ '(testkey . "testvalue")
+ (kvassoq= 'testkey "testvalue" '((testkey . "testvalue")))))
+ (should
+ (equal
+ '("testkey" . "testvalue")
+ (kvassoq= "testkey" "testvalue" '(("testkey" . "testvalue")))))
+ ;; Not sure about this - should we really find strings with symbols?
+ (should
+ (equal
+ '("testkey" . "testvalue")
+ (kvassoq= 'testkey "testvalue" '(("testkey" . "testvalue")))))
+ ;; The nil case, the key isn't present
+ (should
+ (equal
+ nil
+ (kvassoq= 'blah "testvalue" '(("testkey" . "testvalue"))))))
+
+(ert-deftest kvalist2-filter ()
+ (should
+ (equal
+ '(((a . 1)(b . 2)))
+ (kvalist2-filter
+ '(((a . 1)(b . 2))((c . 1)(d . 2)))
+ (lambda (alist)
+ (or
+ (memq 'a (kvalist->keys alist))
+ (memq 'b (kvalist->keys alist))))))))
+
+(ert-deftest kvquery->func ()
+ "Test the query language."
+ (should
+ (equal
+ '((("a" . 1)("b" . 2))(("c" . 1)("d" . 2)))
+ (kvalist2-filter
+ '((("a" . 1)("b" . 2))(("c" . 1)("d" . 2)))
+ (kvquery->func '(|(= "a" 1)(= "d" 2))))))
+ (should
+ (equal
+ '((("a" . 1)("b" . 2)))
+ (kvalist2-filter
+ '((("a" . 1)("b" . 2))(("c" . 1)("d" . 2)))
+ (kvquery->func '(= "a" 1)))))
+ (should
+ (equal
+ '()
+ (kvalist2-filter
+ '((("a" . 1)("b" . 2))(("c" . 1)("d" . 2)))
+ (kvquery->func '(&(= "a" 1)(= "c" 1))))))
+ (should
+ (equal
+ '((("a" . 1)("b" . 2)))
+ (kvalist2-filter
+ '((("a" . 1)("b" . 2))(("c" . 1)("d" . 2)))
+ (kvquery->func '(&(= "a" 1)(= "b" 2)))))))
+
+(ert-deftest kvdotassoc ()
+ (should
+ (equal
+ (dotassoc "a.b.c" '(("a" . (("b" . (("c" . 10)))))))
+ 10)))
+
+(ert-deftest kvdotassq ()
+ (should
+ (equal
+ (dotassq 'a.b.c '((a . ((b . ((c . 10)))))))
+ 10)))
+
+(ert-deftest keyword->symbol ()
+ "Convert keyword into a symbol without the leading `:'"
+ (should
+ (eq
+ 'key
+ (keyword->symbol :key)))
+ (should
+ (eq
+ 'key
+ (keyword->symbol 'key)))
+ (let ((sym (gensym)))
+ (should
+ (eq
+ sym
+ (keyword->symbol sym)))))
+
+
+(ert-deftest kvthing->keyword ()
+ (should (equal :one (kvthing->keyword "one")))
+ (should (equal :one (kvthing->keyword ":one"))))
+
+(ert-deftest kvalist->plist ()
+ "Make alists into plists."
+ (should
+ (equal
+ '(:a1 value1 :a2 value2)
+ (kvalist->plist '((a1 . value1) (a2 . value2))))))
+
+(ert-deftest kvplist->alist ()
+ "Make plists into alists."
+ (should
+ (equal
+ '((a1 . value1) (a2 . value2))
+ (kvplist->alist '(:a1 value1 :a2 value2)))))
+
+(ert-deftest kvplist->filter-keys ()
+ (should
+ (equal
+ (list :key1 "value1" :key4 10)
+ (kvplist->filter-keys
+ (list :key1 "value1" :key2 t :key3 '(big list of symbols) :key4 10)
+ 'key1 'key4))))
+
+(ert-deftest kvplist-merge ()
+ (should
+ (equal
+ '(:key1 "value1" :key2 "new value" :key3 "entirely new")
+ (kvplist-merge '(:key1 "value1" :key2 "old value")
+ '(:key2 "new value" :key3 "entirely new")))))
+
+(ert-deftest kvplist-merge-multiple ()
+ (should
+ (equal
+ '(:key1 "value1" :key2 "new value" :key3 "overwritten new one" :key4 "second entirely new")
+ (kvplist-merge '(:key1 "value1" :key2 "old value")
+ '(:key2 "new value" :key3 "entirely new")
+ '(:key3 "overwritten new one" :key4 "second entirely new")))))
+
+;;; kv-tests.el ends here
diff --git a/kv.el b/kv.el
new file mode 100644
index 0000000..5427c4e
--- /dev/null
+++ b/kv.el
@@ -0,0 +1,462 @@
+;;; kv.el --- key/value data structure functions
+
+;; Copyright (C) 2012 Nic Ferrier
+
+;; Author: Nic Ferrier <nferrier@ferrier.me.uk>
+;; Keywords: lisp
+;; Version: 0.0.19
+;; Maintainer: Nic Ferrier <nferrier@ferrier.me.uk>
+;; Created: 7th September 2012
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Some routines for working with key/value data structures like
+;; hash-tables and alists and plists.
+
+;; This also takes over the dotassoc stuff and provides it separately.
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+
+(defun kvalist->hash (alist &rest hash-table-args)
+ "Convert ALIST to a HASH.
+
+HASH-TABLE-ARGS are passed to the hash-table creation."
+ (let ((table (apply 'make-hash-table hash-table-args)))
+ (mapc
+ (lambda (pair)
+ (puthash (car pair) (cdr pair) table))
+ alist)
+ table))
+
+(defun kvhash->alist (hash &optional func)
+ "Convert HASH to an ALIST.
+
+Optionally filter through FUNC, only non-nil values returned from
+FUNC are stored as the resulting value against the converted
+key."
+ (when hash
+ (let (store)
+ (maphash
+ (lambda (key value)
+ (when key
+ (if (and (functionp func))
+ (let ((res (funcall func key value)))
+ (when res
+ (setq store (acons key res store))))
+ ;; else no filtering, just return
+ (setq store (acons key value store)))))
+ hash)
+ store)))
+
+(defun kvfa (key alist receive)
+ "Call RECEIVE with whatever comes out of ALIST for KEY.
+
+RECEIVE can do whatever destructuring you want, the first
+argument is always the car of the alist pair."
+ (apply receive (let ((a (assoc key alist)))
+ (append (list (car a))
+ (if (listp (cdr a))(cdr a)(list (cdr a)))))))
+
+(defun kva (key alist)
+ "Retrieve the value assigned to KEY in ALIST.
+
+This uses `assoc' as the lookup mechanism."
+ (cdr (assoc key alist)))
+
+(defun kvaq (key alist)
+ "Retrieve the value assigned to KEY in ALIST.
+
+This uses `assq' as the lookup mechanism."
+ (cdr (assq key alist)))
+
+(defun kvaqc (key alist)
+ "Retrieve the value assigned to KEY in ALIST.
+
+This uses first the `assq' and then `assoc' as the lookup
+mechanism."
+ (cdr (or (assq key alist)
+ (assoc key alist))))
+
+(defun kvassoc= (key value alist)
+ "Is the value assocd to KEY in ALIST equal to VALUE?
+
+Returns the value looked up by KEY that passes, so normally:
+
+ KEY . VALUE
+"
+ (let ((v (assoc key alist)))
+ (and v (equal (cdr v) value) v)))
+
+(defun kvassoqc (key alist)
+ "String or symbol assoc."
+ (let ((v (or
+ (assq (if (symbolp key) key (intern key)) alist)
+ (or (assoc key alist)
+ ;; not sure about this behaviour... see test
+ (assoc (symbol-name key) alist))))) v))
+
+(defun kvassoq= (key value alist)
+ "Test the VALUE with the value bound to KEY in ALIST.
+
+The lookup mechanism is to ensure the key is a symbol and then
+use assq. Hence the name of the function being a mix of assoc
+and assq.
+
+Returns the value looked up by KEY that passes, so normally:
+
+ KEY . VALUE
+"
+ (let ((v (kvassoqc key alist)))
+ (and v (equal (cdr v) value) v)))
+
+(defun kvmatch (key regex alist)
+ "Test the value with KEY in ALIST matches REGEX."
+ (let ((v (kvassoqc key alist)))
+ (and v (string-match regex (cdr v)) v)))
+
+(defun* kvquery->func (query &key
+ (equal-func 'kvassoc=)
+ (match-func 'kvmatch))
+ "Turn a simple QUERY expression into a filter function.
+
+EQUAL-FUNC is the function that implements the equality
+predicate.
+
+MATCH-FUNC is the function that implements the match predicate.
+
+The query language is:
+
+ | a b - true if a or b is true
+ & a b - true only if a and b is true
+ = a b - true if a equals b as per the EQUAL-FUNC
+ ~ a b - true if a matches b as per the MATCH-FUNC
+
+So, for example:
+
+ (|(= a b)(= c d))
+
+Means: if `a' equals `b', or if `c' equals `d' then the
+expression is true."
+ (flet ((query-parse (query)
+ (let ((part (car query))
+ (rest (cdr query)))
+ (cond
+ ((eq part '|)
+ (cons 'or
+ (loop for i in rest
+ collect (query-parse i))))
+ ((eq part '&)
+ (cons 'and
+ (loop for i in rest
+ collect (query-parse i))))
+ ((eq part '~)
+ (destructuring-bind (field value) rest
+ (list match-func field value (quote record))))
+ ((eq part '=)
+ (destructuring-bind (field value) rest
+ (list equal-func field value (quote record))))))))
+ (eval `(lambda (record) ,(query-parse query)))))
+
+(defun kvplist2get (plist2 keyword value)
+ "Get the plist with KEYWORD / VALUE from the list of plists."
+ (loop for plist in plist2
+ if (equal (plist-get plist keyword) value)
+ return plist))
+
+(defun kvthing->keyword (str-or-symbol)
+ "Convert STR-OR-SYMBOL into a keyword symbol."
+ (let ((str
+ (cond
+ ((symbolp str-or-symbol) (symbol-name str-or-symbol))
+ ((stringp str-or-symbol) str-or-symbol))))
+ (intern
+ (if (eq (aref str 0) ?:) str (concat ":" str)))))
+
+(defun kvalist->plist (alist)
+ "Convert an alist to a plist."
+ ;; Why doesn't elisp provide this?
+ (loop for pair in alist
+ append (list
+ (kvthing->keyword
+ (car pair))
+ (cdr pair))))
+
+(defun kvacons (&rest args)
+ "Make an alist from the plist style args."
+ (kvplist->alist args))
+
+(defun keyword->symbol (keyword)
+ "A keyword is a symbol leading with a :.
+
+Converting to a symbol means dropping the :."
+ (if (keywordp keyword)
+ (intern (substring (symbol-name keyword) 1))
+ keyword))
+
+(defun kvplist->alist (plist &optional keys-are-keywords)
+ "Convert PLIST to an alist.
+
+The keys are expected to be :prefixed and the colons are removed
+unless KEYS-ARE-KEYWORDS is `t'.
+
+The keys in the resulting alist are always symbols."
+ (when plist
+ (loop for (key value . rest) on plist by 'cddr
+ collect
+ (cons (if keys-are-keywords
+ key
+ (keyword->symbol key))
+ value))))
+
+(defun kvalist2->plist (alist2)
+ "Convert a list of alists too a list of plists."
+ (loop for alist in alist2
+ append
+ (list (kvalist->plist alist))))
+
+(defun kvalist->keys (alist)
+ "Get just the keys from the alist."
+ (mapcar (lambda (pair) (car pair)) alist))
+
+(defun kvalist->values (alist)
+ "Get just the values from the alist."
+ (mapcar (lambda (pair) (cdr pair)) alist))
+
+(defun kvalist-sort (alist pred)
+ "Sort ALIST (by key) with PRED."
+ (sort alist (lambda (a b) (funcall pred (car a) (car b)))))
+
+(defun kvalist-sort-by-value (alist pred)
+ "Sort ALIST by value with PRED."
+ (sort alist (lambda (a b) (funcall pred (cdr a) (cdr b)))))
+
+(defun kvalist->filter-keys (alist &rest keys)
+ "Return the ALIST filtered to the KEYS list.
+
+Only pairs where the car is a `member' of KEYS will be returned."
+ (loop for a in alist
+ if (member (car a) keys)
+ collect a))
+
+(defun kvplist->filter-keys (plist &rest keys)
+ "Filter the plist to just those matching KEYS.
+
+`kvalist->filter-keys' is actually used to do this work."
+ (let ((symkeys
+ (loop for k in keys
+ collect (let ((strkey (symbol-name k)))
+ (if (equal (substring strkey 0 1) ":")
+ (intern (substring strkey 1))
+ k)))))
+ (kvalist->plist
+ (apply
+ 'kvalist->filter-keys
+ (cons (kvplist->alist plist) symkeys)))))
+
+(defun kvplist2->filter-keys (plist2 &rest keys)
+ "Return the PLIST2 (a list of plists) filtered to the KEYS."
+ (loop for plist in plist2
+ collect (apply 'kvplist->filter-keys (cons plist keys))))
+
+(defun kvalist2->filter-keys (alist2 &rest keys)
+ "Return the ALIST2 (a list of alists) filtered to the KEYS."
+ (loop for alist in alist2
+ collect (apply 'kvalist->filter-keys (cons alist keys))))
+
+(defun kvalist2->alist (alist2 car-key cdr-key &optional proper)
+ "Reduce the ALIST2 (a list of alists) to a single alist.
+
+CAR-KEY is the key of each alist to use as the resulting key and
+CDR-KEY is the key of each alist to user as the resulting cdr.
+
+For example, if CAR-KEY is `email' and CDR-KEY is `name' the
+records:
+
+ '((user . \"nic\")(name . \"Nic\")(email . \"nic@domain\")
+ (user . \"jim\")(name . \"Jim\")(email . \"jim@domain\"))
+
+could be reduced to:
+
+ '((\"nic@domain\" . \"Nic\")
+ (\"jim@domain\" . \"Jic\"))
+
+If PROPER is `t' then the alist is a list of proper lists, not
+cons cells."
+ (loop for alist in alist2
+ collect (apply (if proper 'list 'cons)
+ (list
+ (assoc-default car-key alist)
+ (assoc-default cdr-key alist)))))
+
+(defun kvalist-keys->* (alist fn)
+ "Convert the keys of ALIST through FN."
+ (mapcar
+ (lambda (pair)
+ (cons
+ (funcall fn (car pair))
+ (cdr pair)))
+ alist))
+
+(defun* kvalist-keys->symbols (alist &key (first-fn 'identity))
+ "Convert the keys of ALIST into symbols.
+
+If key parameter FIRST-FN is present it should be a function
+which will be used to first transform the string key. A popular
+choice might be `downcase' for example, to cause all symbol keys
+to be lower-case."
+ (kvalist-keys->*
+ alist
+ (lambda (key)
+ (intern (funcall first-fn (format "%s" key))))))
+
+(defun kvalist2-filter (alist2 fn)
+ "Filter the list of alists with FN."
+ (let (value)
+ (loop for rec in alist2
+ do (setq value (funcall fn rec))
+ if value
+ collect rec)))
+
+(defun kvidentity (a b)
+ "Returns a cons of A B."
+ (cons a b))
+
+(defun kvcar (a b)
+ "Given A B returns A."
+ a)
+
+(defun kvcdr (a b)
+ "Given A B returns B."
+ b)
+
+(defun kvcmp (a b)
+ "Do a comparison of the two values using printable syntax.
+
+Use this as the function to pass to `sort'."
+ (string-lessp (if a (format "%S" a) "")
+ (if b (format "%S" b) "")))
+
+(defun kvqsort (lst)
+ "Do a sort using `kvcmp'."
+ (sort lst 'kvcmp))
+
+(progn
+ (put 'kvalist-key
+ 'error-conditions
+ '(error))
+ (put 'kvalist-key
+ 'error-message
+ "No such key found in alist."))
+
+(defun kvalist-set-value! (alist key value)
+ "Destructively set the value of KEY to VALUE in ALIST.
+
+If the assoc is not found this adds it to alist."
+ (let ((cell (assoc key alist)))
+ (if (consp cell)
+ (setcdr cell value)
+ ;; Else what to do?
+ (signal 'kvalist-key (list alist key)))))
+
+(defun kvdotassoc-fn (expr table func)
+ "Use the dotted EXPR to access deeply nested data in TABLE.
+
+EXPR is a dot separated expression, either a symbol or a string.
+For example:
+
+ \"a.b.c\"
+
+or:
+
+ 'a.b.c
+
+If the EXPR is a symbol then the keys of the alist are also
+expected to be symbols.
+
+TABLE is expected to be an alist currently.
+
+FUNC is some sort of `assoc' like function."
+ (let ((state table)
+ (parts
+ (if (symbolp expr)
+ (mapcar
+ 'intern
+ (split-string (symbol-name expr) "\\."))
+ ;; Else it's a string
+ (split-string expr "\\."))))
+ (catch 'break
+ (while (listp parts)
+ (let ((traverse (funcall func (car parts) state)))
+ (setq parts (cdr parts))
+ (if parts
+ (setq state (cdr traverse))
+ (throw 'break (cdr traverse))))))))
+
+(defun kvdotassoc (expr table)
+ "Dotted expression handling with `assoc'."
+ (kvdotassoc-fn expr table 'assoc))
+
+(defun kvdotassq (expr table)
+ "Dotted expression handling with `assq'."
+ (kvdotassoc-fn expr table 'assq))
+
+(defun kvdotassoc= (expr value table)
+ (let ((v (kvdotassoc expr table)))
+ (and v (equal v value) v)))
+
+(defalias 'dotassoc 'kvdotassoc)
+(defalias 'dotassq 'kvdotassq)
+
+;; Thank you taylanub for this wonderful abstraction.
+(defmacro kv--destructuring-map (map-function args sequence &rest body)
+ "Helper macro for `destructuring-mapcar' and `destructuring-map'."
+ (declare (indent 3))
+ (let ((entry (gensym)))
+ `(,map-function (lambda (,entry)
+ (destructuring-bind ,args ,entry ,@body))
+ ,sequence)))
+
+(defmacro kvmap-bind (args sexp seq)
+ "A hybrid of `destructuring-bind' and `mapcar'
+ARGS shall be of the form used with `destructuring-bind'
+
+Unlike most other mapping forms this is a macro intended to be
+used for structural transformations, so the expected usage will
+be that ARGS describes the structure of the items in SEQ, and
+SEXP will describe the structure desired."
+ (declare (indent 2))
+ `(kv--destructuring-map mapcar ,args ,seq ,sexp))
+
+(defalias 'map-bind 'kvmap-bind)
+
+(defun kvplist-merge (&rest plists)
+ "Merge the 2nd and subsequent plists into the first.
+
+Values set by lists to the left are clobbered."
+ (let ((result (car plists))
+ (plists (cdr plists)))
+ (loop for plist in plists do
+ (loop for (key val) on plist by 'cddr do
+ (setq result (plist-put result key val))))
+ result))
+
+(provide 'kv)
+(provide 'dotassoc)
+
+;;; kv.el ends here