XHT: The extensive hash table library (Emacs package)

Below you find the latest version of (1) the package's README and (2) its main source file.

You may also want to read:

For the git repository and issue tracker, see the project's page on sr.ht.

For more packages, see Software.


README.org

Meta

Compact tree-structure of functions

If you're seeing this as README.org (instead of html), you can:

  1. put point at the beginning of the Functions or Commands heading
  2. maybe C-l (recenter-top-bottom) to put it near the top of the screen
  3. and then press C-c C-k (outline-show-branches).

You'll then have a compact tree-structured view of all the functions and commands.

Commands for accessing this very README

These commands give quick local access to this README.org:

Integration with Helpful and Help buffer

This README.org file has more than 200 named source blocks with usage examples for this library's functions. You can integrate them into Helpful or Help buffers with another package of mine called Democratize, which can also do the same for a few other libraries, such as dash, s, f, and native Emacs libraries. The package also has overview commands that offer alternative ways to visualize all of xht's functions.

Overview

My intention with this library is that:

  1. dealing with hash tables in Emacs Lisp be pleasant,
  2. hash tables become your go-to choice for most key–value operations in the language, and
  3. for almost everything hash-table–related that you might want to do in Emacs Lisp, this library will have functions for it — or at least close enough that you can do it by composing a few of them.

Let me know if I succeeded.

Background

Emacs Lisp hash tables are fast. In fact, faster than alists and plists beyond a couple dozen items (see, for example, the benchmarks in Adam Porter's emacs-package-dev-handbook), and barely slower than plists when below that.

After learning the above I started using Wilfred Hughes's ht library. (I already used his suggest and helpful packages — I recommend both.) I found ht to be a good step above the language's native bare-bones one. Hash tables became my usual choice of data structure when dealing with key–value pairs in Elisp.

After some time, I decided to write down a few extra functions that I thought would be useful to complement the ones from it. These “few” functions soon evolved into a full-fledged, systematically-structured library containing more than 200 functions for doing all sorts of things with hash tables: this one.

Installation

How to install

See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.

Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el file.

If you want to turn on the default keybindings, you can use this:

(use-package xht
  :commands (global-xht-fontify-mode
             global-xht-do-mode
             xht-fontify-mode
             xht-do-mode
             xht-see-readme)
  :config
  (global-xht-fontify-mode)
  (global-xht-do-mode))

You can choose your own keybindings instead. Here's an example that picks four of the functions. Shown below are the default keybindings from xht-do-mode-map, which you could then change to something else.

(use-package xht
  :commands (global-xht-fontify-mode
             global-xht-do-mode
             xht-fontify-mode
             xht-do-mode)
  :bind (("C-x x d" . xht-do)
         ("C-x x c" . xht-do-convert-dwim)
         ("C-x x x" . xht-do-h*-ify-dwim-to-point)
         ("C-x x b" . xht-do-h*-ify-dwim-to-new-buffer))
  :config
  (global-xht-fontify-mode))

I use the third one above particularly often, which is why I chose a third ?x instead of ?i.

See the variable xht-do-mode-map for all the keybindings.

To have all the packages' functions available, you can use :demand t, which is equivalent to require:

(use-package xht
  :demand t
  :bind (("C-x x d" . xht-do)
         ("C-x x c" . xht-do-convert-dwim)
         ("C-x x x" . xht-do-h*-ify-dwim-to-point)
         ("C-x x b" . xht-do-h*-ify-dwim-to-new-buffer))
  :config
  (global-xht-fontify-mode)
  (global-xht-do-mode))

or (with require and no keybindings):

(require 'xht)
(global-xht-fontify-mode)
(global-xht-do-mode)

To use the library in a project

Add xht to the comment header of the project's Elisp file:

;; Package-Requires: ((xht "1.0.5"))

Also add this to the beginning of the ;;; Code: section of the project's Elisp file:

(require 'xht)

Other summaries of callables

Here's an overview of this package's...

Functions (non-interactive)

Function Summary
h-new Create empty hash table.
h-empty-clone Create empty hash table with some or all properties from TABLE.
h-clr Create empty hash table whose properties match those of TABLE.
h-clr! Remove all keys from TABLE.
h* Create a hash table with the key–value PAIRS.
h-s* Create a hash table of size SIZE with the key–value PAIRS.
h-t* Create a hash table of function TEST with the key–value PAIRS.
h-st* Create a hash table of SIZE and TEST with the key–value PAIRS.
h-reduce-r Return a one-key nested table with the KEYS-VALUE sequence.
h--zip-vectors-with Anaphoric version of ‘h-zip-vectors-with’.
h-zip-vectors-with Create a hash table with KEYS and VALUES modified by functions.
h-zip-vectors Create a hash table with KEYS and VALUES. Both must be vectors.
h--zip-lists-with Anaphoric version of ‘h-zip-lists-with’.
h-zip-lists-with Create a hash table with KEYS and VALUES modified by functions.
h-zip-lists Create a hash table with KEYS and VALUES. Both must be lists.
h-let Let-bind dotted symbols to their value in TABLE and execute BODY.
h-let-it Let-bind dotted symbols to their value in THING and execute BODY.
h-htbl-form Hash TABLE object as itself.
h-htbl-cm-str Hash TABLE object as a string in compact representation.
h-htbl-pp-str Hash TABLE object as a string in pretty-printed representation.
h-ht-form Restore canonical (ht…) form that creates hash TABLE.
h-ht-cm-str Restore as string canonical (ht…) form that creates hash TABLE.
h-ht-pp-str The pretty-print string of (ht…) form that creates hash TABLE.
h-h*-form Restore canonical (h*…) or (h-st*…) form that creates hash TABLE.
h-h*-cm-str Restore as string (h*…) or (h-st*…) form that creates hash TABLE.
h-h*-pp-str The pretty-print string of (h*…) form that creates hash TABLE.
h-lambdify Convert an HT-LIKE object into a lambda that creates the table.
h-format Format as FORMAT hash table–like object HT-LIKE.
h-vmap Apply FUN to each key–value pair of TABLE and return vector.
h--vmap Anaphoric version of ‘h-vmap’.
h--vunzip-with Anaphoric version of ‘h-vunzip-with’.
h-vunzip-with Return a list of modified vectors of KEYS and VALUES from TABLE.
h-vunzip Return a list of the vectors of KEYS and VALUES from TABLE.
h-vitems Return a vector of two-element lists '(key value) from TABLE.
h-vkeys Return a vector of all the keys in TABLE.
h-vvalues Return a vector of all the values in TABLE.
h-vrandom Return a vector of N pseudo-randomly chosen items from hash TABLE.
h-lmap Apply FUN to each key–value pair of TABLE; list the results.
h--lmap Anaphoric version of ‘h-lmap’.
h-lkeep Apply FUN to each key–value pair of TABLE; list non-nil results.
h--lkeep Anaphoric version of ‘h-lkeep’.
h--lunzip-with Anaphoric version of ‘h-lunzip-with’.
h-lunzip-with Return a list of modified lists of KEYS and VALUES from TABLE.
h-lunzip Return a list of the lists of KEYS and VALUES from TABLE.
h-litems Return a list of two-element lists '(key value) from TABLE.
h-lkeys Return a list of all the keys in TABLE.
h-lvalues Return a list of all the values in TABLE.
h-lrandom Return a list of N pseudo-randomly chosen items from hash TABLE.
h-hmap Return a table from applying KEY-FUN, VAL-FUN to each pair of TABLE.
h--hmap Anaphoric version of ‘h-hmap’.
h-hmap! Update TABLE by applying KEY-FUN, VAL-FUN to its pairs.
h--hmap! Anaphoric version of ‘h-hmap!’.
h-hmap* Return a table from applying KEY-FUN, VAL-FUN to each pair of TABLE.
h--hmap* Anaphoric version of ‘h-hmap*’.
h-hmap*! Update TABLE by applying KEY-FUN, VAL-FUN to each of its pairs.
h--hmap*! Anaphoric version of ‘h-hmap*!’.
h-each Apply function FUN to each key–value pair of TABLE.
h--each Anaphoric version of ‘h-each’.
h-pop Return the very first pair from TABLE.
h-pop! Return the very first pair from TABLE and remove it.
h-pop-random Return a pseudo-random pair from TABLE.
h-pop-random! Return a pseudo-random pair from TABLE and remove it.
h-first Return the first pair from TABLE for which FUN returns non-nil.
h--first Anaphoric version of ‘h-first’.
h-first! Remove from TABLE the first pair for which FUN returns non-nil.
h--first! Anaphoric version of ‘h-first!’.
h-put! Associate KEY in TABLE with VALUE.
h-put Return a table that is TABLE with KEY–VALUE applied.
h-put*! Set VALUE to KEYS sequence in hash table TABLE.
h-put* Return a table that is TABLE with KEYS–VALUE applied.
h-put-add! Add VALUE to the current value of KEY in hash table TABLE.
h-put-add Return a table that is TABLE with VALUE added to KEY's current.
h-put-add*! Add VALUE to the current KEYS sequence's value in hash table TABLE.
h-put-add* Return a table that is TABLE with VALUE added to KEYS' current.
h-rem! Remove KEY from hash table TABLE.
h-rem Return a table that is TABLE with KEY removed.
h-rem*! Remove key at end of KEYS sequence from hash table TABLE.
h-rem* Return a table that is TABLE with key at end of KEYS removed.
h-sel-keys Return a copy of TABLE with only the pairs specified by KEYS.
h-sel-keys! Update TABLE to contain only the pairs specified by KEYS.
h-sel Return a table with pairs from TABLE for which FUN returns non-nil.
h--sel Anaphoric version of ‘h-sel’.
h-sel! Update TABLE by keeping only pairs for which FUN returns non-nil.
h--sel! Anaphoric version of ‘h-sel!’.
h-sel* Return a table with pairs from TABLE for which FUN returns non-nil.
h--sel* Anaphoric version of ‘h-sel*’.
h-sel*! Update TABLE by keeping only pairs for which FUN returns non-nil.
h--sel*! Anaphoric version of ‘h-sel*!’.
h-rej-keys Return a copy of TABLE with all but the pairs specified by KEYS.
h-rej-keys! Update TABLE by removing the pairs specified by KEYS.
h-rej Return a table with pairs from TABLE for which FUN returns nil.
h--rej Anaphoric version of ‘h-rej’.
h-rej! Update TABLE by keeping only pairs for which FUN returns nil.
h--rej! Anaphoric version of ‘h-rej!’.
h-rej* Return a table with pairs from TABLE for which FUN returns nil.
h--rej* Anaphoric version of ‘h-rej*’.
h-rej*! Update TABLE by keeping only pairs for which FUN returns nil.
h--rej*! Anaphoric version of ‘h-rej*!’.
h-reverse Create a hash table identical to TABLE with key order reversed.
h-reverse! Reverse the key order of hash table TABLE.
h-reverse* Create a hash table identical to TABLE with key order reversed.
h-reverse*! Reverse the key order of hash table TABLE.
h-put-num-with* Return copy of TABLE with FUN applied to numeric value of KEYS.
h-put-num-with*! Apply FUN to the numeric value of KEYS in possibly nested hash TABLE.
h-put-inc* Return a copy of TABLE with 1 added to the value of KEYS.
h-put-inc*! Add 1 to the value of KEYS in possibly nested hash TABLE.
h-put-dec* Return a copy of TABLE with 1 subtracted from the value of KEYS.
h-put-dec*! Subtract 1 from the value of KEYS in possibly nested hash TABLE.
h-has-key? Return t if TABLE has KEY.
h-has-key*? Whether possibly-nested TABLE has KEY at some level.
h-has-value? Return t if TABLE has some key whose value is VALUE.
h-has-value*? Whether possibly-nested TABLE has at some level key–VALUE pair.
h-has-pair? Return t if TABLE has KEY and its value is VALUE.
h-has-pair*? Whether possibly-nested TABLE has at some level KEY–VALUE pair.
h-has-keys? Return t if TABLE has all KEYS.
h-has-values? Return t if TABLE has all VALUES.
h-has-pairs? Return t if TABLE has all PAIRS.
h-mix! Update TABLE according to every key–value pair in FROM-TABLES.
h-mix Return a table that has all key–value pairs from the tables.
h-mix*! Update TABLE by adding every key–value pair in FROM-TABLES.
h-mix* Return a table that has all key–value pairs from the tables.
h-dif! Update TABLE by removing all matching key–value pairs in FROM-TABLES.
h-dif Return a table of all key–value pairs in TABLE not in FROM-TABLES.
h-dif*! Update TABLE by removing all matching key–value pairs in FROM-TABLES.
h-dif* Return a table of all key–value pairs in TABLE not in FROM-TABLES.
h-cmn! Update TABLE by keeping only the intersection with FROM-TABLES.
h-cmn Return a table of all key–value pairs in TABLE also in FROM-TABLES.
h-cmn*! Update TABLE by keeping only the intersection with FROM-TABLES.
h-cmn* Return a table of all key–value pairs in TABLE also in FROM-TABLES.
h-kvl= Return t only if all KEY–VALUE LINES have same KEY–VALUE pairs.
h-pr== One of the ways to see if all hash tables TABLES are equivalent.
h-pr= One of the ways to see if all hash tables TABLES are equivalent.
h== One of the ways to see if all hash tables TABLES are equivalent.
h= One of the ways to see if all hash tables TABLES are equivalent.
h_= One of the ways to see if all hash tables TABLES are equivalent.
h~= One of the ways to see if all hash tables TABLES are equivalent.
h-props= Whether all hash tables TABLES have the same properties.
h-alist= Return t only if all ALISTs have same KEY–VALUE pairs.
h-plist= Return t only if all PLISTs have same KEY–VALUE pairs.
h-json= Return t only if all JSONs have same KEY–VALUE pairs.
h-lol= Return t only if all LISTS OF LISTS have same KEY–VALUE pairs.
h-orgtbl= Return t only if all ORG TABLES have same KEY–VALUE pairs.
h-tsv= Return t only if all TSVs have same KEY–VALUE pairs.
h-csv= Return t only if all CSVs have same KEY–VALUE pairs.
h-ssv= Return t only if all SSVs have same KEY–VALUE pairs.
h-type Detect object OBJ's type-for-conversion. Return a keyword.
h-type= Are all OBJECTS of same type?
h-it= Are all OBJECTS, all of same type, reducible to each other?
h-it~ Are all OBJECTS equivalent according to XHT tests?
h-clone* Create a deep copy of possibly-nested hash table TABLE.
h<-ht Given hash table TABLE, return it or a clone, depending on params.
h-2d<-1d Make 2D the 1D hash table TABLE by adding header fields F1, F2.
h-1d<-2d Make 1D (KVL-like) the 2D hash table TABLE2D.
h<-vector Convert vector VECTOR to a hash table using VECTOR's indices as keys.
h->vector Convert hash table TABLE to vector.
h<-list Convert LIST to a hash table using LIST's indices as keys.
h->list Convert hash table TABLE to list.
h<-lines Convert string STR to a hash table using line numbers as keys.
h->lines Convert hash table TABLE whose keys are numbers to string.
h<-kvl Convert key–value lines KVL to a hash table.
h->kvl Convert hash table TABLE to SEP-separated key–value lines.
h<-cons-pair Convert CONS-PAIR to hash table.
h->cons-pair Convert hash table TABLE to cons pair.
h<-alist Convert ALIST to hash table.
h<-alist* Convert possibly nested ALIST to hash table.
h->alist Convert hash table TABLE to alist.
h->alist* Convert hash table TABLE to alist, maybe recursing.
h<-plist Convert PLIST to hash table.
h<-plist* Convert possibly nested PLIST to hash table.
h->plist Convert hash table TABLE to plist.
h->plist* Convert hash table TABLE to plist, maybe recursing.
h<-json* Convert possibly nested JSON to hash table.
h->json* Convert hash table TABLE to JSON, maybe recursing.
h<-lol Create a hash table with initial values according to list of lists LOL.
h->lol Do the opposite of ‘h<-lol’, which see. Return list of lists.
h<-orgtbl Create a hash table with initial values according to org table ORGTBL.
h->orgtbl Do the opposite of ‘h<-orgtbl’, which see. Return org table as string.
h<-tsv Create a hash table with initial values according to TSV.
h->tsv Do the opposite of ‘h<-tsv’, which see. Return tsv.
h<-ssv Create a hash table with initial values according to SSV.
h->ssv Do the opposite of ‘h<-ssv’, which see. Return ssv.
h->csv Do the opposite of ‘h<-csv’, which see. Return csv.
h<-it Convert THING to hash table. Try to guess what it is.
h-kvl? Could string STR be a KVL?
h-alist? Non-nil if and only if LIST is a non-nil alist with simple keys.
h-plist? Non-nil if and only if LIST is a non-nil plist with non-nil atom keys.
h-lol? Return t if OBJ is a list of lists.
h-orgtbl? Could string STR be an Org Table?
h-json? Could string STR be a JSON object?
h-tsv? Could string STR be a TSV?
h-csv? Could string STR be a CSV?
h-ssv? Could string STR be an SSV?
h-prop Return the value of property PROP of hash table TABLE.
h-props Return a hash table containing properties of hash table TABLE.
h-dim Determine dimension of hash table TABLE.
h-1d? Return t if OBJ is a 1D hash table, nil otherwise.
h-2d? Return t if OBJ is a 2D hash table, nil otherwise.
h-nested? Return t if OBJ is a nested hash table, nil otherwise.
h-it-dim Determine dimension of object OBJ.
h-it-empty? Return t if OBJ is empty, nil otherwise.
h-it-1d? Return t if OBJ is of dimension 1, nil otherwise.
h-it-2d? Return t if OBJ is of dimension 2, nil otherwise.
h-it-nested? Return t if OBJ is nested, nil otherwise.
h-write! Write OBJ to file FILENAME.
h-write-ht-like! Write hash table HT-LIKE to file FILENAME formatted as FORMAT.
h-write-htbl-cm! Write hash table HT-LIKE to file FILENAME formatted as 'htbl-cm.
h-write-htbl-pp! Write hash table HT-LIKE to file FILENAME formatted as 'htbl-pp.
h-write-h*-cm! Write hash table HT-LIKE to file FILENAME formatted as 'h*-cm.
h-write-ht-cm! Write hash table HT-LIKE to file FILENAME formatted as 'ht-cm.
h-write-h*-pp! Write hash table HT-LIKE to file FILENAME formatted as 'h*-pp.
h-write-ht-pp! Write hash table HT-LIKE to file FILENAME formatted as 'ht-pp.
h-read Deal with object OBJ, expected to be of type TYPE.
h-read-ht-like Deal with object OBJ, expected to be ht-like.
h-read-htbl Deal with object OBJ, expected to be a hash table.
h-read-s Deal with object OBJ, expected to be a string.
h-read-kvl Deal with object OBJ, expected to be a KVL.
h-read-tsv Deal with object OBJ, expected to be a TSV.
h-read-csv Deal with object OBJ, expected to be a CSV.
h-read-ssv Deal with object OBJ, expected to be a SSV.
h-read-json Deal with object OBJ, expected to be JSON.
h-read-lines Deal with object OBJ, expected to be lines of text.
h-read-orgtbl Deal with object OBJ, expected to be an org table.
h-read-lol Deal with object OBJ, expected to be a list of lists.
h-read-alist Deal with object OBJ, expected to be an association list.
h-read-plist Deal with object OBJ, expected to be a property list.
h-read-list Deal with object OBJ, expected to be a list.
h-read-cons-pair Deal with object OBJ, expected to be a cons pair.
h-read-vector Deal with object OBJ, expected to be a vector.
h-as-string Return OBJ as a string.
h-as-keyword Return OBJ as a keyword.
h-as-symbol Return OBJ as an interned symbol.
h-as-number Return OBJ as a number, if convertible.
h-2d-new Return a 2D hash table from KEYS and HEADER.
h-2d-header Return the header of 2D hash table TABLE2d.
h-2d-idcol The ID column of 2D hash table TABLE2D, as list. It's ID + keys.
h-2d-id Find the ID-like element of the header of 2D hash table TABLE2D.
h-2d-row List row from 2D hash table TABLE2D given KEY and HEADER.
h-2d-col List column from 2D hash table TABLE2D given COL and IDCOL.
h-2d-hmap Return a 2D table from applying functions to each triad of TABLE2D.
h--2d-hmap Anaphoric version of ‘h-2d-hmap’.
h-2d-hmap! Update TABLE2D by applying functions to each of its triads.
h--2d-hmap! Anaphoric version of ‘h-2d-hmap!’.
h-2d-sel-cols Return a table with only selected named columns COLS of TABLE2D.
h-2d-sel-cols! Remove all but named columns COLS from 2D hash table TABLE2D.
h-2d-rej-cols Return a table without the named columns COLS of TABLE2D.
h-2d-rej-cols! Remove named columns COLS from 2D hash table TABLE2D.
h-2d-transpose Return a hash table that is TABLE2D transposed.
h-2d-transpose! Transpose TABLE2D.
h-as-set<-vector Create a hash table as set from VECTOR.
h-as-set<-list Create a hash table as set from LIST.
h-as-set<-lines Create a hash table as set from the lines of string STR.
h-as-set<-words Create a hash table as set from the words of string STR.
h-count<-vector Create a hash table with the elements of VECTOR and their count.
h-count<-list Create a hash table with the elements of LIST and their count.
h-count<-lines Create a hash table with the lines of string STR and their count.
h-count<-words Create a hash table with the words of string STR and their count.
h-count-put Create a hash table with objects OBJS with the counts as values.
h-count-put! Add counts of objects OBJS to the counts of hash table TABLE.

Commands

Function Summary
xht-do The main command for you to do all sorts of hash table things.
xht-do-convert-dwim Convert region, thing at point or last-sexp — try to guess it.
xht-do-h*-ify-dwim-to-clipboard First h*-ify region, thing at point or last-sexp; send it to clipboard.
xht-do-h*-ify-dwim-to-file First h*-ify region, thing at point or last-sexp; write to file.
xht-do-h*-ify-dwim-to-replace First h*-ify region, thing at point or last-sexp; replace source.
xht-do-h*-ify-dwim-to-new-buffer First h*-ify region, thing at point or last-sexp; insert at new buffer.
xht-do-h*-ify-dwim-to-point First h*-ify region, thing at point or last-sexp; insert at point.
xht-do-h*-ify-selected-region-to-point First h*-ify selected region; then insert results at point.
xht-do-h*-ify-thing-at-point-to-point First h*-ify thing at point; then insert results at point.
xht-do-h*-ify-last-sexp-to-point First h*-ify last-sexp; then insert results at point.
xht-do-mode Enable keys for ‘xht-do’ actions.
global-xht-do-mode Toggle Xht-Do mode in all buffers.
xht-fontify-mode Toggle fontification of XHT/HT special variables.
global-xht-fontify-mode Toggle Xht-Fontify mode in all buffers.
xht-see-readme Open xht's README.org file.
xht-see-function-in-readme See FUNCTION in xht's README.org.
xht-see-news See the News in xht's README.org file.

Aliases

This function… is an alias to…
h-get ht-get
h-get* ht-get*
h-empty? ht-empty?
h? hash-table-p
h-size hash-table-count
h-copy copy-hash-table
h-has-key-p h-has-key?
h-has-key*-p h-has-key*?
h-has-keys-p h-has-keys?
h-has-value-p h-has-value?
h-has-value*-p h-has-value*?
h-has-values-p h-has-values?
h-has-pair-p h-has-pair?
h-has-pair*-p h-has-pair*?
h-has-pairs-p h-has-pairs?
h-1d-p h-1d?
h-2d-p h-2d?
h-nested-p h-nested?
h-it-1d-p h-it-1d?
h-it-2d-p h-it-2d?
h-it-nested-p h-it-nested?
h-csv-p h-csv?
h-ssv-p h-ssv?
h-tsv-p h-tsv?
h-kvl-p h-kvl?
h-lol-p h-lol?
h-json-p h-json?
h-alist-p h-alist?
h-plist-p h-plist?
h-orgtbl-p h-orgtbl?
h-empty-p ht-empty-p
h-p hash-table-p
h-random h-lrandom
h-unzip-with h-lunzip-with
h--unzip-with h--lunzip-with
h-unzip h-lunzip
h-items h-litems
h-keys h-lkeys
h-values h-lvalues
h-write-h*! h-write-h*-pp!
h-write-ht! h-write-ht-pp!
h-write-htbl! h-write-htbl-pp!
h-read-h h-read-ht-like
h-2d-keys h-lkeys
h-1d->2d h-2d<-1d
h-2d->1d h-1d<-2d
h<-csv xht<--csv

And if you're already familiar with ht's functions, there's still another summary table in the Appendix.


Enough summaries.

Non-interactive functions and commands are described in detail below, accompanied by about 2056 examples.

You'll note that each of the examples have an equality operator. Here's what they mean:

;;  symbol   function
  '((   => . equal    )   ; regular equality
    (Hp==> . h-pr==   )   ; equality for hash tables       ├> Functions
    ( Hp=> . h-pr=    )   ; equality for hash tables       │  provided
    ( H==> . h==      )   ; equality for hash tables       │  by this
    (  H=> . h=       )   ; equality for hash tables       │  very
    ( H_=> . h_=      )   ; equality for hash tables       │  package
    ( H~=> . h~=      )   ; equality for hash tables       │
    (  O=> . h-orgtbl=)   ; equality for org table strings │
    (  A=> . h-alist= )   ; equality for association lists │
    (  P=> . h-plist= )   ; equality for property lists    │
    (  L=> . h-lol=   )   ; equality for lists of lists    │
    (  T=> . h-tsv=   )   ; equality for TSVs              │
    (  C=> . h-csv=   )   ; equality for CSVs              │
    (  S=> . h-ssv=   )   ; equality for SSVs              │
    (  J=> . h-json=  )   ; equality for JSONs             │
    (  K=> . h-kvl=   )   ; equality for key–value lines   │
    (   ~> . exemplify-ert-approx=)) ; equality for floats

Some simple examples, so that you get their meaning:

(+ 20 10 10 2)       =>  42

(cons :a '(1))       =>  '(:a 1)

'(:a 1 :b 2)        P=>  '(:b 2 :a 1)

'((a . 1) (b . 2))  A=>  '((b . 2) (a . 1))

'((a . 1) (b . 2))  A=>  '((b . 2) (a . 1) (b . 5))

(ht (:a 1) (:b 2))  H=>  (ht (:b 2) (:a 1))

(ht (:a 1) (:b 2))  H=>  (h* :a 1 :b 2)

(h* :b 2 :a 1)      H=>  (h* :a 1 :b 2)

So let's see the functions and commands.

Naming conventions

This library adopts the following naming conventions:

These functions look like
Regular h-foo
Anaphoric h--foo
Interactive xht-foo
Private xht--foo

and

These variables look like
Regular h-foo
Customizable xht-foo
Private xht--foo

The two tables above represent almost all of the more than 500
symbols defined by xht. These also happen to be the same familiar
conventions that have long been adopted by dash.

There are some functions that convert from and to hash tables:

These functions look like
Regular (conversion) h->foo, h<-foo
Private (conversion) xht-->foo, xht<--foo

This intuitive use of '->' and '<-' to represent conversion will
certainly look familiar to those who have used ht.

That's pretty much it.

The only symbols that don't perfectly match the tables above are:

Now, complementing the previous conventions, you'll notice that
some suffixes are consistently employed:

?
Functions that have an ? as suffix are predicates
(they "ask" a yes-or-no question and return t or nil).
They have a corresponding -p alias.
=
Functions that have an = as suffix are equality predicates
and therefore check whether two or more things can be
considered equivalent according to some standard.
*
Functions that act on nested hash tables have an * as
suffix, and vice-versa. (With the notable exception of h*,
whose * has a slightly different, but hopefully obvious
enough, connotation.)
!

Functions that destructively modify the table that is
passed as argument have an ! as suffix, and vice-versa.

There are no !-less aliases for destructive functions:
wherever the ! is dropped, it means it's side-effect free:
the value of the result of the operations is returned, and
TABLE remains unmodified. If you find it happening
otherwise, it's most likely unintentional and a bug — please
report.

Note that this differs from ht's naming convention, in which
ht-clear, ht-remove, ht-set, and ht-update remain as
aliases to, respectively, ht-clear!, ht-remove!,
ht-set!, and ht-update! — all of which destructive
functions. (But ht-reject is not an alias to ht-reject!:
the former is non-destructive, the latter is destructive.)

On the other hand, xht follows this convention consistently.
For example:

Non-destructive Destructive
h-rej h-rej!
h-sel h-sel!
h-put h-put!
h-mix h-mix!
h-mix* h-mix*!

etc.

The library itself may be called either XHT or xht. Both are fine.

Mixed casing is better avoided. Yes, you'll see it titleized in the
docstrings of global-xht-do-mode and global-xht-fontify-mode
but only because these docstrings happen to be automatically
generated by define-globalized-minor-mode, which calls
easy-mmode-pretty-mode-name, which titleizes names.

Functions

General hash table operations

Creation

Functions that create a hash table.

Empty one

Functions that create an empty hash table.

h-new (&optional sz ts we rs rt)

Create empty hash table.

Arguments passed correspond to, respectively, size, test, weakness,
rehash-size, rehash-threshold. When nil, they match that of TABLE.

SZ is the initial assigned size. When nil, default to 65.

TS is the function used to compare the hash keys.
It can be eq, eql, equal or a user-supplied test created
via define-hash-table-test. When nil, default to equal.

The other three default to Emacs' defaults:

  • WE defaults to nil.
  • RS defaults to 1.5.
  • RT defaults to 0.8.

This is similar to ht-create. The differences are:

  • size as a first optional argument, test as second;
  • possibility of passing other parameters accepted
    by make-hash-table, which see.

See also: h-clr and h-clr!.

;; note: remember that H=> (that is, #'h=)
;; doesn't check for :size or :test
(h-new)          H=> (ht-create)
(h-new 20)       H=> (ht-create)
(h-new nil 'eq)  H=> (ht-create)

(-> (h-new)                 (h-prop        'size))  => 65
(-> (h-new)                 (h-prop        'test))  => 'equal
(-> (h-new 20)              (h-prop        'size))  => 20
(-> (h-new 20)              (h-prop        'test))  => 'equal
(-> (h-new nil 'eq)         (h-prop        'size))  => 65
(-> (h-new nil 'eq)         (h-prop        'test))  => 'eq
(-> (h-new 20  'eq)         (h-prop        'size))  => 20
(-> (h-new 20  'eq)         (h-prop        'test))  => 'eq
(-> (h-new nil nil nil 1.4) (h-prop 'rehash-size))  => 1.4
(-> (h-new nil nil 'key)    (h-prop    'weakness))  => 'key
h-empty-clone (table &optional sz ts we rs rt)

Create empty hash table with some or all properties from TABLE.

Arguments passed correspond to, respectively, size, test, weakness,
rehash-size, rehash-threshold. When nil, they match that of TABLE.

This is a non-destructive function.
See also: h-clr and h-clr!.

(-> (h-st* 10 'eq :a 1)  (h-empty-clone 5)  (h-prop 'size)) => 5
(-> (make-hash-table :rehash-size 1.2 :test 'eq)
    (h-put :a 1)
    (h-put :b 2)
    (h-empty-clone nil nil nil 1.3))
Hp==> (make-hash-table :rehash-size 1.3 :test 'eq)

;;;; The below should be identical to using h-clr
(-> (h-st* 10 'eq :a 1)  h-empty-clone  (h-prop 'test)) => 'eq
(-> (h-st* 10 'eq :a 1)  h-empty-clone  (h-prop 'size)) => 10
(-> (make-hash-table :rehash-size 1.2 :test 'eq)
    (h-put :a 1)
    (h-put :b 2)
    h-empty-clone)
Hp==> (make-hash-table :rehash-size 1.2 :test 'eq)

(let* ((tbl (ht (:a 1) (:b 2))))
  (h-empty-clone tbl))
H=>  (ht)

(let* ((tbl (make-hash-table :size 10 :test 'eq))
       res)
  (h-put! tbl :a 1)
  (setq res (h-empty-clone tbl))
  (list 'tbl "has properties"
        :test (h-prop  tbl 'test)
        :size (h-prop  tbl 'size)
        :data (ht->plist tbl)

        'res "has properties"
        :test (h-prop  res 'test)
        :size (h-prop  res 'size)
        :data (ht->plist res)))
=> (list 'tbl "has properties" :test 'eq :size 10 :data '(:a 1)
         'res "has properties" :test 'eq :size 10 :data nil)
h-clr (table)

Create empty hash table whose properties match those of TABLE.

It preserves all of the properties of the original table — except
data, which is empty. It's therefore particularly useful as a basis
for other hash table functions (such as h-hmap*) that perform
non-destructive operations — for which a fresh empty table must be
created, and for which the use of h-new or ht-create would
result in a loss of these properties.

This is a non-destructive function.
Its destructive counterpart is h-clr!.

(-> (h-st* 10 'eq :a 1)  h-clr  (h-prop 'test)) => 'eq
(-> (h-st* 10 'eq :a 1)  h-clr  (h-prop 'size)) => 10
(-> (make-hash-table :rehash-size 1.2 :test 'eq)
    (h-put :a 1)
    (h-put :b 2)
    h-clr)
Hp==> (make-hash-table :rehash-size 1.2 :test 'eq)

(let* ((tbl (ht (:a 1) (:b 2))))
  (h-clr tbl))
H=>  (ht)

(let* ((tbl (make-hash-table :size 10 :test 'eq))
       res)
  (h-put! tbl :a 1)
  (setq res (h-clr tbl))
  (list 'tbl "has properties"
        :test (h-prop  tbl 'test)
        :size (h-prop  tbl 'size)
        :data (ht->plist tbl)

        'res "has properties"
        :test (h-prop  res 'test)
        :size (h-prop  res 'size)
        :data (ht->plist res)))
=> (list 'tbl "has properties" :test 'eq :size 10 :data '(:a 1)
         'res "has properties" :test 'eq :size 10 :data nil)
h-clr! (table)

Remove all keys from TABLE.

This is equivalent to current ht-clear!.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-clr.

(let* ((tbl (ht (:a 1) (:b 2))))
  (h-clr! tbl)
  tbl)
H=>  (ht)

(let* ((tbl (make-hash-table :size 10 :test 'eq)))
  (h-put! tbl :a 1)
  (h-clr! tbl)
  (list 'tbl "has properties"
        :test (h-prop  tbl 'test)
        :size (h-prop  tbl 'size)
        :data (ht->plist tbl)))
=> `(tbl "has properties" :test eq :size 10 :data nil)
Cloning and copying

See h-clone* and h<-ht.

From keys and values

Functions to create a hash table from keys and values.

passed directly
h* (key-1 value-1 key-2 value-2 ...)

Create a hash table with the key–value PAIRS.

Keys are compared with equal.

This function is similar to ht, with the difference that no
parentheses are used around each key–value pair: so its syntax is
identical to that used to create a plist, but replacing list with
h*.

Also, caution: with plists and alists, repeated items that come
first have priority, whereas when passing pairs to ht or h*.
it's the opposite — the repeated item's last value wins.

Compare:

(plist-get '(:a 1 :b 2 :a 5) :a)       => 1
(ht-get (h*  :a 1 :b 2 :a 5) :a)       => 5
(ht-get (ht (:a 1) (:b 2) (:a 5)) :a)  => 5

Another difference is that, instead of 65, its initial nominal size
defaults to the result of xht--init-size, which determines
initial nominal size as a function of the number of pairs.
Currently, xht--init-size uses xht--init-size-exact, which
returns the exact number of pairs.

(h* :a 1 :b 2 :c 3 :d 4 :e 5)  H=>  (ht (:a 1)(:b 2)(:c 3)(:d 4)(:e 5))
(h* :a 1 :b 2)                 H=>  (ht (:a 1) (:b 2))
(h* 'a 1 'b 2)                 H=>  (ht ('a 1) ('b 2))
(h* "a"1 "b"2)                 H=>  (ht ("a"1) ("b"2))
(h* :a 1 :b (h* :c 3))         H=>  (ht (:a 1) (:b (ht (:c 3))))
(h* :a 1)                      H=>  (ht (:a 1))
(h*)                           H=>  (ht)
(h* :a 1 :b)                   !!>  wrong-number-of-arguments
(h* :a)                        !!>  wrong-number-of-arguments
h-s* (size key-1 value-1 key-2 value-2 ...)

Create a hash table of size SIZE with the key–value PAIRS.

This is exactly like h* (which see), with the difference that its
nominal size is explicitly passed as first argument instead of being
inferred.

When SIZE is nil, it defaults to h*'s default.

(-> (h-s* 20  :b 2)      (h-prop 'size)) => 20
(-> (h-s* 20  :b 2)      (h-prop 'size)) => 20
(-> (h-s* nil :b 2)      (h-prop 'size)) => 1
(-> (h-s* nil :a 1 :b 2) (h-prop 'size)) => 2
(-> (h-s* nil :a 1 :b 2) (h-prop 'size)) => 2

(h-s* nil :a 1 :b 2)          H=> (h* :a 1 :b 2)

(h-s* 20 :a 1 :b 2 :c 3)      H=>  (ht (:a 1)(:b 2)(:c 3))
(h-s* 20 :a 1 :b 2)           H=>  (ht (:a 1) (:b 2))
(h-s* 20 'a 1 'b 2)           H=>  (ht ('a 1) ('b 2))
(h-s* 20 "a"1 "b"2)           H=>  (ht ("a"1) ("b"2))
(h-s* 20 :a 1 :b (h* :c 3))   H=>  (ht (:a 1) (:b (ht (:c 3))))
(h-s* 20 :a 1)                H=>  (ht (:a 1))
(h-s* 20 )                    H=>  (ht)
(h-s* 20 :a 1 :b)             !!>  wrong-number-of-arguments
(h-s* 20 :a)                  !!>  wrong-number-of-arguments
(h-s* 20 :a 1 :b)             !!>  wrong-number-of-arguments
(h-s* 20 :a)                  !!>  wrong-number-of-arguments
(h-s* :a 1 :b)                !!>  error
h-t* (test key-1 value-1 key-2 value-2 ...)

Create a hash table of function TEST with the key–value PAIRS.

This is exactly like h* (which see), with the difference that its
test function is explicitly passed as first argument instead of
being inferred.

When TEST is nil, it defaults to h*'s default, which is h-new's
default: 'equal.

(-> (h-t* 'eq :b 2)      (h-prop 'test)) => 'eq
(-> (h-t* nil :b 2)      (h-prop 'test)) => 'equal
(-> (h-t* 'eq :b 2)      (h-prop 'test)) => 'eq
(-> (h-t* nil :a 1 :b 2) (h-prop 'test)) => 'equal

(h-t* nil :a 1 :b 2)           H=> (h* :a 1 :b 2)

(h-t* 'eq :a 1 :b 2 :c 3)      H=>  (ht (:a 1)(:b 2)(:c 3))
(h-t* 'eq :a 1 :b 2)           H=>  (ht (:a 1) (:b 2))
(h-t* 'eq 'a 1 'b 2)           H=>  (ht ('a 1) ('b 2))
(h-t* 'eq "a"1 "b"2)           H=>  (ht ("a"1) ("b"2))
(h-t* 'eq :a 1 :b (h* :c 3))   H=>  (ht (:a 1) (:b (ht (:c 3))))
(h-t* 'eq :a 1)                H=>  (ht (:a 1))
(h-t* 'eq )                    H=>  (ht)
(h-t* 'eq :a 1 :b)             !!>  wrong-number-of-arguments
(h-t* 'eq :a)                  !!>  wrong-number-of-arguments
(h-t* nil :a 1 :b)             !!>  wrong-number-of-arguments
(h-t* nil :a)                  !!>  wrong-number-of-arguments
(h-t* :a 1 :b)                 !!>  error
h-st* (size test key-1 value-1 key-2 value-2 ...)

Create a hash table of SIZE and TEST with the key–value PAIRS.

This is exactly like h* (which see), with the difference that its
nominal size and its test function are explicitly passed as
first arguments instead of being inferred.

When SIZE is nil, it defaults to h*'s default.

When TEST is nil, it defaults to h*'s default, which is h-new's
default: 'equal.

(-> (h-st* 20  'eq :b 2)      (h-prop 'test)) => 'eq
(-> (h-st* 20  'eq :b 2)      (h-prop 'size)) => 20
(-> (h-st* 20  nil :b 2)      (h-prop 'test)) => 'equal
(-> (h-st* 20  nil :b 2)      (h-prop 'size)) => 20
(-> (h-st* nil 'eq :b 2)      (h-prop 'test)) => 'eq
(-> (h-st* nil 'eq :b 2)      (h-prop 'size)) => 1
(-> (h-st* nil 'eq :a 1 :b 2) (h-prop 'size)) => 2
(-> (h-st* nil nil :a 1 :b 2) (h-prop 'test)) => 'equal
(-> (h-st* nil nil :a 1 :b 2) (h-prop 'size)) => 2

(h-st* nil nil :a 1 :b 2)          H=> (h* :a 1 :b 2)

(h-st* 20 'eq :a 1 :b 2 :c 3)      H=>  (ht (:a 1)(:b 2)(:c 3))
(h-st* 20 'eq :a 1 :b 2)           H=>  (ht (:a 1) (:b 2))
(h-st* 20 'eq 'a 1 'b 2)           H=>  (ht ('a 1) ('b 2))
(h-st* 20 'eq "a"1 "b"2)           H=>  (ht ("a"1) ("b"2))
(h-st* 20 'eq :a 1 :b (h* :c 3))   H=>  (ht (:a 1) (:b (ht (:c 3))))
(h-st* 20 'eq :a 1)                H=>  (ht (:a 1))
(h-st* 20 'eq )                    H=>  (ht)
(h-st* 20 'eq :a 1 :b)             !!>  wrong-number-of-arguments
(h-st* 20 'eq :a)                  !!>  wrong-number-of-arguments
(h-st* 20 nil :a 1 :b)             !!>  wrong-number-of-arguments
(h-st* 20 nil :a)                  !!>  wrong-number-of-arguments
(h-st* 20 :a 1 :b)                 !!>  error
h-reduce-r (&rest keys-value)

Return a one-key nested table with the KEYS-VALUE sequence.

So, for example, (h-reduce-r :a :b :c 2) is equivalent
to (h* :a (h* :b (h* :c 2))), which is the same
as (ht (:a (ht (:b (ht (:c 2)))))).

(h-reduce-r :a :b :c 2)  Hp=>  (h* :a (h* :b (h* :c 2)))
(h-reduce-r :a :b :c 2)   H=>  (ht (:a (ht (:b (ht (:c 2))))))
(h-reduce-r :a :b 2)      H=>  (ht (:a (ht (:b 2))))
(h-reduce-r :a 2)         H=>  (ht (:a 2))
(h-reduce-r :a)           !!>   wrong-number-of-arguments
(h-reduce-r)              H=>  (ht)
;; Both nominal and actual size are 1
(-> (h-reduce-r :a :b :c 2) (h-prop 'size)) => 1
(-> (h-reduce-r :a :b :c 2) ht-size)        => 1
passed as vectors
h--zip-vectors-with (key-form val-form keys values &optional size test)

Anaphoric version of h-zip-vectors-with.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Each element of KEYS is bound to the symbol key.
Each element of VALUES is bound to the symbol value.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

The (roughly) inverse of this function is h--vunzip-with.

(h--zip-vectors-with (format "%s-%s" key value)
                     key
                     [a b]   [1 2])
Hp=> (h* "a-1" 'a "b-2" 'b)

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [a b]   [1 2])
Hp=> (h* "a" "1" "b" "2")

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [a b]   [1])
Hp=> (h* "a" "1" "b" nil)

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [a b c] [1 2])
Hp=> (h* "a" "1" "b" "2" "c" nil)

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [a b]   [nil nil])
Hp=> (h* "a" nil "b" nil)

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [a b]   [])
Hp=> (h* "a" nil "b" nil)

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [] [])
Hp=> (h-new 1)

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     [])
!!> error

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value)))
!!> error

(h--zip-vectors-with (symbol-name key)
                     (when value (number-to-string value))
                     3)
!!> error
h-zip-vectors-with (key-fun val-fun keys values &optional size test)

Create a hash table with KEYS and VALUES modified by functions.

The function KEY-FUN modifies each key.
The function VAL-FUN modifies each value.

Both functions take two variables: key and value, respectively.

Each KEY and VALUE is taken one by one from their corresponding
positions at the vectors KEYS and VALUES.

If KEYS is nil, return empty hash table.
If KEYS is shorter than VALUES, ignore additional elements of VALUES.
If VALUES is shorter than KEYS, complete VALUES with nils.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

The anaphoric version of this function is h--zip-vectors-with.

The (roughly) inverse of this function is h-vunzip-with.

(h-zip-vectors-with (lambda (key value) (format "%s-%s" key value))
                    (lambda (key _value) key)
                    [a b]   [1 2])
Hp=> (h* "a-1" 'a "b-2" 'b)

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [a b]   [1 2])
Hp=> (h* "a" "1" "b" "2")

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [a b]   [1])
Hp=> (h* "a" "1" "b" nil)

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [a b c] [1 2])
Hp=> (h* "a" "1" "b" "2" "c" nil)

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [a b]   [nil nil])
Hp=> (h* "a" nil "b" nil)

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [a b]   [])
Hp=> (h* "a" nil "b" nil)

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [] [])
Hp=> (h-new 1)

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    [])
!!> error

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value))))
!!> error

(h-zip-vectors-with (lambda (key _value) (symbol-name key))
                    (lambda (_key value) (when value (number-to-string value)))
                    3)
!!> error
h-zip-vectors (keys values &optional size test)

Create a hash table with KEYS and VALUES. Both must be vectors.

This is just like h-zip-vectors-with, but without modifying any of the keys
and values before putting them on the table.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

The (roughly) inverse of this function is h-vunzip.

(h-zip-vectors [:a :b]    [1])        Hp=> (h* :a 1 :b nil)
(h-zip-vectors [:a :b]    [1 2])      Hp=> (h* :a 1 :b 2)
(h-zip-vectors [:a :b :c] [1 2])      Hp=> (h* :a 1 :b 2 :c nil)
(h-zip-vectors [:a :b]    [nil nil])  Hp=> (h* :a nil :b nil)
(h-zip-vectors [:a :b]    [])         Hp=> (h* :a nil :b nil)
(h-zip-vectors [] [])                 Hp=> (h-new 1)
(h-zip-vectors [])                     !!> error
(h-zip-vectors)                        !!> error
(h-zip-vectors 3)                      !!> error
passed as lists
h--zip-lists-with (key-form val-form keys values &optional size test)

Anaphoric version of h-zip-lists-with.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Each element of KEYS is bound to the symbol key.
Each element of VALUES is bound to the symbol value.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

The (roughly) inverse of this function is h--lunzip-with.

(h--zip-lists-with (format "%s" key)
                   (when value (format "%s" value))
                   '(a b c d) '(1 2 3)
                   5 'equal)
Hp=> (h-s* 5 "a" "1" "b" "2" "c" "3" "d" nil)

(h--zip-lists-with (format "%s-%s" key value)
                   key
                   '(a b)   '(1 2))
Hp=> (h* "a-1" 'a "b-2" 'b)

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '(a b)   '(1 2))
Hp=> (h* "a" "1" "b" "2")

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '(a b)   '(1))
Hp=> (h* "a" "1" "b" nil)

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '(a b c) '(1 2))
Hp=> (h* "a" "1" "b" "2" "c" nil)

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '(a b)   '(nil nil))
Hp=> (h* "a" nil "b" nil)

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '(a b)   '())
Hp=> (h* "a" nil "b" nil)

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '() '())
Hp=> (h-new 1)

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   '())
!!> error

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value)))
!!> error

(h--zip-lists-with (symbol-name key)
                   (when value (number-to-string value))
                   3)
!!> error
h-zip-lists-with (key-fun val-fun keys values &optional size test)

Create a hash table with KEYS and VALUES modified by functions.

The function KEY-FUN modifies each key.
The function VAL-FUN modifies each value.

Both functions take two variables: key and value, respectively.

Each KEY and VALUE is taken one by one from their corresponding
positions at the lists KEYS and VALUES.

If KEYS is nil, return empty hash table.
If KEYS is shorter than VALUES, ignore additional elements of VALUES.
If VALUES is shorter than KEYS, complete VALUES with nils.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

The anaphoric version of this function is h--zip-lists-with.

The (roughly) inverse of this function is h-lunzip-with.

(h-zip-lists-with (lambda (key value) (format "%s-%s" key value))
                  (lambda (key _value) key)
                  '(a b)   '(1 2))
Hp=> (h* "a-1" 'a "b-2" 'b)

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '(a b)   '(1 2))
Hp=> (h* "a" "1" "b" "2")

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '(a b)   '(1))
Hp=> (h* "a" "1" "b" nil)

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '(a b c) '(1 2))
Hp=> (h* "a" "1" "b" "2" "c" nil)

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '(a b)   '(nil nil))
Hp=> (h* "a" nil "b" nil)

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '(a b)   '())
Hp=> (h* "a" nil "b" nil)

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '() '())
Hp=> (h-new 1)

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  '())
!!> error

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value))))
!!> error

(h-zip-lists-with (lambda (key _value) (symbol-name key))
                  (lambda (_key value) (when value (number-to-string value)))
                  3)
!!> error
h-zip-lists (keys values &optional size test)

Create a hash table with KEYS and VALUES. Both must be lists.

This is just like h-zip-lists-with, but without modifying any of the keys
and values before putting them on the table.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

The (roughly) inverse of this function is h-lunzip.

(h-zip-lists '(:a :b)    '(1))        Hp=> (h* :a 1 :b nil)
(h-zip-lists '(:a :b)    '(1 2))      Hp=> (h* :a 1 :b 2)
(h-zip-lists '(:a :b :c) '(1 2))      Hp=> (h* :a 1 :b 2 :c nil)
(h-zip-lists '(:a :b)    '(nil nil))  Hp=> (h* :a nil :b nil)
(h-zip-lists '(:a :b)    '())         Hp=> (h* :a nil :b nil)
(h-zip-lists '() '())                 Hp=> (h-new 1)
(h-zip-lists '())                      !!> error
(h-zip-lists)                          !!> error
(h-zip-lists 3)                        !!> error
Binding

Functions to bind the values of a hash table to variables.
The main and the helper functions of h-let were inspired by, and
adapted from, let-alist.

Dot-binding (h-let and h-let-it)
h-let (table &rest body)

Let-bind dotted symbols to their value in TABLE and execute BODY.

Dotted symbol is any symbol starting with a dot. Only those present
in BODY are let-bound, and this search is done at compile time.

For instance, the following code:

(h-let table
  (if (and .title .body)
      .body
    .site
    .site.contents))

essentially expands to:

(let ((.title (h-get 'title table))
      (.body  (h-get 'body  table))
      (.site  (h-get 'site  table))
      (.site.contents (h-get 'contents (h-get 'site table))))
  (if (and .title .body)
      .body
    .site
    .site.contents))

That is still somewhat simplified, however. In fact, this line:

(h-get 'title table)

looks more like:

(or (h-get "title" table)
    (h-get 'title  table)
    (h-get :title  table))

so that your query should work regardless of whether the key is a
string, a symbol, or a keyword. (Or even number.)

Note that if you mix these types in the keys and two keys happen to
share the same word (such as having both 'title and :title as keys
of the same table), results will be unpredictable. But this case
should be as rare as it's frequent the need for type autodetection
in a given one-typed-key hash table.

If you nest h-let invocations, the inner one can't access the
variables of the outer one. You can, however, access nested hash
tables by using dots inside the symbol, as showed in the example
above.

This function was inspired by, and adapted from, let-alist.

See also: h-let-it.

(let ((my-htbl (h* 'id   1234
                   'payload (h* 'url     "https://example.com"
                                'title   "Example Domain"
                                'content "This domain is...")))
      (my-alist  '((id . 1234)
                   (payload (url      .  "https://example.com")
                            (title    .  "Example Domain")
                            (content  .  "This domain is...")))))
  ;; These three plists are the same:
  (h-plist= (h-let     my-htbl  (list :id .id :url .payload.url))
            (let-alist my-alist (list :id .id :url .payload.url))
            '(:id 1234 :url "https://example.com")))
=> t   ; ^Adapted from a =let-alist= example from elisp-demos.

(let ((tbl (h* :a 1 :foo "bar")))
  (h-let tbl
    (format "Great %s!" .foo)))
=> "Great bar!"

(h-let (h* 'user   "bob"
           'editor (h* 'home "Vim"
                       'work "Emacs"))
  (format "%s uses %s at home but %s at work."
          (capitalize .user) .editor.home .editor.work))
=> "Bob uses Vim at home but Emacs at work."

;;;; Works with keys in different formats
(h-let (h* "a" 1
           "b" 2
           "c" (h* "d" 4
                   "e" 5))
  .c.e)
=> 5

(h-let (h* 'a 1
           'b 2
           'c (h* 'd 4
                  'e 5))
  .c.e)
=> 5

(h-let (h* :a 1
           :b 2
           :c (h* :d 4
                  :e 5))
  .c.e)
=> 5

(h-let (h* "b" (h* "id"   "b"
                   "name" "bob"
                   "age"  "30")
           "e" (h* "id"   "e"
                   "name" "emily"
                   "age"  "21"))
  .b.name)
=> "bob"

(h-let (h* 'b (h* 'id   "b"
                  'name "bob"
                  'age  "30")
           'e (h* 'id   "e"
                  'name "emily"
                  'age  "21"))
  .b.name)
=> "bob"

(h-let (h* :b (h* :id   "b"
                  :name "bob"
                  :age  "30")
           :e (h* :id   "e"
                  :name "emily"
                  :age  "21"))
  .b.name)
=> "bob"

(h-let (h* :b (h* 'id   "b"
                  'name "bob"
                  'age  "30")
           :e (h* 'id   "e"
                  'name "emily"
                  'age  "21"))
  .b.name)
=> "bob"

(h-let (h* 1 (h* 'id   "b"
                 'name "bob"
                 'age  "30")
           2 (h* 'id   "e"
                 'name "emily"
                 'age  "21"))
  (capitalize .1.name))
=> "Bob"

(h-let (h* 1 (h* 'id   "b"
                 'name "bob"
                 'age  30)
           2 (h* 'id   "e"
                 'name "emily"
                 'age  21))
  .2.age)
=> 21

(h-let (h* "01" (h* "id"     "01"
                    "name"   "Alice"
                    "age"    "42"
                    "editor" "Emacs")
           "02" (h* "id"     "02"
                    "name"   "Bob"
                    "age"    "30"
                    "editor" "Vim")
           "03" (h* "id"     "03"
                    "name"   "Emily"
                    "age"    "21"
                    "editor" "Nano"))
  (downcase .01.editor))
=> "emacs"

;;;; Can promptly be used after conversions
(h-let (h<-orgtbl
        "| id | name  | age | editor |
         |----+-------+-----+--------|
         | 01 | Alice |  42 | Emacs  |
         | 02 | Bob   |  30 | Vim    |
         | 03 | Emily |  21 | Nano   |")
  .01.editor)
=> "Emacs"


(-> "| name  | age | editor |
     +-------+-----+--------|
     | Alice |  42 | Emacs  |
     | Bob   |  30 | Vim    |
     | Emily |  21 | Nano   |"
    h<-orgtbl  (h-let .Alice.editor))
=> "Emacs"

(--> "| id   | name  | age | editor |
      |------+-------+-----+--------|
      | id01 | Alice |  42 | Emacs  |
      | id02 | Bob   |  30 | Vim    |
      | id03 | Emily |  21 | Nano   |"
     h<-orgtbl  (h-let it .id01.age)  string-to-number)
=> 42

(--> "| id | name  | age |
      |----+-------+-----|
      | b  | bob   |  30 |
      | e  | emily |  21 |"
     h<-orgtbl  (h-let it .b.name)  capitalize)
=> "Bob"
h-let-it (thing &rest body)

Let-bind dotted symbols to their value in THING and execute BODY.

Behind the scenes, it converts key–value THING to hash table using
h<-it, and then dot-binds the result using h-let.

  ;; NOTE: For lists and lines: better decompose and use the specific
  ;; conversion functions to avoid wrong auto-detection of types.
  ;;
  ;; A list with even number of items will be thought of as a plist, for
  ;; example; and a string with many lines could be parsed as TSV or KVL
  ;; depending on the presence of characters that could be detected as
  ;; separators.

  ;;; 1D: implicit
  ;;;; list

  (let ((thing '(Alice Bob Charlie)))
    (h-let-it thing
      .\1))
  => 'Bob

  ;; the above works only because no other kv thing would match — better
  ;; to explicitly decompose it as follows:

  (let ((thing '(Alice Bob Charlie)))
    (h-let (h<-list thing)
      .\1))
  => 'Bob

  (let ((thing '(Alice Bob Charlie)))
    (h-let (h<-list thing)
      (format "%s and %s" .\0 .\1)))
  => "Alice and Bob"

  (let ((thing '(a b c d e f g h i j k l m n o p q r s t u v w x y z)))
    (h-let (h<-list thing)
      (format "%s and %s"
              (s-titleize (format "%s%s%s%s%s" .\0 .\11 .\8 .\2 .\4))
              (s-titleize (format "%s%s%s"     .\1 .\14 .\1)))))
  => "Alice and Bob"


  ;;;; vector

  (let ((thing [Alice Bob Charlie]))
    (h-let-it thing
      (format "%s and %s" .\0 .\1)))
  => "Alice and Bob"

  (let ((thing "Alice\nBob\nCharlie"))
    (h-let-it thing
      (format "%s and %s" .\0 .\1)))
  => "Alice and Bob"


  ;;;; lines

  (let ((thing "\
Alice
Bob
Charlie"))
    (h-let-it thing
      (format "%s and %s" .\0 .\1)))
  => "Alice and Bob"

  ;; the above works only because no other kv thing would match — better
  ;; to explicitly decompose as below:
  (let ((thing "\
Alice
Bob
Charlie"))
    (h-let (h<-lines thing)
      (format "%s and %s" .\0 .\1)))
  => "Alice and Bob"


  ;;; 1D: explicit
  ;;;; key–value lines (kvl)

  (let ((thing "Alice=42\nBob=21"))
    (h-let-it thing
      .Alice))
  => "42"

  (let ((thing "Alice = 42\nBob    = 21"))
    (h-let-it thing
      .Alice))
  => "42"

  (let ((h-kvl-sep-re ":")
        (thing "Alice:42\nBob:21"))
    (h-let-it thing
      (h-as-number .Alice)))
  => 42

  (let ((h-kvl-sep-re " *: *")
        (thing "\
Alice : 42
Bob   : 21"))
    (h-let-it thing
      (h-as-number .Alice)))
  => 42

  ;;;; cons cell

  (let ((thing '(Alice . 42)))
    (h-let-it thing
      .Alice))
  => 42


  ;;; ≥1D unspecific
  ;;;; alist

  (let ((thing '((a . Alice)
                 (b . ((ba . Barbara)
                       (bo . Bob))))))
    (h-let-it thing
      .b.bo))
  => 'Bob

  (let ((thing '((:a . Alice)
                 (:b . ((:ba . Barbara)
                        (:bo . Bob))))))
    (h-let-it thing
      .b.bo))
  => 'Bob

  (let ((thing '(("a" . "Alice")
                 ("b" . (("ba" . "Barbara")
                         ("bo" . "Bob"))))))
    (h-let-it thing
      .b.bo))
  => "Bob"


  ;;;; plist

  (let ((thing (list 'a 'Alice
                     'b (list 'ba 'Barbara
                              'bo 'Bob))))
    (h-let-it thing
      .b.bo))
  => 'Bob

  (let ((thing (list :a 'Alice
                     :b (list :ba 'Barbara
                              :bo 'Bob))))
    (h-let-it thing
      .b.bo))
  => 'Bob

  (let ((thing (list "a" "Alice"
                     "b" (list "ba" "Barbara"
                               "bo" "Bob"))))
    (h-let-it thing
      (h-as-keyword .b.bo)))
  => :Bob


  ;;;; json

  (let ((thing "{  \"a\": \"Alice\",
                   \"b\": {  \"ba\": \"Barbara\",
                             \"bo\": \"Bob\"  } }"))
    (h-let-it thing
      .b.bo))
  => "Bob"


  ;;;; hash table

  (let ((thing (h* 'a 'Alice
                   'b (h* 'ba 'Barbara
                          'bo 'Bob))))
    (h-let-it thing
      .b.bo))
  => 'Bob

  ;; the above is fine — but [[#h-let][h-let]], which expects a hash table, is shorter
  ;; and a bit cheaper:
  (let ((thing (h* 'a 'Alice
                   'b (h* 'ba 'Barbara
                          'bo 'Bob))))
    (h-let thing
      .b.bo))
  => 'Bob

  ;; any hash table format should work — here, in (ht…):
  (let ((thing (ht ('a 'Alice)
                   ('b (ht ('ba 'Barbara)
                           ('bo 'Bob))))))
    (h-let thing
      .b.bo))
  => 'Bob

  ;; and here in that ungainly native format (trimmed of some params):
  (let ((thing #s(hash-table
                  size 2 test equal data
                  (a Alice b #s(hash-table
                                size 2 test equal data
                                (ba Barbara bo Bob))))))
    (h-let thing
      .b.bo))
  => 'Bob


  ;;; 2D
  ;;;; list of lists (lol)

  (let ((thing '((name  age  editor)
                 (Alice  42   Emacs)
                 (Bob    30     Vim)
                 (Emily  21    Nano))))
    (h-let-it thing
      .Alice.age))
  => 42

  (let ((thing '((name  age  editor)
                 (Alice  42   Emacs)
                 (Bob    30     Vim)
                 (Emily  21    Nano))))
    (h-let-it thing
      (format "%s is %s and prefers %s."
              "Alice" .Alice.age .Alice.editor)))
  => "Alice is 42 and prefers Emacs."


  ;;;; org table

  (let ((thing "| name  | age | editor |
                +-------+-----+--------|
                | Alice |  42 | Emacs  |
                | Bob   |  30 | Vim    |
                | Emily |  21 | Nano   |"))
    (h-let-it thing
      (h-as-number .Alice.age)))
  => 42


  ;;;; tsv

  (let ((thing "name\tage\nAlice\t42\nBob\t21"))
    (h-let-it thing
      .Alice.age))
  => "42"

  (let ((thing "\
name\tage
Alice\t42
Bob\t21"))
    (h-let-it thing
      .Alice.age))
  => "42"

  (let ((thing "\
name    age
Alice   42
Bob 21"))
    (h-let-it thing
      .Alice.age))
  => "42"

  (--> "name\tage\nAlice\t42\nBob\t21"
       (h-let-it it  ;<--- we're literally h-letting-it!
         .Alice.age))
  => "42"


  ;;;; csv

  (let ((thing "name,age\nAlice,42\nBob,21"))
    (h-let-it thing
      .Alice.age))
  => "42"

  (let ((thing "\
name,age
Alice,42
Bob,21"))
    (h-let-it thing
      .Alice.age))
  => "42"


  ;;;; ssv

  (let ((thing "\
name   age  editor
Alice  42   Emacs
Bob    21   Vim
Emily  30   Nano"))
    (h-let-it thing
      (format "%s is %s and prefers %s."
              "Alice" .Alice.age .Alice.editor)))
  => "Alice is 42 and prefers Emacs."
Canonical representations
Changing canonical representation — from table

Functions returning a canonical form that would've generated a hash table.

Note that hash tables will fail equality tests such as:

(eq     #s(hash-table test equal data ('a 1))
        #s(hash-table test equal data ('a 1))) ;=> nil

or:

(eql    #s(hash-table test equal data ('a 1))
        #s(hash-table test equal data ('a 1))) ;=> nil

or:

(equal  #s(hash-table test equal data ('a 1))
        #s(hash-table test equal data ('a 1))) ;=> nil

or:

(equal  (ht ('a 1))  (ht ('a 1))) => nil

But, of course:

(equal '(ht ('a 1)) '(ht ('a 1))) => t
(equal '(h*  'a 1)  '(h*  'a 1))  => t

So the comparisons below work.

Note: h-htbl-form, not shown here, is just the table itself in
regular form: #s(hash-table…)

#s(hash-table…)
h-htbl-form (table)

Hash TABLE object as itself.

Function defined only so we have a parallel with ht and h* forms.

(h-htbl-form (ht ("a" 1)))
H=> #s(hash-table size 65 test equal rehash-size 1.5
                  rehash-threshold 0.8 data ("a" 1))

(h-htbl-form (ht ("a" 1) ("b" 2)))
H=>  #s(hash-table size 65 test equal rehash-size 1.5
                   rehash-threshold 0.8 data ("a" 1 "b" 2))

(h-htbl-form #s(hash-table test equal data ("a" 1)))
H=>  #s(hash-table size 65 test equal rehash-size 1.5
                   rehash-threshold 0.8 data ("a" 1))

(h-htbl-form #s(hash-table test equal data ("a" 1 "b" 2)))
H=>  #s(hash-table size 65 test equal rehash-size 1.5
                   rehash-threshold 0.8 data ("a" 1 "b" 2))

(h-htbl-form (ht ("a" 1) ("b" (ht ("c" 3)))))
H=>  #s(hash-table size 65 test equal rehash-size 1.5
                   rehash-threshold 0.8 data
                   ("a" 1
                    "b" #s(hash-table size 65
                                      test equal rehash-size 1.5
                                      rehash-threshold 0.8 data ("c" 3))))

(h-htbl-form (ht-create))   ; ht doesn't specify size/test
H=> #s(hash-table size 65 test equal rehash-size 1.5
                  rehash-threshold 0.8 data ())

(h-htbl-form (h-new 3))
H=> #s(hash-table size 3 test equal rehash-size 1.5
                  rehash-threshold 0.8 data ())
h-htbl-cm-str (table)

Hash TABLE object as a string in compact representation.

 (h-htbl-cm-str (ht ("a" 1)))
 => "#s(hash-table size 65 test equal rehash-size 1.5\
rehash-threshold 0.8 data (\"a\" 1))"

 (h-htbl-cm-str (ht ("a" 1) ("b" 2)))
 =>  "#s(hash-table size 65 test equal rehash-size 1.5\
rehash-threshold 0.8 data (\"a\" 1 \"b\" 2))"

 (h-htbl-cm-str #s(hash-table test equal data ("a" 1)))
 =>  "#s(hash-table size 65 test equal rehash-size 1.5\
rehash-threshold 0.8 data (\"a\" 1))"

 (h-htbl-cm-str #s(hash-table test equal data ("a" 1 "b" 2)))
 =>  "#s(hash-table size 65 test equal rehash-size 1.5\
rehash-threshold 0.8 data (\"a\" 1 \"b\" 2))"

 (h-htbl-cm-str (ht ("a" 1) ("b" (ht ("c" 3)))))
 =>  "#s(hash-table size 65 test equal rehash-size 1.5\
rehash-threshold 0.8 data (\"a\" 1 \"b\" #s(hash-table size 65\
test equal rehash-size 1.5 rehash-threshold 0.8 data (\"c\" 3))))"

 (h-htbl-cm-str (ht-create))  ; ht doesn't specify size/test
 => "#s(hash-table size 65 test equal rehash-size 1.5\
rehash-threshold 0.8 data ())"

 (h-htbl-cm-str (h-new 3))
 => "#s(hash-table size 3 test equal rehash-size 1.5\
rehash-threshold 0.8 data ())"
h-htbl-pp-str (table)

Hash TABLE object as a string in pretty-printed representation.

;; The let is only necessary for testing. Used to make sure the string is
;; EXACTLY the same when pretty-printing, since different Emacsen may have
;; different configs for indentation:
(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-htbl-pp-str (ht ("a" 1) ("b" 2))))
=> "#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              (\"a\" 1 \"b\" 2))\n"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-htbl-pp-str #s(hash-table test equal data ("a" 1 "b" 2))))
=> "#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              (\"a\" 1 \"b\" 2))\n"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-htbl-pp-str (ht ("a" 1) ("b" (ht ("c" 3) ("d" 4))))))
=>
"#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              (\"a\" 1 \"b\" #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data\n                                       (\"c\" 3 \"d\" 4))))\n"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-htbl-pp-str (ht-create))) ; ht doesn't specify size/test
=> "#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              ())\n"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-htbl-pp-str (h-new 3 'eq)))
=> "#s(hash-table size 3 test eq rehash-size 1.5 rehash-threshold 0.8 data\n              ())\n"
(ht…)
h-ht-form (table)

Restore canonical (ht…) form that creates hash TABLE.

Note that this doesn't preserve TABLE's equality test: (ht…)
defaults to 'equal as test, so if TABLE's test is anything else, it
will become 'equal if the form is evaluated.

(h-ht-form (ht ("a" 1)))           => '(ht ("a" 1))
(h-ht-form (ht ("a" 1) ("b" 2)))   => '(ht ("a" 1) ("b" 2))
(h-ht-form #s(hash-table test equal data ("a" 1))) => '(ht ("a" 1))
(h-ht-form #s(hash-table test equal data ("a" 1 "b" 2)))
=> '(ht ("a" 1) ("b" 2))
(h-ht-form (ht ("a" 1) ("b" (ht ("c" 3)))))
=> '(ht ("a" 1) ("b" (ht ("c" 3))))
(h-ht-form (ht-create))   => '(ht)  ; ht doesn't specify size/test
(h-ht-form (h-new 3 'eq)) => '(ht)
h-ht-cm-str (table)

Restore as string canonical (ht…) form that creates hash TABLE.

Compact representation.

(h-ht-cm-str (ht ("a" 1)))           => "(ht (\"a\" 1))"
(h-ht-cm-str (ht ("a" 1) ("b" 2)))   => "(ht (\"a\" 1) (\"b\" 2))"
(h-ht-cm-str #s(hash-table test equal data ("a" 1))) => "(ht (\"a\" 1))"
(h-ht-cm-str #s(hash-table test equal data ("a" 1 "b" 2)))
=> "(ht (\"a\" 1) (\"b\" 2))"
(h-ht-cm-str (ht ("a" 1) ("b" (ht ("c" 3)))))
=> "(ht (\"a\" 1) (\"b\" (ht (\"c\" 3))))"
(h-ht-cm-str (ht-create))   => "(ht)"   ; ht doesn't specify size/test
(h-ht-cm-str (h-new 3 'eq)) => "(ht)"
h-ht-pp-str (table)

The pretty-print string of (ht…) form that creates hash TABLE.

(h-ht-pp-str (ht ("a" 1)))           => "(ht (\"a\" 1))"
;; The let is only necessary for testing. Used to make sure the string is
;; EXACTLY the same when pretty-printing, since different Emacsen may have
;; different configs for indentation:
(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-ht-pp-str (ht ("a" 1) ("b" 2))))
=> "(ht (\"a\" 1)\n    (\"b\" 2))"

(h-ht-pp-str #s(hash-table test equal data ("a" 1))) => "(ht (\"a\" 1))"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-ht-pp-str #s(hash-table test equal data ("a" 1 "b" 2))))
=> "(ht (\"a\" 1)\n    (\"b\" 2))"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-ht-pp-str (ht ("a" 1) ("b" (ht ("c" 3) ("d" 4))))))
=> "(ht (\"a\" 1)\n    (\"b\" (ht (\"c\" 3)\n             (\"d\" 4))))"
(h-ht-pp-str (ht-create))   => "(ht)"  ; ht doesn't specify size/test
(h-ht-pp-str (h-new 3 'eq)) => "(ht)"
(h*…) and (h-st*…)
h-h*-form (table)

Restore canonical (h*…) or (h-st*…) form that creates hash TABLE.

If TABLE's test is:

  • 'equal, we use (h*…), which preserves it. Note that if this form
    is evaluated the nominal size of that table will be determined by
    h*. (This is unlikely to be an issue.)
  • not 'equal, we recover its nominal size and equality test and
    use (h-st*…).
(h-h*-form (h* "a" 1))                             => '(h* "a" 1)
(h-h*-form (ht  ("a" 1)))                          => '(h* "a" 1)
(h-h*-form #s(hash-table test equal data ("a" 1))) => '(h* "a" 1)
(h-h*-form (h* "a" 1   "b" 2))    => '(h* "a" 1 "b" 2)
(h-h*-form (ht  ("a" 1) ("b" 2))) => '(h* "a" 1 "b" 2)
(h-h*-form #s(hash-table test equal data ("a" 1 "b" 2)))
=> '(h* "a" 1 "b" 2)
(h-h*-form (ht ("a" 1) ("b" (ht ("c" 3)))))
=> '(h* "a" 1 "b" (h* "c" 3))
(h-h*-form (ht-create))      => '(h*)
(h-h*-form (h-new nil 'eq))  => '(h-st* 65 'eq)
(h-h*-form (h-new 3   'eq))  => '(h-st*  3 'eq)
(h-h*-form (h-t* 'eq :a 1))  => '(h-st*  1 'eq :a 1)
;; when only size is different, this is ignored and
;; the simpler form is used:
(h-h*-form (h-new 3 'equal)) => '(h*)
(h-h*-form (h-new 3))        => '(h*)
(h-h*-form (h-s* 1 "a" 1))   => '(h* "a" 1)
h-h*-cm-str (table)

Restore as string (h*…) or (h-st*…) form that creates hash TABLE.

Compact representation.

(h-h*-cm-str (h* "a" 1))                             => "(h* \"a\" 1)"
(h-h*-cm-str (ht  ("a" 1)))                          => "(h* \"a\" 1)"
(h-h*-cm-str #s(hash-table test equal data ("a" 1))) => "(h* \"a\" 1)"
(h-h*-cm-str (h* "a" 1   "b" 2))    => "(h* \"a\" 1 \"b\" 2)"
(h-h*-cm-str (ht  ("a" 1) ("b" 2))) => "(h* \"a\" 1 \"b\" 2)"
(h-h*-cm-str #s(hash-table test equal data ("a" 1 "b" 2)))
=> '"(h* \"a\" 1 \"b\" 2)"
(h-h*-cm-str (ht ("a" 1) ("b" (ht ("c" 3) ("d" 4)))))
=> "(h* \"a\" 1 \"b\" (h* \"c\" 3 \"d\" 4))"
(h-h*-cm-str (ht-create))      => "(h*)"
(h-h*-cm-str (h-new nil 'eq))  => "(h-st* 65 (quote eq))"
(h-h*-cm-str (h-new 3   'eq))  => "(h-st* 3 (quote eq))"
(h-h*-cm-str (h-t* 'eq :a 1))  => "(h-st* 1 (quote eq) :a 1)"
;; when only size is different, this is ignored and
;; the simpler form is used:
(h-h*-cm-str (h-new 3 'equal)) => "(h*)"
(h-h*-cm-str (h-new 3))        => "(h*)"
(h-h*-cm-str (h-s* 1 "a" 1))   => "(h* \"a\" 1)"
h-h*-pp-str (table)

The pretty-print string of (h*…) form that creates hash TABLE.

If table's test isn't 'equal, use (h-st*…) form.

(h-h*-pp-str (h* "a" 1))                             => "(h* \"a\" 1)"
(h-h*-pp-str (ht  ("a" 1)))                          => "(h* \"a\" 1)"
(h-h*-pp-str #s(hash-table test equal data ("a" 1))) => "(h* \"a\" 1)"
;; The let is only necessary for testing. Used to make sure the string is
;; EXACTLY the same when pretty-printing, since different Emacsen may have
;; different configs for indentation:
(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-h*-pp-str (h* "a" 1   "b" 2)))
=> "(h* \"a\" 1\n    \"b\" 2)"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-h*-pp-str (ht  ("a" 1) ("b" 2))))
=> "(h* \"a\" 1\n    \"b\" 2)"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-h*-pp-str #s(hash-table test equal data ("a" 1 "b" 2))))
=> '"(h* \"a\" 1\n    \"b\" 2)"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-h*-pp-str (ht ("a" 1) ("b" (ht ("c" 3) ("d" 4))))))
=> "(h* \"a\" 1\n    \"b\" (h* \"c\" 3\n            \"d\" 4))"
(h-h*-pp-str (ht-create))      => "(h*)"
(h-h*-pp-str (h-new nil 'eq))  => "(h-st* 65 'eq)"
(h-h*-pp-str (h-new 3   'eq))  => "(h-st* 3 'eq)"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line))
  (h-h*-pp-str (h-t* 'eq :a 1)))
=> "(h-st* 1 'eq\n       :a 1)"
;; when only size is different, this is ignored and
;; the simpler form is used:
(h-h*-pp-str (h-new 3 'equal)) => "(h*)"
(h-h*-pp-str (h-new 3))        => "(h*)"
(h-h*-pp-str (h-s* 1 "a" 1))   => "(h* \"a\" 1)"
Changing canonical representation — from any ht-like form

Choose different representations of hash tables.

h-lambdify (ht-like)

Convert an HT-LIKE object into a lambda that creates the table.

HT-LIKE may come in any of the following autodetected formats:

As Lisp objects:

  1. #s(hash-table…) = regular representation
  2. (ht…) form
  3. (h*…), (h-s*…), (h-t*…), or (h-st*…) form

or any of the above formatted as string (presumably with %S).

(h-lambdify '(h*))                           => `(lambda () (h*))
(h-lambdify "(h* :a 1 :b 2)")                => `(lambda () (h* :a 1 :b 2))
(-> "(h* :a 1 :b 2)"    h-lambdify funcall) H=>  (h* :a 1 :b 2)
(-> '(h* :a 1 :b 2)     h-lambdify funcall) H=>  (h* :a 1 :b 2)
(-> '(ht (:a 1) (:b 2)) h-lambdify funcall) H=>  (h* :a 1 :b 2)
h-format (format ht-like)

Format as FORMAT hash table–like object HT-LIKE.

HT-LIKE may come in any of the following autodetected formats:

As Lisp objects:

  1. #s(hash-table…) = regular representation
  2. (ht…) form
  3. (h*…), (h-s*…), (h-t*…), or (h-st*…) form

or as strings:
Any of the above formatted as string (presumably with %S)

The output FORMAT, in turn, must be specified as one of:

  1. htbl :: #s(hash-table…) (Lisp object)
  2. h*-form :: (h*…) form (Lisp object)
  3. ht-form :: (ht…) form (Lisp object)
  4. htbl-cm-str :: #s(hash-table…) string in compact format
  5. htbl-pp-str :: #s(hash-table…) string in pretty-printed format
  6. h*-cm-str :: (h*…) string in compact format
  7. h*-pp-str :: (h*…) string in pretty-printed format
  8. ht-cm-str :: (ht…) string in compact format
  9. ht-pp-str :: (ht…) string in pretty-printed format

Any of these FORMAT options may be passed as either a string, a
keyword, or a symbol — as you prefer. So, for example, any among
"htbl", :htbl, or 'htbl works.

 (h-format :htbl      (h* :a 1 :b (h* :c 3 :d 4)))
 H=> #s(hash-table
        size 2 test equal
        rehash-size 1.5 rehash-threshold 0.8
        data (:a 1 :b #s(hash-table
                         size 2 test equal
                         rehash-size 1.5 rehash-threshold 0.8
                         data (:c 3 :d 4))))

 (h-format :htbl      (h* :a 1 :b (h* :c 3 :d 4)))
 H=> (h* :a 1 :b (h* :c 3 :d 4))

 (h-format :h*-form   (h* :a 1 :b (h* :c 3 :d 4)))
 => '(h* :a 1 :b (h* :c 3 :d 4))

 (h-format 'h*-form   (h* :a 1 :b (h* :c 3 :d 4)))
 => '(h* :a 1 :b (h* :c 3 :d 4))

 (h-format "h*-form"  (h* :a 1 :b (h* :c 3 :d 4)))
 => '(h* :a 1 :b (h* :c 3 :d 4))

 (h-format :ht-form   (h* :a 1 :b (h* :c 3 :d 4)))
 => '(ht (:a 1) (:b (ht (:c 3) (:d 4))))

 (h-format :htbl-cm-str  (h* :a 1 :b (h* :c 3 :d 4)))
 => "#s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data\
(:a 1 :b #s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data (:c 3 :d 4))))"

 (h-format :htbl-pp-str  (h* :a 1 :b (h* :c 3 :d 4)))
 => "#s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data\n\
             (:a 1 :b #s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data\n\
                                    (:c 3 :d 4))))\n"

 (h-format :h*-cm-str (h* :a 1 :b (h* :c 3 :d 4)))
 =>  "(h* :a 1 :b (h* :c 3 :d 4))"

 (h-format :ht-cm-str (h* :a 1 :b (h* :c 3 :d 4)))
 =>  "(ht (:a 1) (:b (ht (:c 3) (:d 4))))"

 ;; The let is only necessary for testing. Used to make sure the string is
 ;; EXACTLY the same when pretty-printing, since different Emacsen may have
 ;; different configs for indentation:
 (let* (fill-prefix
        indent-region-function
        (indent-line-function #'lisp-indent-line))
   (h-format :h*-pp-str (h* :a 1 :b (h* :c 3 :d 4))))
 =>  "(h* :a 1\n    :b (h* :c 3\n           :d 4))"

 (let* (fill-prefix
        indent-region-function
        (indent-line-function #'lisp-indent-line))
   (h-format :ht-pp-str (h* :a 1 :b (h* :c 3 :d 4))))
 =>  "(ht (:a 1)\n    (:b (ht (:c 3)\n            (:d 4))))"

 (h-format :h*-form (h-t* 'eq :a "a" :b "b"))
 => '(h-st* 2 'eq :a "a" :b "b")

 (h-format :h*-form  '((I . am) (an . alist!)))
 !!> error
Mapping from hash table
to vector

Functions that apply a function to every key–value pair of a
hash table and write the results to a vector.

In particular, here's a guideline for the 'vmap family' of
functions:

  • the added 'v' means 'result is a vector'
  • an added '-' means 'anaphoric: enter a form, not a lambda'

In the anaphoric version, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a vector

  a lambda a form (anaphoric)
vector may have nils h-vmap h--vmap
h-vmap (fun table &optional reverse)

Apply FUN to each key–value pair of TABLE and return vector.

Function FUN is called with two arguments, key and value.

If REVERSE is non-nil, show it in reverse order.

If you don't use both of these variables, then to avoid warnings
use an underscore before the to-be-ignored variable. For example,
to return a vector of all (expected to be integer) values plus 1:

(h-vmap (lambda (_key value) (1+ value)) table)

Its anaphoric counterpart is h--vmap.

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-vmap fun h31))
=> ["Bob: cat!" "Whiskers: cat!" "Bubbly: fish!"]

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-vmap fun h31 'rev))
=> ["Bubbly: fish!" "Whiskers: cat!" "Bob: cat!"]

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (_key value) (when (numberp value)
                              (expt (+ 10 value) 2)))))
  (h-vmap fun h41))
=> [121 144 nil]

(let ((h42  (h* "a" 1 'd  (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (key value)
              (format "%s→%s" (type-of key) (type-of value)))))
  (h-vmap fun h42))
=> ["string→integer" "symbol→hash-table"]

(h-vmap (lambda (key value)
          (concat (upcase key) "-" (int-to-string value)))
        (h* "a" 1 "b" 2 "c" 3))
=> ["A-1" "B-2" "C-3"]

(let ((h11  (h-new 5))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-vmap fun h11))
=> []
h--vmap (form table &optional reverse)

Anaphoric version of h-vmap.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound. Results given as vector.

If REVERSE is non-nil, show it in reverse order.

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--vmap (format "%s: %s!" key value) h31))
=> ["Bob: cat!" "Whiskers: cat!" "Bubbly: fish!"]

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--vmap (format "%s: %s!" key value) h31 'rev))
=> ["Bubbly: fish!" "Whiskers: cat!" "Bob: cat!"]

(let ((h41 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--vmap (when (numberp value) (expt (+ 10 value) 2)) h41))
=> [121 144 nil]

(let ((h42 (h* "a" 1 'd  (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--vmap (format "%s→%s" (type-of key) (type-of value)) h42))
=> ["string→integer" "symbol→hash-table"]

(h--vmap (concat (upcase key) "-" (int-to-string value))
         (h* "a" 1 "b" 2 "c" 3))
=> ["A-1" "B-2" "C-3"]

(let ((h11 (h-new 5)))
  (h--vmap (format "%s: %s!" key value) h11))
=> []
h--vunzip-with (key-form val-form table &optional reverse)

Anaphoric version of h-vunzip-with.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Both forms take two variables: key and value, respectively.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

If REVERSE is non-nil, reverse the order of both vectors.

The (roughly) inverse of this function is h-zip-vectors.

See also: h--lunzip-with.

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a 1 'b nil))
=> '(["a" "b"]    ["1" nil])

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a 1 'b 2))
=> '(["a" "b"]    ["1" "2"])

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a 1 'b 2 'c nil))
=> '(["a" "b" "c"] ["1" "2" nil])

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a nil 'b nil))
=> '(["a" "b"]    [nil nil])

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a nil 'b nil))
=> '(["a" "b"]    [nil nil])

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h-new 1))
=> '([] [])

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                )
!!> wrong-number-of-arguments

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                '())
!!> error

(h--vunzip-with (symbol-name key)
                (when value (number-to-string value))
                3)
!!> error
h-vunzip-with (key-fun val-fun table &optional reverse)

Return a list of modified vectors of KEYS and VALUES from TABLE.

If REVERSE is non-nil, reverse the order of both vectors.

The function KEY-FUN modifies each key in the results.
The function VAL-FUN modifies each value in the results.

Both functions take two variables: key and value, respectively.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

The (roughly) inverse of this function is h-zip-vectors.

Its anaphoric counterpart is h--vunzip-with.

See also: h-lunzip-with.

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a 1 'b nil))
=> '(["a" "b"]    ["1" nil])

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a 1 'b 2))
=> '(["a" "b"]    ["1" "2"])

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a 1 'b 2 'c nil))
=> '(["a" "b" "c"] ["1" "2" nil])

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a nil 'b nil))
=> '(["a" "b"]    [nil nil])

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a nil 'b nil))
=> '(["a" "b"]    [nil nil])

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h-new 1))
=> '([] [])

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               )
!!> wrong-number-of-arguments

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               '())
!!> error

(h-vunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               3)
!!> error
h-vunzip (table &optional reverse)

Return a list of the vectors of KEYS and VALUES from TABLE.

If REVERSE is non-nil, reverse the order of both vectors.

The (roughly) inverse of this function is h-zip-vectors.

See also: h-lunzip.

(h-vunzip (h* :a 1 :b nil))        => '([:a :b]    [1 nil])
(h-vunzip (h* :a 1 :b 2))          => '([:a :b]    [1 2])
(h-vunzip (h* :a 1 :b 2 :c nil))   => '([:a :b :c] [1 2 nil])
(h-vunzip (h* :a nil :b nil))      => '([:a :b]    [nil nil])
(h-vunzip (h* :a nil :b nil))      => '([:a :b]    [nil nil])
(h-vunzip (h-new 1))               => '([]         [])
(h-vunzip)                        !!> wrong-number-of-arguments
(h-vunzip '())                    !!> error
(h-vunzip 3)                      !!> error
h-vitems (table &optional reverse)

Return a vector of two-element lists '(key value) from TABLE.

If REVERSE is non-nil, show it in reverse order (the vector;
the pair remains in the (key, value) order).

(h-vitems (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
=> [("Bob"      "cat")
    ("Whiskers" "cat")
    ("Bubbly"  "fish")]

(h-vitems (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")
          'reverse)
=> [("Bubbly"  "fish")
    ("Whiskers" "cat")
    ("Bob"      "cat")]

(h-vitems (h-new 5))
=> []
h-vkeys (table &optional reverse)

Return a vector of all the keys in TABLE.

If REVERSE is non-nil, show it in reverse order.

(h-vkeys (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
=> ["Bob" "Whiskers" "Bubbly"]

(h-vkeys (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")
         'reverse)
=> ["Bubbly" "Whiskers" "Bob"]

(h-vkeys (h-new 5))
=> []

;; This function isn't recursive:
(h-vkeys (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
=> ["a" "b" "d"]

(h-vkeys (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))
         t)
=> ["d" "b" "a"]
h-vvalues (table &optional reverse)

Return a vector of all the values in TABLE.

If REVERSE is non-nil, show it in reverse order.

(h-vvalues (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
=> ["cat" "cat" "fish"]

(h-vvalues (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")
           'rev)
=> ["fish" "cat" "cat"]

(h-vvalues (h-new 5))
=> []
h-vrandom (table &optional n)

Return a vector of N pseudo-randomly chosen items from hash TABLE.

N must be an integer; if not, signal error.

  • If N is nil, make it N=1.
  • If N≥ size(TABLE), return a vector of all TABLE's items (same
    result as running h-vitems).
  • If N=0, return nil.
  • If N=1, item is returned as a two-item list (key, value).
  • If N>1, items are returned as a vector of two-item lists (key, value).
  • If N is negative, make N = N + size(TABLE). For example, if TABLE
    has 10 items and N=-2, return 8 pseudo-random items.

See also: h-lrandom.

;;;; It's random, so all we can do is check length and whether subset
(let* ((htbl  (h* :a 1 :b 2 :c 3 :d 4))
       (items (h-items htbl))
       (res   (h-vrandom htbl 2))
       (res-l (append res nil)))
  (and (= 2 (length res))
       (equal res-l (-intersection res-l items))))
=> t

;;;; One by one is repetitive. Let's check many at once?
(let* ((htbl  (h* :a 1 :b 2 :c 3 :d 4))
       (items (h-items htbl))
       (ns    '(1 2 3 4 5 -1 -2))
       (sizes '(1 2 3 4 4  3  2)))
  (--every (and (= (nth it-index sizes) (length it))
                (equal (append it nil)
                       (-intersection (append it nil) items)))
           (-map (-cut h-vrandom htbl <>) ns)))
=> t

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4))
       (res  (h-vrandom htbl))
       (res-l (append res nil)))
  (and (= 1 (length res))
       (equal res-l
              (-intersection res-l (h-items htbl)))))
=> t

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4)))
  (null (h-vrandom htbl 0)))
=> t

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4)))
  (null (h-vrandom htbl "something")))
!!> error

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4)))
  (null (h-vrandom htbl 3.5)))
!!> error
to list

Functions that apply a function to every key–value pair of a
hash table and write the results to a list.

In particular, here's a guideline for the 'lmap/lkeep family' of
functions:

  • the added 'l' means 'result is a list'
  • an added '-' means 'anaphoric: enter a form, not a lambda'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a list

  a lambda a form (anaphoric)
list may have nils h-lmap h--lmap
any nils are removed h-lkeep h--lkeep
h-lmap (fun table &optional reverse)

Apply FUN to each key–value pair of TABLE; list the results.

Function FUN is called with two arguments, key and value.

Identical to current ht-map, with the difference that the
returned list matches the current order of the TABLE's keys.

If REVERSE is non-nil, show it in reverse order.

If you don't use both of these variables, then to avoid warnings
use an underscore before the to-be-ignored variable. For example,
to return a list of all (expected to be integer) values plus 1:

(h-lmap (lambda (_key value) (1+ value)) table).

Its anaphoric counterpart is h--lmap.

See also: h-vmap.

(h-lmap #'concat (ht ("Bob"      "cat")
                     ("Whiskers" "cat")
                     ("Bubbly"   "fish")))
=> '("Bobcat" "Whiskerscat" "Bubblyfish")

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-lmap fun h31))
=> '("Bob: cat!" "Whiskers: cat!" "Bubbly: fish!")

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-lmap fun h31 'rev))
=> '("Bubbly: fish!" "Whiskers: cat!" "Bob: cat!")

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (_key value) (when (numberp value)
                              (expt (+ 10 value) 2)))))
  (h-lmap fun h41))
=> '(121 144 nil)

(let ((h42  (h* "a" 1 'd  (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (key value)
              (format "%s→%s" (type-of key) (type-of value)))))
  (h-lmap fun h42))
=> '("string→integer" "symbol→hash-table")

(h-lmap (lambda (key value)
          (concat (upcase key) "-" (int-to-string value)))
        (h* "a" 1 "b" 2 "c" 3))
=> '("A-1" "B-2" "C-3")

(let ((h11  (h-new 5))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-lmap fun h11))
=> '()
h--lmap (form table &optional reverse)

Anaphoric version of h-lmap.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound. List the results.

If REVERSE is non-nil, show it in reverse order.

See also: h--vmap.

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--lmap (format "%s: %s!" key value) h31))
=> '("Bob: cat!" "Whiskers: cat!" "Bubbly: fish!")

(let ((h41 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--lmap (when (numberp value) (expt (+ 10 value) 2))
           h41))
=> '(121 144 nil)

(let ((h41 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--lmap (when (numberp value) (expt (+ 10 value) 2))
           h41 'rev))
=> '(nil 144 121)

(let ((h42 (h* "a" 1 'd  (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--lmap (format "%s→%s" (type-of key) (type-of value)) h42))
=> '("string→integer" "symbol→hash-table")

(h--lmap (concat (upcase key) "-" (int-to-string value))
         (h* "a" 1 "b" 2 "c" 3))
=> '("A-1" "B-2" "C-3")

(let ((h11 (h-new 5)))
  (h--lmap (format "%s: %s!" key value) h11))
=> '()
h-lkeep (fun table &optional reverse)

Apply FUN to each key–value pair of TABLE; list non-nil results.

We follow Dash's naming scheme here, for which 'keep' is just like
'map' — but with the nils removed.

Function FUN is called with two arguments, key and value.

If REVERSE is non-nil, show it in reverse order.

As with h-lmap, if you don't use both of these variables, then to
avoid warnings use an underscore before the to-be-ignored variable.

Its anaphoric counterpart is h--lkeep.

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-lkeep fun h31))
=> '("Bob: cat!" "Whiskers: cat!" "Bubbly: fish!")

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (_key value) (when (numberp value)
                              (expt (+ 10 value) 2)))))
  (h-lkeep fun h41))
=> '(121 144)

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (_key value) (when (numberp value)
                              (expt (+ 10 value) 2)))))
  (h-lkeep fun h41 'rev))
=> '(144 121)

(let ((h42  (h* "a" 1 'd  (h* "d1" 41 "d2" 42 "e1" 51)))
      (fun  (lambda (key value)
              (format "%s→%s" (type-of key) (type-of value)))))
  (h-lkeep fun h42))
=> '("string→integer" "symbol→hash-table")

(let ((h11  (h-new 5))
      (fun  (lambda (key value) (format "%s: %s!" key value))))
  (h-lkeep fun h11))
=> '()
h--lkeep (form table &optional reverse)

Anaphoric version of h-lkeep.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound. List the non-nil results.

If REVERSE is non-nil, show it in reverse order.

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--lkeep (format "%s: %s!" key value) h31))
=> '("Bob: cat!" "Whiskers: cat!" "Bubbly: fish!")

(let ((h41 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--lkeep (when (numberp value) (expt (+ 10 value) 2))
            h41))
=> '(121 144)

(let ((h41 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--lkeep (when (numberp value) (expt (+ 10 value) 2))
            h41 'rev))
=> '(144 121)

(let ((h42 (h* "a" 1 'd  (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--lkeep (format "%s→%s" (type-of key) (type-of value)) h42))
=> '("string→integer" "symbol→hash-table")

(let ((h11 (h-new 5)))
  (h--lkeep (format "%s: %s!" key value) h11))
=> '()
h--lunzip-with (key-form val-form table &optional reverse)

Anaphoric version of h-lunzip-with.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Each element of KEYS is bound to the symbol key.
Each element of VALUES is bound to the symbol value.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

If REVERSE is non-nil, reverse the order of both lists.

The (roughly) inverse of this function is h-zip-lists.

Alias: h--unzip-with.

See also: h--vunzip-with.

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a 1 'b nil))
=> '(("a" "b")    ("1" nil))

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a 1 'b 2))
=> '(("a" "b")    ("1" "2"))

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a 1 'b 2 'c nil))
=> '(("a" "b" "c") ("1" "2" nil))

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a nil 'b nil))
=> '(("a" "b")    (nil nil))

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h* 'a nil 'b nil))
=> '(("a" "b")    (nil nil))

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                (h-new 1))
=> '(nil nil)

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value)))
!!> wrong-number-of-arguments

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                '())
!!> error

(h--lunzip-with (symbol-name key)
                (when value (number-to-string value))
                3)
!!> error
h-lunzip-with (key-fun val-fun table &optional reverse)

Return a list of modified lists of KEYS and VALUES from TABLE.

If REVERSE is non-nil, reverse the order of both lists.

The function KEY-FUN modifies each key in the results.
The function VAL-FUN modifies each value in the results.

Both functions take two variables: key and value, respectively.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

The (roughly) inverse of this function is h-zip-lists.

Its anaphoric counterpart is h--lunzip-with.

Alias: h-unzip-with.

See also: h-vunzip-with.

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a 1 'b nil))
=> '(("a" "b")    ("1" nil))

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a 1 'b 2))
=> '(("a" "b")    ("1" "2"))

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a 1 'b 2 'c nil))
=> '(("a" "b" "c") ("1" "2" nil))

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a nil 'b nil))
=> '(("a" "b")    (nil nil))

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h* 'a nil 'b nil))
=> '(("a" "b")    (nil nil))

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               (h-new 1))
=> '(nil nil)

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               )
!!> wrong-number-of-arguments

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               '())
!!> error

(h-lunzip-with (lambda (key _value) (symbol-name key))
               (lambda (_key value) (when value (number-to-string value)))
               3)
!!> error
h-lunzip (table &optional reverse)

Return a list of the lists of KEYS and VALUES from TABLE.

If REVERSE is non-nil, reverse the order of both lists.

The (roughly) inverse of this function is h-zip-lists.

Alias: h-unzip.

See also: h-vunzip.

(h-lunzip (h* :a 1 :b nil))        => '((:a :b)    (1 nil))
(h-lunzip (h* :a 1 :b 2))          => '((:a :b)    (1 2))
(h-lunzip (h* :a 1 :b 2 :c nil))   => '((:a :b :c) (1 2 nil))
(h-lunzip (h* :a nil :b nil))      => '((:a :b)    (nil nil))
(h-lunzip (h* :a nil :b nil))      => '((:a :b)    (nil nil))
(h-lunzip (h-new 1))               => '(nil nil)
(h-lunzip)                        !!> wrong-number-of-arguments
(h-lunzip '())                    !!> error
(h-lunzip 3)                      !!> error
h-litems (table &optional reverse)

Return a list of two-element lists '(key value) from TABLE.

Like ht-items, but in TABLE's stored order by default.

If REVERSE is non-nil, show it in reverse order (the larger list;
the pair remains in the (key, value) order).

Alias: h-items.

(h-litems (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
=> '(("Bob"      "cat")
     ("Whiskers" "cat")
     ("Bubbly"  "fish"))

(h-litems (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")
          'reverse)
=> '(("Bubbly"  "fish")
     ("Whiskers" "cat")
     ("Bob"      "cat"))

(h-litems (h-new 5))
=> '()
h-lkeys (table &optional reverse)

Return a list of all the keys in TABLE.

Like ht-keys, but in TABLE's stored order by default.

If REVERSE is non-nil, show it in reverse order.

Aliases: h-keys, h-2d-keys.

(h-lkeys (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
=> '("Bob" "Whiskers" "Bubbly")

(h-lkeys (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")
         'rev)
=> '("Bubbly" "Whiskers" "Bob")

(h-lkeys (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
=> '("a" "b" "d")

(h-lkeys (h-new 5))
=> '()
h-lvalues (table &optional reverse)

Return a list of all the values in TABLE.

Like ht-values, but in TABLE's stored order.

If REVERSE is non-nil, show it in reverse order.

Alias: h-values.

(h-lvalues (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
=> '("cat" "cat" "fish")

(h-lvalues (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")
           'rev)
=> '("fish" "cat" "cat")

(h-lvalues (h-new 5))
=> '()
h-lrandom (table &optional n)

Return a list of N pseudo-randomly chosen items from hash TABLE.

N must be an integer; if not, signal error.

  • If N is nil, make it N=1.
  • If N≥ size(TABLE), return a list of all TABLE's items in pseudo-random order

    (like h-litems, but shuffled).

  • If N=0, return nil.
  • If N=1, item is returned as a two-item list (key, value).
  • If N>1, items are returned as a list of two-item lists (key, value).
  • If N is negative, make N = N + size(TABLE). For example, if TABLE has 10
    items and N=-2, return 8 pseudo-random items.

See also: h-vrandom, h-pop-random, and h-pop-random!.

;;;; It's random, so all we can do is check length and whether subset
(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4))
       (items (h-items htbl))
       (res  (h-lrandom htbl 2)))
  (and (= 2 (length res))
       (equal res (-intersection res items))))
=> t

;;;; One by one is repetitive. Let's check many at once?
(let* ((htbl  (h* :a 1 :b 2 :c 3 :d 4))
       (items (h-items htbl))
       (ns    '(1 2 3 4 5 -1 -2))
       (sizes '(1 2 3 4 4  3  2)))
  (--every (and (= (nth it-index sizes) (length it))
                (equal it (-intersection it items)))
           (-map (-cut h-lrandom htbl <>) ns)))
=> t

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4))
       (res  (h-lrandom htbl)))
  (and (= 1 (length res))
       (equal res
              (-intersection res (h-items htbl)))))
=> t

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4)))
  (null (h-lrandom htbl 0)))
=> t

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4)))
  (null (h-lrandom htbl "something")))
!!> error

(let* ((htbl (h* :a 1 :b 2 :c 3 :d 4)))
  (null (h-lrandom htbl 3.5)))
!!> error
to hash table

Functions that apply two functions to every key–value pair of a hash table
and write the results to a hash table.

For example, if you want to produce a fresh table whose keys
(currently strings) are upcased and the values (currently
integers) are increased by 1, you could run:

(h-hmap (lambda (key _value) (upcase key))
        (lambda (_key value) (1+ value))
        table)

The underlines above are so that there's no warnings about unused
variables. We do it because we didn't use VALUE in KEY-FUN, nor KEY
in VAL-FUN — but this doesn't need to be so: we could want that the
new key be also (or instead) a function of the VALUE, and
vice-versa.

Note that the pair is only added if KEY-FUN returns non-nil. So no
spurious (nil, something) pair results from some reasonable
conditional KEY-FUN such as:

(lambda (key, value) (when (> value 2) key))

In the rare case where for some reason you chose to have nil as a
key, you'll have to treat it specially.

IMPORTANT: unlike mapping to a list, mapping to a hash table
demands that the results of the keys be unique. So you must pay
attention to possible collisions. If, for example, in the case
above the original table had both "a" and "A" as original keys,
one of them would end up overwritten, because both return "A"
when upcased. Which one will depend on the key order. So:

actual size of resulting table ≤ size of original table.

In mathematical terms, excepting the rare case of a nil key in the
original, the number of keys in the resulting hash table will match
the number of keys in the original one if, and only if:

  1. there's a bijection between the set of keys and the set of
    key-fun(key,value).
  2. there's no (key,value) for which key-fun(key,value) returns nil.

Guideline for the 'hmap family' of functions:

  • the added 'h' means 'result is a hash table'
  • an added '-' means 'anaphoric: enter 2 forms, not 2 lambdas'
  • an added '*' means 'recurse: apply the values-function or -form
    to the values whenever these are hash tables'
  • an added '!' means 'destructive: modify TABLE instead of
    creating a fresh one'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return fresh table

  2 lambdas 2 forms (anaphoric)
simple, doesn't recurse h-hmap h--hmap
nesting-aware, recurses h-hmap* h--hmap*

Destructive: modify original TABLE, return nil

  2 lambdas 2 forms (anaphoric)
simple, doesn't recurse h-hmap! h--hmap!
nesting-aware, recurses h-hmap*! h--hmap*!

See also: h-2d-hmap, h--2d-hmap, h-2d-hmap!, h--2d-hmap!

non-recursive
h-hmap (key-fun val-fun table)

Return a table from applying KEY-FUN, VAL-FUN to each pair of TABLE.

While h-lmap produces a list, this one produces a hash table.

Both KEY-FUN and VAL-FUN are each called with two arguments, KEY
and VALUE.

  • The result of KEY-FUN is the new key.
  • The result of VAL-FUN is the new value.

Its anaphoric counterpart is h--hmap.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (kfun (lambda (key _value) (read (format ":%s" key))))
      (vfun (lambda (_key value) (if (numberp value)
                                     (expt (+ 10 value) 2)
                                   value))))
  (h-hmap kfun vfun h41))
H=> (h* :a 121
        :b 144
        :d (h* "d1" 41
               "d2" 42
               "e1" 51))

(let ((h21  (h* :a 1 :b 2 :c 3)))
  (h-hmap (lambda (key value) (when (>= value 2) key))
          (lambda (_key value) value)
          h21))
H=> (h* :b 2 :c 3)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (kfun (lambda (key _value) (upcase key)))
      (vfun (lambda (key  value) (concat key (capitalize value)))))
  (h-hmap kfun vfun h31))
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

;;;; Beware of collisions!
(let ((h31  (h* "a" "cat" "A" "nother cat"))
      (kfun (lambda (key _value) (upcase key)))
      (vfun (lambda (key  value) (concat key (upcase value)))))
  (h-hmap kfun vfun h31))
H=> (h* "A" "ANOTHER CAT")

(let ((h31  (h* "A" "nother cat" "a" "cat"))
      (kfun (lambda (key _value) (upcase key)))
      (vfun (lambda (key  value) (concat key (upcase value)))))
  (h-hmap kfun vfun h31))
H=> (h* "A" "aCAT")
;; In the previous two examples, the transforming function upcase
;; made a previous key be overridden.

(let ((h11  (h* 'a 1 'b 2))
      (kfun (lambda (key _value) key))
      (vfun (lambda (_key value)  (format "%s!" value))))
  (h-hmap kfun vfun  h11))
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h* 'a 1 'b 2))
      (kfun (lambda (key _value) key))
      (vfun (lambda (_key value)  (format "%s!" value))))
  (h-hmap kfun vfun  h11))
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5))
      (kfun (lambda (key _value) key))
      (vfun (lambda (key value)  (format "%s: %s!" key value))))
  (h-hmap kfun vfun  h11))
H=> (h-new 5)
h--hmap (key-form val-form table)

Anaphoric version of h-hmap.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

While h--lkeep produces a list, this one produces a hash table.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--hmap (read (format ":%s" key))
           (if (numberp value)
               (expt (+ 10 value) 2)
             value)
           h41))
H=> (h* :a 121
        :b 144
        :d (h* "d1" 41
               "d2" 42
               "e1" 51))

(let ((h21  (h* :a 1 :b 2 :c 3)))
  (h--hmap (when (>= value 2) key)
           value h21))
H=> (h* :b 2 :c 3)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--hmap (upcase key)
           (concat key (capitalize value))
           h31))
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2)))
  (h--hmap key (format "%s!" value) h11))
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5)))
  (h--hmap key (format "%s: %s!" key value) h11))
H=> (h-new 5)
h-hmap! (key-fun val-fun table)

Update TABLE by applying KEY-FUN, VAL-FUN to its pairs.

Just like h-hmap (which see), but modifies TABLE instead of
producing a fresh one.

Its anaphoric counterpart is h--hmap!.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (kfun (lambda (key _value) (read (format ":%s" key))))
      (vfun (lambda (_key value) (if (numberp value)
                                     (expt (+ 10 value) 2)
                                   value))))
  (h-hmap! kfun vfun h41)
  h41)
H=> (h* :a 121
        :b 144
        :d (h* "d1" 41
               "d2" 42
               "e1" 51))

(let ((h21  (h* :a 1 :b 2 :c 3)))
  (h-hmap! (lambda (key value) (when (>= value 2) key))
           (lambda (_key value) value)
           h21)
  h21)
H=> (h* :b 2 :c 3)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (kfun (lambda (key _value) (upcase key)))
      (vfun (lambda (key  value) (concat key (capitalize value)))))
  (h-hmap! kfun vfun h31)
  h31)
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2))
      (kfun (lambda (key _value) key))
      (vfun (lambda (_key value)  (format "%s!" value))))
  (h-hmap! kfun vfun h11)
  h11)
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5))
      (kfun (lambda (key _value) key))
      (vfun (lambda (key value)  (format "%s: %s!" key value))))
  (h-hmap! kfun vfun h11)
  h11)
H=> (h-new 5)
h--hmap! (key-form val-form table)

Anaphoric version of h-hmap!.

For every key–value pair in TABLE, evaluate KEY-FORM and VAL-FORM
with the variables key and value bound.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--hmap! (read (format ":%s" key))
            (if (numberp value)
                (expt (+ 10 value) 2)
              value)
            h41)
  h41)
H=> (h* :a 121
        :b 144
        :d (h* "d1" 41
               "d2" 42
               "e1" 51))

(let ((h21  (h* :a 1 :b 2 :c 3)))
  (h--hmap! (when (>= value 2) key)
            value h21)
  h21)
H=> (h* :b 2 :c 3)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--hmap! (upcase key)
            (concat key (capitalize value))
            h31)
  h31)
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2)))
  (h--hmap! key (format "%s!" value) h11)
  h11)
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5)))
  (h--hmap! key (format "%s: %s!" key value) h11)
  h11)
H=> (h-new 5)
recursive
h-hmap* (key-fun val-fun table)

Return a table from applying KEY-FUN, VAL-FUN to each pair of TABLE.

Just like h-hmap, but recurses wherever TABLE is nested.

This means that for any VALUE that is itself a hash table, instead
of replacing it with the result of VAL-FUN, as h-hmap would
do, VALUE will be this very h-hmap* recursively applied to
that hash table with KEY-FUN and VAL-FUN.

Its anaphoric counterpart is h--hmap*.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (kfun (lambda (key _value) (read (format ":%s" key))))
      (vfun (lambda (_key value) (expt (+ 10 value) 2))))
  (h-hmap* kfun vfun h41))
H=> (h* :a 121
        :b 144
        :d (h* :d1 2601
               :d2 2704
               :e1 3721))

(let ((h21  (h* :a 1 :b 2 :c (h* :d 1 :e 5 :f 0 :g 7))))
  (h-hmap* (lambda (key value) (when (or (ht? value) (>= value 2))
                                 key))
           (lambda (_key value) value)
           h21))
H=> (h* :b 2 :c (h* :e 5 :g 7))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (kfun (lambda (key _value) (upcase key)))
      (vfun (lambda (key  value) (concat key (capitalize value)))))
  (h-hmap* kfun vfun h31))
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2))
      (kfun (lambda (key _value) key))
      (vfun (lambda (_key value)  (format "%s!" value))))
  (h-hmap* kfun vfun  h11))
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5))
      (kfun (lambda (key _value) key))
      (vfun (lambda (key value)  (format "%s: %s!" key value))))
  (h-hmap* kfun vfun  h11))
H=> (h-new 5)
h--hmap* (key-form val-form table)

Anaphoric version of h-hmap*.

For every key–value pair in TABLE, evaluate KEY-FORM and VAL-FORM
with the variables key and value bound.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--hmap* (read (format ":%s" key))
            (expt (+ 10 value) 2)
            h41))
H=> (h* :a 121
        :b 144
        :d (h* :d1 2601
               :d2 2704
               :e1 3721))

(let ((h21  (h* :a 1 :b 2 :c (h* :d 1 :e 5 :f 0 :g 7))))
  (h--hmap* (when (or (ht? value) (>= value 2))
              key)
            value h21))
H=> (h* :b 2 :c (h* :e 5 :g 7))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--hmap* (upcase key)
            (concat key (capitalize value))
            h31))
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2)))
  (h--hmap* key (format "%s!" value) h11))
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5)))
  (h--hmap* key (format "%s: %s!" key value) h11))
H=> (h-new 5)
h-hmap*! (key-fun val-fun table)

Update TABLE by applying KEY-FUN, VAL-FUN to each of its pairs.

Just like h-hmap* (which see), but modifies TABLE instead of
producing a fresh one.

Its anaphoric counterpart is h--hmap*!.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51)))
      (kfun (lambda (key _value) (read (format ":%s" key))))
      (vfun (lambda (_key value) (expt (+ 10 value) 2))))
  (h-hmap*! kfun vfun h41)
  h41)
H=> (h* :a 121
        :b 144
        :d (h* :d1 2601
               :d2 2704
               :e1 3721))
(let ((h21  (h* :a 1 :b 2 :c (h* :d 1 :e 5 :f 0 :g 7))))
  (h-hmap*! (lambda (key value) (when (or (ht? value) (>= value 2))
                                  key))
            (lambda (_key value) value)
            h21)
  h21)
H=> (h* :b 2 :c (h* :e 5 :g 7))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (kfun (lambda (key _value) (upcase key)))
      (vfun (lambda (key  value) (concat key (capitalize value)))))
  (h-hmap*! kfun vfun h31)
  h31)
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2))
      (kfun (lambda (key _value) key))
      (vfun (lambda (_key value)  (format "%s!" value))))
  (h-hmap*! kfun vfun h11)
  h11)
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5))
      (kfun (lambda (key _value) key))
      (vfun (lambda (key value)  (format "%s: %s!" key value))))
  (h-hmap*! kfun vfun h11)
  h11)
H=> (h-new 5)
h--hmap*! (key-form val-form table)

Anaphoric version of h-hmap*!.

For every key–value pair in TABLE, evaluate KEY-FORM and VAL-FORM
with the variables key and value bound.

(let ((h41  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51))))
  (h--hmap*! (read (format ":%s" key))
             (expt (+ 10 value) 2)
             h41)
  h41)
H=> (h* :a 121
        :b 144
        :d (h* :d1 2601
               :d2 2704
               :e1 3721))
(let ((h21  (h* :a 1 :b 2 :c (h* :d 1 :e 5 :f 0 :g 7))))
  (h--hmap*! (when (or (ht? value) (>= value 2))
               key)
             value h21)
  h21)
H=> (h* :b 2 :c (h* :e 5 :g 7))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--hmap*! (upcase key)
             (concat key (capitalize value))
             h31)
  h31)
H=> (h* "BOB"      "BobCat"
        "WHISKERS" "WhiskersCat"
        "BUBBLY"   "BubblyFish")

(let ((h11  (h* 'a 1 'b 2)))
  (h--hmap*! key (format "%s!" value) h11)
  h11)
H=> (h* 'a "1!" 'b "2!")

(let ((h11  (h-new 5)))
  (h--hmap*! key (format "%s: %s!" key value) h11)
  h11)
H=> (h-new 5)
for side-effects only

Functions that map over the hash table for producing side-effects
somewhere.

h-each (table fun)

Apply function FUN to each key–value pair of TABLE.

FUN is called with two arguments: key and value.

Intended to be used for side-effects only. Returns nil.

This function is similar to current ht-each (in turn an alias to
maphash), but with TABLE as the first argument.

Its anaphoric counterpart is h--each.

(let ((table (h* "a" 1 "b" 2 "c" 3))
      res)
  (h-each table
    (lambda (k v)
      (push (format "%s--%d" (upcase k) v)
            res)))
  (nreverse res))
=> '("A--1" "B--2" "C--3")

(let ((table (h* "a" 1 "b" 2 "c" 3)))
  (with-output-to-string
    (h-each table
      (lambda (key value)
        (princ (upcase key))
        (princ " = ")
        (princ value)
        (princ "\n")))))
=> "A = 1\nB = 2\nC = 3\n"

(let ((table (h* 'a 1 'b 2))
      res)
  (h-each table
    (lambda (key _value)
      (push (upcase (format "%s%s" key key))
            res)))
  (nreverse res))
=> '("AA" "BB")

(let ((table (h*))
      res)
  (h-each table
    (lambda (key _value)
      (push (format "%s" (upcase key))
            res)))
  (nreverse res))
=> '()
h--each (table &rest body)

Anaphoric version of h-each.

For every key–value pair in TABLE, evaluate BODY with the variables
key and value bound.

This function is similar to current ht-aeach. Differences:

  1. TABLE is the first argument.
  2. BODY instead of FORM, obviating the need of progn whenever
    more than one form is needed.
  3. issues no warnings if either KEY or VALUE isn't used.
(let ((table (h* "a" 1 "b" 2 "c" 3))
      res)
  (h--each table
    (push (format "%s--%d" (upcase key) value)
          res))
  (nreverse res))
=> '("A--1" "B--2" "C--3")

(let ((table (h* "a" 1 "b" 2 "c" 3)))
  (with-output-to-string
    (h--each table
      (princ (upcase key))
      (princ " = ")
      (princ value)
      (princ "\n"))))
=> "A = 1\nB = 2\nC = 3\n"

;;;;; it can be used to call destructive functions on the table...
(let ((table (h* 0 "alice" 1 "bob")))
  (h--each table
    (h-put! table key (capitalize value)))
  table)
H=> (h* 0 "Alice"
        1 "Bob")
;;;;; ...but for that, better use h--hmap! instead
(let ((table (h* 0 "alice" 1 "bob")))
  (h--hmap! key (capitalize value) table)
  table)
H=> (h* 0 "Alice"
        1 "Bob")

;;;;; empty
(let ((table (h*))
      res)
  (h--each table
    (push (format "%s--%d" (upcase key) value)
          res))
  (nreverse res))
=> '()
Keys operations
Retrieval (getting)

Functions that retrieve key–value pairs from tables.

Return value given key

Currently, there are no functions here.

  • h-get is aliased to ht-get.
  • h-get* is aliased to ht-get*.
Return the very first pair (pop)
h-pop (table)

Return the very first pair from TABLE.

Pair is returned as a two-element list: '(key value).

Note that this function is not destructive: it won't remove the
pair from TABLE. For that, use h-pop!.

;;;; Return the first pair
(h-pop (h* "a" 1 "b" 2 "c" 3))
=> '("a" 1)

;;;; Return the key of first-found pair
(car (h-pop (h* "a" 1 "b" 2 "c" 3)))
=> "a"

;;;; Nested
(cadr (h-pop (h* "a" (h* "b" 2 "c" 3) "d" 4 "e" 5)))
H=> (h* "b" 2 "c" 3)

;;;; Null
(h-pop (h*))
=> '()
h-pop! (table)

Return the very first pair from TABLE and remove it.

Pair is returned as a two-element list: '(key value).

If you don't want it removed, use h-pop instead.

;;;; Return the first pair
(let ((tbl (h* "a" 1 "b" 2 "c" 3)))
  (h-pop! tbl))
=> '("a" 1)

(let ((tbl (h* "a" 1 "b" 2 "c" 3)))
  (h-pop! tbl)
  tbl)
H=> (h* "b" 2 "c" 3)

;;;; Return the key of first-found pair
(car (h-pop! (h* "a" 1 "b" 2 "c" 3)))
=> "a"

;;;; Nested
(let ((tbl (h* "a" (h* "b" 2 "c" 3) "d" 4 "e" 5)))
  (cadr (h-pop! tbl)))
H=> (h* "b" 2 "c" 3)

(let ((tbl (h* "a" (h* "b" 2 "c" 3) "d" 4 "e" 5)))
  (h-pop! tbl)
  tbl)
H=> (h* "d" 4 "e" 5)

;;;; Null
(h-pop! (h*))
=> '()
Return a random pair
h-pop-random (table)

Return a pseudo-random pair from TABLE.

Pair is returned as a two-element list: '(key value).

Note that this function is not destructive: it won't remove the
pair from TABLE. For that, use h-pop-random!.

See also: h-vrandom and h-lrandom.

;;;; It's random, so as a weak test we'll check whether a subset,
;;;; as well as the hash table's size after the operation
(let* ((htbl  (h* :a 1 :b 2 :c 3 :d 4))
       (size  (h-size  htbl))
       (items (h-items htbl))
       (res   (h-pop-random htbl)))
  (and (member res items)
       (= (h-size htbl) size)))
=> t
h-pop-random! (table)

Return a pseudo-random pair from TABLE and remove it.

Pair is returned as a two-element list: '(key value).

If you don't want it removed, use h-pop-random.

See also: h-vrandom and h-lrandom.

;;;; It's random, so as a weak test we'll check whether a subset,
;;;; as well as the hash table's size after the operation
(let* ((htbl  (h* :a 1 :b 2 :c 3 :d 4))
       (size  (h-size  htbl))
       (items (h-items htbl))
       (res   (h-pop-random! htbl)))
  (and (member res items)
       (= (h-size htbl) (1- size))))
=> t
Return the first pair matching a predicate
h-first (fun table)

Return the first pair from TABLE for which FUN returns non-nil.

In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

Function FUN is called with two arguments, key and value.

This function is exactly like current ht-find.

;;;; Return the key that has the first-found value >1
(car (h-first (lambda (_k value) (> value 1))
              (h* "a" 1 "b" 2 "c" 3)))
=> "b"

;;;; Return the very first pair stored in the table (like h-pop)
(h-first (lambda (_k _v) t) (h* "a" 1 "b" 2 "c" 3))
=> '("a" 1)
h--first (form table)

Anaphoric version of h-first.

Return the first pair from TABLE for which FORM returns non-nil.
In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

FORM is called with two arguments: key and value.

;;;; Return the key that has the first-found value >1
(car (h--first (> value 1)
               (h* "a" 1 "b" 2 "c" 3)))
=> "b"

;;;; Return the very first pair stored in the table (like h-pop)
(h--first t (h* "a" 1 "b" 2 "c" 3))
=> '("a" 1)
h-first! (fun table)

Remove from TABLE the first pair for which FUN returns non-nil.

In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

Function FUN is called with two arguments, key and value.

;;;; Return the key that has the first-found value >1
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (car (h-first! (lambda (_k value) (> value 1)) htbl)))
=> "b"

;;;;; ...and here is the hash table after the above
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (car (h-first! (lambda (_k value) (> value 1)) htbl))
  htbl)
H=> (h* "a" 1 "c" 3)

;;;; Return the very first pair stored in the table (like h-pop!)
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (h-first! (lambda (_k _v) t) htbl))
=> '("a" 1)

;;;;; ...and here is the hash table after the above
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (h-first! (lambda (_k _v) t) htbl)
  htbl)
H=> (h* "b" 2 "c" 3)
h--first! (form table)

Anaphoric version of h-first!.

Remove from TABLE the first pair for which FORM returns non-nil.
In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

FORM is called with two arguments, key and value.

;;;; Return the key that has the first-found value >1
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (car (h--first! (> value 1) htbl)))
=> "b"

;;;;; ...and here is the hash table after the above
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (car (h--first! (> value 1) htbl))
  htbl)
H=> (h* "a" 1 "c" 3)

;;;; Return the very first pair stored in the table (like h-pop!)
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (h--first! t htbl))
=> '("a" 1)

;;;;; ...and here is the hash table after the above
(let ((htbl (h* "a" 1 "b" 2 "c" 3)))
  (h--first! t htbl)
  htbl)
H=> (h* "b" 2 "c" 3)
Addition (setting)

Functions that add a key–value pair to tables, either destructively or
side-effects-free. Included are those that take a sequence of keys
and a final value — for nested hash tables.

Same logic applies to put functions as with mix functions — only
that it's table + key–value instead of table + tables; and only one
kv pair makes sense (otherwise it'd be either a hash table with
the pairs, to which we'd apply mixing; or some alist or plist,
which we'd convert to htbl then mix).

It may seem a bit odd to "put a pair non-destructively", but the
logic is the same as that of h-mix, and having an exactly analogous
abstraction is useful.

put

Summary:

  Non-destructive Destructive
simple, doesn't recurse h-put h-put!
nesting-aware, recurses h-put* h-put*!

While the destructive h-put! is like ht-set!, there's no non-destructive
counterparts in ht library.

h-put! (table key value)

Associate KEY in TABLE with VALUE.

This is EXACTLY equivalent to current ht-set!.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-put.

This function is not nesting-aware.
Its also-destructive but nesting-aware counterpart is h-put*!.

For nesting-awareness with no side-effects, use h-put*.

;;;;; replacing nested won't work
(let ((tbl (ht ("a" (ht ("b" (ht ("c" (ht ("d" 'something))))))))))
  (h-put! tbl "a" "b" "c" "d" 4)
  tbl)
!!> wrong-number-of-arguments

;;;;; replace a value-that-is-ht with non-ht value
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put! tbl :a 5)
  tbl)
H=> (h* :z 'a :a 5)

;;;;; replace a non-ht value with a non-ht value
(let ((tbl (ht ("a" 1))))
  (h-put! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put! tbl "b" 2)
  tbl)
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))
h-put (table key value)

Return a table that is TABLE with KEY–VALUE applied.

This is a side-effect-free function.
Its destructive counterpart is h-put!.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is h-put*.

For nesting-awareness with side-effects, use h-put*!.

;;;;; replacing nested won't work
(let ((tbl (ht ("a" (ht ("b" (ht ("c" (ht ("d" 'something))))))))))
  (h-put tbl "a" "b" "c" "d" 4))
!!> wrong-number-of-arguments

;;;;; replace a value-that-is-ht with non-ht value
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put tbl :a 5))
H=> (h* :z 'a :a 5)

;;;;; replace a non-ht value with a non-ht value
(let ((tbl (ht ("a" 1))))
  (h-put tbl "a" 2))
H=> (ht ("a" 2))

;;;;; note: original tbl remains unmodified
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put tbl :a 5)
  tbl)
H=> (h* :z 'a
        :a (h* :x 4
               :b 2))

(let ((tbl (ht ("a" 1))))
  (h-put tbl "a" 2)
  tbl)
H=> (ht ("a" 1))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put tbl "b" 2))
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put tbl "a" 2))
H=> (ht ("a" 2))
h-put*! (table &rest keys-value)

Set VALUE to KEYS sequence in hash table TABLE.

KEYS-VALUE are KEYS and VALUE.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

This last key will be set to VALUE.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-put*.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is h-put!.

For no nesting-awareness and no side-effects, use h-put.

;;;;; a quick comparison with ht
;; In 'ht's README, it says that these are equivalent — and they are:
(let ((table (ht (1 (ht (2 (ht (3 "three"))))))))
  (ht-set! (ht-get (ht-get table 1) 2) 3 :three)
  table)
H=> (let ((table (ht (1 (ht (2 (ht (3 "three"))))))))
      (setf (ht-get* table 1 2 3) :three)
      table)

;; With h-put*! it gets even shorter:
(let ((table (ht (1 (ht (2 (ht (3 "three"))))))))
  (h-put*! table 1 2 3 :three)
  table)
H=> (h* 1 (h* 2 (h* 3 :three)))

;;;;; replacing last key
(let ((tbl (ht ("a" (ht ("b" (ht ("c" (ht ("d" 'something))))))))))
  (h-put*! tbl "a" "b" "c" "d" 4)
  tbl)
H=> (ht ("a" (ht ("b" (ht ("c" (ht ("d" 4))))))))

;;;;; creating keys on-the-fly (replace non-ht key)
(let ((tbl (ht ("a" "b"))))
  (h-put*! tbl "a" "b" "c" "d" 4)
  tbl)
H=> (ht ("a" (ht ("b" (ht ("c" (ht ("d" 4))))))))

;;;;; Replace a value-that-is-ht with a non-ht value
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put*! tbl :a :b :c :d 1)
  tbl)
H=> (h* :z 'a
        :a (h* :x 4
               :b (h* :c (h* :d 1))))

(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b (h* :e 2)))))
  (h-put*! tbl :a :b :c :d 1)
  tbl)
H=> (h* :z 'a
        :a (h* :x 4
               :b (h* :e 2
                      :c (h* :d 1))))

(let* ((tbl (h* :z 5
                :a 4)))
  (h-put*! tbl :w :b :c :d 1)
  tbl)
H=> (h* :z 5
        :a 4
        :w (h* :b (h* :c (h* :d 1))))

;;;; Simple: behave like ht-set!
;;;;; replace a value-that-is-ht with non-ht value
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put*! tbl :a 5)
  tbl)
H=> (h* :z 'a :a 5)

;;;;; replace a non-ht value with a non-ht value
(let ((tbl (ht ("a" 1))))
  (h-put*! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put*! tbl "b" 2)
  tbl)
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put*! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))
h-put* (table &rest keys-value)

Return a table that is TABLE with KEYS–VALUE applied.

KEYS-VALUE are KEYS and VALUE.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

This last key will be set to VALUE.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This is a side-effect-free function.
Its destructive counterpart is h-put*!.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is h-put.

For no nesting-awareness but with side-effects, use h-put!.

;;;;; a quick comparison with ht
;; In 'ht's README, it says that these are equivalent — and they are:
(let ((table (ht (1 (ht (2 (ht (3 "three"))))))))
  (ht-set! (ht-get (ht-get table 1) 2) 3 :three)
  table)
H=> (let ((table (ht (1 (ht (2 (ht (3 "three"))))))))
      (setf (ht-get* table 1 2 3) :three)
      table)

;; With h-put* it gets even shorter:
(let ((table (ht (1 (ht (2 (ht (3 "three"))))))))
  (h-put* table 1 2 3 :three))
H=> (h* 1 (h* 2 (h* 3 :three)))

;;;;; replacing last key
(let ((tbl (ht ("a" (ht ("b" (ht ("c" (ht ("d" 'something))))))))))
  (h-put* tbl "a" "b" "c" "d" 4))
H=> (ht ("a" (ht ("b" (ht ("c" (ht ("d" 4))))))))

;;;;; creating keys on-the-fly (replace non-ht key)
(let ((tbl (ht ("a" "b"))))
  (h-put* tbl "a" "b" "c" "d" 4))
H=> (ht ("a" (ht ("b" (ht ("c" (ht ("d" 4))))))))

;;;;; replace a value-that-is-ht with a non-ht value
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put* tbl :a :b :c :d 1))
H=> (h* :z 'a
        :a (h* :x 4
               :b (h* :c (h* :d 1))))

(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b (h* :e 2)))))
  (h-put* tbl :a :b :c :d 1))
H=> (h* :z 'a
        :a (h* :x 4
               :b (h* :e 2
                      :c (h* :d 1))))

(let* ((tbl (h* :z 5
                :a 4)))
  (h-put* tbl :w :b :c :d 1))
H=> (h* :z 5
        :a 4
        :w (h* :b (h* :c (h* :d 1))))

;;;;; replace a value-that-is-ht with non-ht value
(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put* tbl :a 5))
H=> (h* :z 'a :a 5)

;;;;; replace a non-ht value with a non-ht value
(let ((tbl (ht ("a" 1))))
  (h-put* tbl "a" 2))
H=> (ht ("a" 2))

;;;;; note: original tbl remains unmodified
(let* ((tbl (h* :z 5
                :a 4)))
  (h-put* tbl :w :b :c :d 1)
  tbl)
H=> (h* :z 5
        :a 4)

(let* ((tbl (h* :z 'a
                :a (h* :x 4
                       :b 2))))
  (h-put* tbl :a 5)
  tbl)
H=> (h* :z 'a
        :a (h* :x 4
               :b 2))

(let ((tbl (ht ("a" 1))))
  (h-put* tbl "a" 2)
  tbl)
H=> (ht ("a" 1))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put* tbl "b" 2))
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put* tbl "a" 2))
H=> (ht ("a" 2))

;;;; A few proofs that the original table isn't modified
;; (it uses h-clone* to generate the initial copy)
(let ((tbl (h* :a 1 :b (h* :c 3))))
  (-> tbl
      (h-put* :a 2))
  tbl)
H=> (h* :a 1
        :b (h* :c 3))

(let ((tbl (h* :a 1 :b (h* :c 3))))
  (-> tbl
      (h-put* :b :c 4))
  tbl)
H=> (h* :a 1
        :b (h* :c 3))

(let ((tbl (h* :a '(1 2) :b (h* :c 3))))
  (-> tbl
      (h-put* :a '(6 7)))
  tbl)
H=> (h* :a '(1 2)
        :b (h* :c 3))

(let ((tbl (h* :a '(1 2) :b (h* :c 3))))
  (-> tbl
      (h-put* :a (cons 7 (h-get tbl :a))))
  tbl)
H=> (h* :a '(1 2)
        :b (h* :c 3))

(let ((tbl (h* :a 1 :b (h* :c 3 :d 4))))
  (-> tbl
      (h-put* :a 2)
      (h-put* :b :c 5))
  tbl)
H=> (h* :a 1
        :b (h* :c 3
               :d 4))

(let ((tbl (h* :a 1 :b (h* :c 3 :d (h* :e 5)))))
  (-> tbl
      (h-put* :a 2)
      (h-put* :b :d :e 7))
  tbl)
H=> (h* :a 1
        :b (h* :c 3
               :d (h* :e 5)))
put-add

While 'put' replaces any current value (call it 'curval')
with the newly provided value, 'put-add' adds to it.

The idea is that you are adding to a set. So:

Current value New value
#s(hash table) - if VALUE is also a hash table,
  then (h-mix*! VALUE CURVAL);
  - if VALUE is an explicit key–value structure, try
  to convert it to hash table, then h-mix*!;
  - otherwise, don't add: keep CURVAL intact.
   
  By 'explicit' I mean: no implicit ones, such as
  simple list, vector, or string lines.
   
  And for simplicity, 'key–value lines' string
  is also out.
[some vector] [VALUE some vector]
'(some list) '(VALUE some list)
nil VALUE

With anything else, make a list, adding VALUE to its first place.
Example:

Current value New value
42 '(VALUE 42)
:keyword '(VALUE :keyword)
'symbol '(VALUE symbol)
"string" '(VALUE "string")
etc.  

Summary:

  Non-destructive Destructive
simple, doesn't recurse h-put-add h-put-add!
nesting-aware, recurses h-put-add* h-put-add*!
h-put-add! (table key value)

Add VALUE to the current value of KEY in hash table TABLE.

While h-put! replaces any current value (call it 'curval')
with the newly provided value, h-put-add! adds to it.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-put-add.

This function is not nesting-aware.
Its also-destructive but nesting-aware counterpart is h-put-add*!.

For nesting-awareness with no side-effects, use h-put-add*.

;;;; Nope: this is for h-put-add*!
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add! tbl :a :c :d 5)
  tbl)
!!> wrong-number-of-arguments

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add! tbl :a :c (h* :d 5))
  tbl)
!!> wrong-number-of-arguments

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add! tbl :a :c (h* :e 5))
  tbl)
!!> wrong-number-of-arguments

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add! tbl :a :c '(:e 5))
  tbl)
!!> wrong-number-of-arguments

;;;; Behavior should be different from h-put!
(let ((tbl (ht ("a" 1))))
  (h-put-add! tbl "a" 2)
  tbl)
H=> (ht ("a" '(2 1)))

(let ((tbl (ht ("a" 1) ("k" 10))))
  (h-put-add! tbl "a" 2)
  tbl)
H=> (ht ("a" '(2 1)) ("k" 10))

(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add! tbl "a" 2)
  tbl)
H=> (ht ("a" (ht ("c" 3))) ("k" 10))

;;;;; adding hash table
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add! tbl :a (ht (:d 4)))
  tbl)
H=> (h* :a (h* :c 3 :d 4) :k 10)

(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add! tbl :a (ht (:c 30)))
  tbl)
H=> (h* :a (h* :c 30) :k 10)

;;;;; adding plist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add! tbl :a '(:d 4 :e 5))
  tbl)
H=> (h* :a (h* :c 3 :d 4 :e 5) :k 10)

;;;;; adding alist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add! tbl :a '((:c . 30) (:e . 5)))
  tbl)
H=> (h* :a (h* :c 30 :e 5) :k 10)

;;;;; adding json
(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add! tbl "a" "{\n  \"c\": 30,\n  \"e\": 5\n}")
  tbl)
H=> (h* "a" (h* "c" 30 "e" 5) "k" 10)

;;;;; adding lol
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" '(("id" "name"  "age")
                        ("b"  "bob"   "30")
                        ("e"  "emily" "21")))
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding orgtbl
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "| id | name  | age |
                       |----+-------+-----|
                       | b  | bob   |  30 |
                       | e  | emily |  21 |")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding tsv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add! tbl "m" "id\tname\nb\tbob\ne\temily")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "id\tname\tage\nb\tbob\t30\ne\temily\t21")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))


(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "id\tname\tage\nb\tbob\t30\na\talice\t35")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding csv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add! tbl "m" "id,name\nb,bob\ne,emily")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "id,name,age\nb,bob,30\ne,emily,21")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "id,name,age\nb,bob,30\na,alice,35")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding ssv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add! tbl "m" "id   name\nb   bob\ne   emily")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "id   name   age\nb   bob   30\ne   emily   21")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add! tbl "m" "id   name   age\nb   bob   30\na   alice   35")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))


;;;; Behavior should be like ht-put!
;;;;; replace key
(let ((tbl (ht ("a" nil))))
  (h-put-add! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put-add! tbl "b" 2)
  tbl)
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put-add! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))
h-put-add (table key value)

Return a table that is TABLE with VALUE added to KEY's current.

While h-put replaces any current value with the newly provided
value, h-put-add adds to it.

This is a side-effect-free function.
Its destructive counterpart is h-put-add!.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is h-put-add*.

For nesting-awareness with side-effects, use h-put-add*!.

;;;; Nope: this is for h-put-add*
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add tbl :a :c :d 5))
!!> wrong-number-of-arguments

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add tbl :a :c (h* :d 5)))
!!> wrong-number-of-arguments

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add tbl :a :c (h* :e 5)))
!!> wrong-number-of-arguments

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add tbl :a :c '(:e 5)))
!!> wrong-number-of-arguments

;;;; Behavior should be different from h-put
(let ((tbl (ht ("a" 1))))
  (h-put-add tbl "a" 2))
H=> (ht ("a" '(2 1)))

(let ((tbl (ht ("a" 1) ("k" 10))))
  (h-put-add tbl "a" 2))
H=> (ht ("a" '(2 1)) ("k" 10))

(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add tbl "a" 2))
H=> (ht ("a" (ht ("c" 3))) ("k" 10))

;;;;; adding hash table
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add tbl :a (ht (:d 4))))
H=> (h* :a (h* :c 3 :d 4) :k 10)

(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add tbl :a (ht (:c 30))))
H=> (h* :a (h* :c 30) :k 10)

;;;;; adding plist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add tbl :a '(:d 4 :e 5)))
H=> (h* :a (h* :c 3 :d 4 :e 5) :k 10)

;;;;; adding alist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add tbl :a '((:c . 30) (:e . 5))))
H=> (h* :a (h* :c 30 :e 5) :k 10)

;;;;; adding json
(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add tbl "a" "{\n  \"c\": 30,\n  \"e\": 5\n}"))
H=> (h* "a" (h* "c" 30 "e" 5) "k" 10)

;;;;; adding lol
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" '(("id" "name"  "age")
                       ("b"  "bob"   "30")
                       ("e"  "emily" "21"))))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding orgtbl
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "| id | name  | age |
                      |----+-------+-----|
                      | b  | bob   |  30 |
                      | e  | emily |  21 |"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding tsv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add tbl "m" "id\tname\nb\tbob\ne\temily"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "id\tname\tage\nb\tbob\t30\ne\temily\t21"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))


(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "id\tname\tage\nb\tbob\t30\na\talice\t35"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding csv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add tbl "m" "id,name\nb,bob\ne,emily"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "id,name,age\nb,bob,30\ne,emily,21"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "id,name,age\nb,bob,30\na,alice,35"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding ssv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add tbl "m" "id   name\nb   bob\ne   emily"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "id   name   age\nb   bob   30\ne   emily   21"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add tbl "m" "id   name   age\nb   bob   30\na   alice   35"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))


;;;; Behavior should be like ht-put
;;;;; replace key
(let ((tbl (ht ("a" nil))))
  (h-put-add tbl "a" 2))
H=> (ht ("a" 2))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put-add tbl "b" 2))
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put-add tbl "a" 2))
H=> (ht ("a" 2))
h-put-add*! (table &rest keys-value)

Add VALUE to the current KEYS sequence's value in hash table TABLE.

KEYS-VALUE are KEYS and VALUE.

While h-put*! replaces any current value with the newly provided
value, h-put-add*! adds to it.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-put-add*.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is h-put-add!.

For no nesting-awareness and no side-effects, use h-put-add.

;;;; Can't be done with h-put-add!
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add*! tbl :a :c :d 5)
  tbl) ;; adding the 5 to the 4: a list
H=> (h* :a (h* :c (h* :d '(5 4))) :k 10)

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add*! tbl :a :c (h* :d 5))
  tbl) ;; adding (h* :d 5) to (h* :d 4): a hash table
H=> (h* :a (h* :c (h* :d 5)) :k 10)

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add*! tbl :a :c (h* :e 5))
  tbl) ;; adding (h* :e 5) to (h* :d 4): a hash table
H=> (h* :a (h* :c (h* :d 4 :e 5)) :k 10)

(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add*! tbl :a :c '(:e 5))
  tbl) ;; adding plist '(:e 5) to (h* :d 4): a hash table
H=> (h* :a (h* :c (h* :d 4 :e 5)) :k 10)

;;;; Behavior should be different from h-put*!
(let ((tbl (ht ("a" 1))))
  (h-put-add*! tbl "a" 2)
  tbl)
H=> (ht ("a" '(2 1)))

(let ((tbl (ht ("a" 1) ("k" 10))))
  (h-put-add*! tbl "a" 2)
  tbl)
H=> (ht ("a" '(2 1)) ("k" 10))

(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add*! tbl "a" 2)
  tbl)
H=> (ht ("a" (ht ("c" 3))) ("k" 10))

;;;;; adding hash table
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add*! tbl :a (ht (:d 4)))
  tbl)
H=> (h* :a (h* :c 3 :d 4) :k 10)

(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add*! tbl :a (ht (:c 30)))
  tbl)
H=> (h* :a (h* :c 30) :k 10)

;;;;; adding plist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add*! tbl :a '(:d 4 :e 5))
  tbl)
H=> (h* :a (h* :c 3 :d 4 :e 5) :k 10)

;;;;; adding alist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add*! tbl :a '((:c . 30) (:e . 5)))
  tbl)
H=> (h* :a (h* :c 30 :e 5) :k 10)

;;;;; adding json
(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add*! tbl "a" "{\n  \"c\": 30,\n  \"e\": 5\n}")
  tbl)
H=> (h* "a" (h* "c" 30 "e" 5) "k" 10)

;;;;; adding lol
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" '(("id" "name"  "age")
                         ("b"  "bob"   "30")
                         ("e"  "emily" "21")))
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding orgtbl
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "| id | name  | age |
                        |----+-------+-----|
                        | b  | bob   |  30 |
                        | e  | emily |  21 |")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding tsv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add*! tbl "m" "id\tname\nb\tbob\ne\temily")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "id\tname\tage\nb\tbob\t30\ne\temily\t21")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))


(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "id\tname\tage\nb\tbob\t30\na\talice\t35")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding csv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add*! tbl "m" "id,name\nb,bob\ne,emily")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "id,name,age\nb,bob,30\ne,emily,21")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "id,name,age\nb,bob,30\na,alice,35")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding ssv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add*! tbl "m" "id   name\nb   bob\ne   emily")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "id   name   age\nb   bob   30\ne   emily   21")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add*! tbl "m" "id   name   age\nb   bob   30\na   alice   35")
  tbl)
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))


;;;; Behavior should be like h-put!
;;;;; replace key
(let ((tbl (ht ("a" nil))))
  (h-put-add*! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put-add*! tbl "b" 2)
  tbl)
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put-add*! tbl "a" 2)
  tbl)
H=> (ht ("a" 2))
h-put-add* (table &rest keys-value)

Return a table that is TABLE with VALUE added to KEYS' current.

KEYS-VALUE are KEYS and VALUE.

While h-put* replaces any current value with the newly provided
value, h-put-add* adds to it.

For more information, please see h-put-add!.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This is a side-effect-free function.
Its destructive counterpart is h-put-add*!.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is h-put-add.

For no nesting-awareness but with side-effects, use h-put-add!.

;;;; Can't be done with h-put-add
;; adding the 5 to the 4: a list
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add* tbl :a :c :d 5))
H=> (h* :a (h* :c (h* :d '(5 4))) :k 10)

;; adding (h* :d 5) to (h* :d 4): a hash table
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add* tbl :a :c (h* :d 5)))
H=> (h* :a (h* :c (h* :d 5)) :k 10)

;; adding (h* :e 5) to (h* :d 4): a hash table
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add* tbl :a :c (h* :e 5)))
H=> (h* :a (h* :c (h* :d 4 :e 5)) :k 10)

;; adding plist '(:e 5) to (h* :d 4): a hash table
(let ((tbl (h* :a (h* :c (h* :d 4)) :k 10)))
  (h-put-add* tbl :a :c '(:e 5)))
H=> (h* :a (h* :c (h* :d 4 :e 5)) :k 10)

;;;; Behavior should be different from h-put*
(let ((tbl (ht ("a" 1))))
  (h-put-add* tbl "a" 2))
H=> (ht ("a" '(2 1)))

(let ((tbl (ht ("a" 1) ("k" 10))))
  (h-put-add* tbl "a" 2))
H=> (ht ("a" '(2 1)) ("k" 10))

(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add* tbl "a" 2))
H=> (ht ("a" (ht ("c" 3))) ("k" 10))

;;;;; adding hash table
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add* tbl :a (ht (:d 4))))
H=> (h* :a (h* :c 3 :d 4) :k 10)

(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add* tbl :a (ht (:c 30))))
H=> (h* :a (h* :c 30) :k 10)

;;;;; adding plist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add* tbl :a '(:d 4 :e 5)))
H=> (h* :a (h* :c 3 :d 4 :e 5) :k 10)

;;;;; adding alist
(let ((tbl (ht (:a (ht (:c 3))) (:k 10))))
  (h-put-add* tbl :a '((:c . 30) (:e . 5))))
H=> (h* :a (h* :c 30 :e 5) :k 10)

;;;;; adding json
(let ((tbl (ht ("a" (ht ("c" 3))) ("k" 10))))
  (h-put-add* tbl "a" "{\n  \"c\": 30,\n  \"e\": 5\n}"))
H=> (h* "a" (h* "c" 30 "e" 5) "k" 10)

;;;;; adding lol
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" '(("id" "name"  "age")
                        ("b"  "bob"   "30")
                        ("e"  "emily" "21"))))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding orgtbl
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "| id | name  | age |
                       |----+-------+-----|
                       | b  | bob   |  30 |
                       | e  | emily |  21 |"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

;;;;; adding tsv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add* tbl "m" "id\tname\nb\tbob\ne\temily"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "id\tname\tage\nb\tbob\t30\ne\temily\t21"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "id\tname\tage\nb\tbob\t30\na\talice\t35"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding csv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add* tbl "m" "id,name\nb,bob\ne,emily"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "id,name,age\nb,bob,30\ne,emily,21"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "id,name,age\nb,bob,30\na,alice,35"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))

;;;;; adding ssv
(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id" "a" "name" "alice")))))
  (h-put-add* tbl "m" "id   name\nb   bob\ne   emily"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice")
                "b" (h* "id" "b" "name" "bob")
                "e" (h* "id" "e" "name" "emily")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "id   name   age\nb   bob   30\ne   emily   21"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "42")
                "b" (h* "id" "b" "name" "bob"   "age" "30")
                "e" (h* "id" "e" "name" "emily" "age" "21")))

(let ((tbl (h* "k" 10
               "m" (h* "a" (h* "id"   "a"
                               "name" "alice"
                               "age"  "42")))))
  (h-put-add* tbl "m" "id   name   age\nb   bob   30\na   alice   35"))
H=> (h* "k" 10
        "m" (h* "a" (h* "id" "a" "name" "alice" "age" "35")
                "b" (h* "id" "b" "name" "bob"   "age" "30")))


;;;; Behavior should be like h-put
;;;;; replace key
(let ((tbl (ht ("a" nil))))
  (h-put-add* tbl "a" 2))
H=> (ht ("a" 2))

;;;;; new key
(let ((tbl (ht ("a" 1))))
  (h-put-add* tbl "b" 2))
H=> (ht ("a" 1) ("b" 2))

;;;;; new key from empty
(let ((tbl (ht)))
  (h-put-add* tbl "a" 2))
H=> (ht ("a" 2))
Removal

Functions that remove a key–value pair from tables, either destructively
or side-effects-free. Included are those that take a sequence of keys — for
nested hash tables.

rem

Summary:

  Non-destructive Destructive
simple, doesn't recurse h-rem h-rem!
nesting-aware, recurses h-rem* h-rem*!
h-rem! (table key)

Remove KEY from hash table TABLE.

This is EXACTLY equivalent to current ht-remove!.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-rem.

This function is not nesting-aware.
Its also-destructive but nesting-aware counterpart is h-rem*!.

For nesting-awareness with no side-effects, use h-rem*.

;;;;; replacing nested won't work — this is for h-rem*!
(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem! htbl :a :b :c)
  htbl)
!!> wrong-number-of-arguments

(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem! htbl :a :b)
  htbl)
!!> wrong-number-of-arguments

(let ((htbl (h* :b 1)))
  (h-rem! htbl :a :c)
  htbl)
!!> wrong-number-of-arguments


;;;;; regular
(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem! htbl :a)
  htbl)
H=> (h* :b (h* :c 3
               :d 4))

(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem! htbl :b)
  htbl)
H=> (h* :a 1)

(let ((htbl (h* :a 1)))
  (h-rem! htbl :a)
  htbl)
H=> (ht)

(let ((htbl (h* :b 1)))
  (h-rem! htbl :a)
  htbl)
H=> (h* :b 1)

(let ((htbl (ht)))
  (h-rem! htbl :a)
  htbl)
H=> (ht)
h-rem (table key)

Return a table that is TABLE with KEY removed.

This is a side-effect-free function.
Its destructive counterpart is h-rem!.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is h-rem*.

For nesting-awareness with side-effects, use h-rem*!.

;;;;; replacing nested won't work — this is for h-rem*
(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem htbl :a :b :c))
!!> wrong-number-of-arguments

(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem htbl :a :b))
!!> wrong-number-of-arguments

(let ((htbl (h* :b 1)))
  (h-rem htbl :a :c))
!!> wrong-number-of-arguments


;;;;; regular
(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem htbl :a))
H=> (h* :b (h* :c 3
               :d 4))

(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem htbl :b))
H=> (h* :a 1)

(let ((htbl (h* :a 1)))
  (h-rem htbl :a))
H=> (ht)

(let ((htbl (h* :b 1)))
  (h-rem htbl :a))
H=> (h* :b 1)

(let ((htbl (ht)))
  (h-rem htbl :a))
H=> (ht)
h-rem*! (table &rest keys)

Remove key at end of KEYS sequence from hash table TABLE.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

The last will be removed, together with the value associated with it.

If any of the keys in the sequence KEYS can't be found,
no action is taken.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-rem*.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is h-rem!.

For no nesting-awareness and no side-effects, use h-rem.

;;;;; replacing nested will work here
(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem*! htbl :a :b :c)
  htbl)
H=> (h* :a (h* :b (h* :d 4)))

(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem*! htbl :a :b)
  htbl)
H=> (h* :a (h*))

(let ((htbl (h* :b 1)))
  (h-rem*! htbl :a :c)
  htbl)
H=> (h* :b 1)

;;;;; regular
(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem*! htbl :a)
  htbl)
H=> (h* :b (h* :c 3
               :d 4))

(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem*! htbl :b)
  htbl)
H=> (h* :a 1)

(let ((htbl (h* :a 1)))
  (h-rem*! htbl :a)
  htbl)
H=> (ht)

(let ((htbl (h* :b 1)))
  (h-rem*! htbl :a)
  htbl)
H=> (h* :b 1)

(let ((htbl (ht)))
  (h-rem*! htbl :a)
  htbl)
H=> (ht)
h-rem* (table &rest keys)

Return a table that is TABLE with key at end of KEYS removed.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

The last will be removed, together with the value associated with it.

If any of the keys in the sequence KEYS can't be found,
no action is taken.

This is a side-effect-free function.
Its destructive counterpart is h-rem*!.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is h-rem.

For no nesting-awareness but with side-effects, use h-rem!.

;;;;; replacing nested will work here
(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem* htbl :a :b :c))
H=> (h* :a (h* :b (h* :d 4)))

(let ((htbl (h* :a (h* :b (h* :c 3
                              :d 4)))))
  (h-rem* htbl :a :b))
H=> (h* :a (h*))

(let ((htbl (h* :b 1)))
  (h-rem* htbl :a :c))
H=> (h* :b 1)

;;;;; regular
(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem* htbl :a))
H=> (h* :b (h* :c 3
               :d 4))

(let ((htbl (h* :a 1
                :b (h* :c 3
                       :d 4))))
  (h-rem* htbl :b))
H=> (h* :a 1)

(let ((htbl (h* :a 1)))
  (h-rem* htbl :a))
H=> (ht)

(let ((htbl (h* :b 1)))
  (h-rem* htbl :a))
H=> (h* :b 1)

(let ((htbl (ht)))
  (h-rem* htbl :a))
H=> (ht)
Selection

Functions that create a hash table that has only the specified keys.

sel-keys

Summary:

  Non-destructive Destructive
simple, doesn't recurse h-sel-keys h-sel-keys!
h-sel-keys (table keys)

Return a copy of TABLE with only the pairs specified by KEYS.

KEYS is a list.

This function is similar to current ht-select-keys. The main
difference is that the copy's size is equal to TABLE's instead of
the default 65. All properties are preserved.

This is a side-effect-free function.
Its destructive counterpart is h-sel-keys!.

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-sel-keys htbl '(:a :d :e)))
H=> (h* :a 1 :d 4 :e 5)

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-sel-keys htbl '(:a :x :c :z)))
H=> (h* :a 1 :c 3)

(let ((htbl (h* :a 1)))
  (h-sel-keys htbl '(:x :c :z)))
H=> (h-new)
h-sel-keys! (table keys)

Update TABLE to contain only the pairs specified by KEYS.

KEYS is a list.

This is a destructive function.
Its side-effect-free counterpart is h-sel-keys.

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-sel-keys! htbl '(:a :d :e))
  htbl)
H=> (h* :a 1 :d 4 :e 5)

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-sel-keys! htbl '(:a :x :c :z))
  htbl)
H=> (h* :a 1 :c 3)

(let ((htbl (h* :a 1)))
  (h-sel-keys! htbl '(:x :c :z))
  htbl)
H=> (h-new)
sel

Guideline for the 'sel family' of functions:

  • 'sel' means 'select: a nil in the result of FUN excludes KEY
    from the results.'
  • an added '-' means 'anaphoric: enter a form, not a lambda'
  • an added '*' means 'recurse: apply the values-function or -form
    to the values whenever these are hash tables'
  • an added '!' means 'destructive: modify TABLE instead of
    creating a fresh one'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a fresh table

  a lambda a form (anaphoric)
simple, doesn't recurse h-sel h--sel
nesting-aware, recurses h-sel* h--sel*

Destructive: modify original TABLE, return nil

  a lambda a form (anaphoric)
simple, doesn't recurse h-sel! h--sel!
nesting-aware, recurses h-sel*! h--sel*!
h-sel (fun table)

Return a table with pairs from TABLE for which FUN returns non-nil.

Function FUN is called with two arguments, key and value.

This function is similar to current ht-select.

This is a side-effect-free function.
Its destructive counterpart is h-sel!.
Its anaphoric counterpart is h--sel.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (and (numberp value)
                                (= 2 (% value 10))))))
  (h-sel fun h51))
H=> (h* "b" 2)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-sel fun h31))
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-sel fun h1))
H=> (h-new)
h--sel (form table)

Anaphoric version of h-sel.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is h--sel!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--sel (and (numberp value)
               (= 2 (% value 10)))
          h51))
H=> (h* "b" 2)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--sel (or (equal key   "Bob")
              (equal value "fish"))
          h31))
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--sel (> value 3) h1))
H=> (h-new)
h-sel! (fun table)

Update TABLE by keeping only pairs for which FUN returns non-nil.

Function FUN is called with two arguments, key and value.

This is a destructive function.
Its side-effect-free counterpart is h-sel.
Its anaphoric counterpart is h--sel!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (and (numberp value)
                                (= 2 (% value 10))))))
  (h-sel! fun h51)
  h51)
H=> (h* "b" 2)

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-sel! fun h31)
  h31)
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-sel! fun h1)
  h1)
H=> (h-new)
h--sel! (form table)

Anaphoric version of h-sel!.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is h--sel.

(let ((h51 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--sel! (and (numberp value)
                (= 2 (% value 10)))
           h51)
  h51)
H=> (h* "b" 2)

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--sel! (or (equal key   "Bob")
               (equal value "fish"))
           h31)
  h31)
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--sel! (> value 3) h1)
  h1)
H=> (h-new)
h-sel* (fun table)

Return a table with pairs from TABLE for which FUN returns non-nil.

Function FUN is called with two arguments, key and value.

Just like h-sel, but recurses wherever TABLE is nested.

This means that for any VALUE that is itself a hash table, instead
of returning it as it is when FUNCTION returns non-nil, as h-sel
would do, VALUE will be the very h-sel* recursively applied to
that hash table with the same FUN.

This is a side-effect-free function.
Its destructive counterpart is h-sel*!.
Its anaphoric counterpart is h--sel*.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (or (ht? value)
                               (and (numberp value)
                                    (= 2 (% value 10)))))))
  (h-sel* fun h51))
H=> (h* "b" 2
        "d" (h* "d2" 42
                "e2" 52))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-sel* fun h31))
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-sel* fun h1))
H=> (h-new)
h--sel* (form table)

Anaphoric version of h-sel*.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is h--sel*!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--sel* (or (ht? value)
               (and (numberp value)
                    (= 2 (% value 10))))
           h51))
H=> (h* "b" 2
        "d" (h* "d2" 42
                "e2" 52))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--sel* (or (equal key   "Bob")
               (equal value "fish"))
           h31))
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--sel* (> value 3) h1))
H=> (h-new)
h-sel*! (fun table)

Update TABLE by keeping only pairs for which FUN returns non-nil.

Function FUN is called with two arguments, key and value.

This is a destructive function.
Its side-effect-free counterpart is h-sel*.
Its anaphoric counterpart is h--sel*!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (or (ht? value)
                               (and (numberp value)
                                    (= 2 (% value 10)))))))
  (h-sel*! fun h51)
  h51)
H=> (h* "b" 2
        "d" (h* "d2" 42
                "e2" 52))

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-sel*! fun h31)
  h31)
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-sel*! fun h1)
  h1)
H=> (h-new)
h--sel*! (form table)

Anaphoric version of h-sel*!.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is h--sel*.

(let ((h51 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--sel*! (or (ht? value)
                (and (numberp value)
                     (= 2 (% value 10))))
            h51)
  h51)
H=> (h* "b" 2
        "d" (h* "d2" 42
                "e2" 52))

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--sel*! (or (equal key   "Bob")
                (equal value "fish"))
            h31)
  h31)
H=> (h* "Bob"    "cat"
        "Bubbly" "fish")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--sel*! (> value 3) h1)
  h1)
H=> (h-new)
Rejection

Functions that create a hash table that has all but the specified keys.

rej-keys

Summary:

  Non-destructive Destructive
simple, doesn't recurse h-rej-keys h-rej-keys!
h-rej-keys (table keys)

Return a copy of TABLE with all but the pairs specified by KEYS.

KEYS is a list.

This is a side-effect-free function.
Its destructive counterpart is h-rej-keys!.

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-rej-keys htbl '(:a :d :e)))
H=> (h* :b 2 :c 3 :f 6)

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-rej-keys htbl '(:a :x :c :z)))
H=> (h* :b 2 :d 4 :e 5 :f 6)

(let ((htbl (h* :a 1)))
  (h-rej-keys htbl '(:x :c :z)))
H=> (h* :a 1)
h-rej-keys! (table keys)

Update TABLE by removing the pairs specified by KEYS.

KEYS is a list.

This is a destructive function.
Its side-effect-free counterpart is h-rej-keys.

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-rej-keys! htbl '(:a :d :e))
  htbl)
H=> (h* :b 2 :c 3 :f 6)

(let ((htbl (h* :a 1 :b 2 :c 3 :d 4 :e 5 :f 6)))
  (h-rej-keys! htbl '(:a :x :c :z))
  htbl)
H=> (h* :b 2 :d 4 :e 5 :f 6)

(let ((htbl (h* :a 1)))
  (h-rej-keys! htbl '(:x :c :z))
  htbl)
H=> (h* :a 1)
rej

Guideline for the 'rej family' of functions:

  • 'rej' means 'reject: non-nil in the result of FUN excludes KEY
    from the results.'
  • an added '-' means 'anaphoric: enter a form, not a lambda'
  • an added '*' means 'recurse: apply the values-function or -form
    to the values whenever these are hash tables'
  • an added '!' means 'destructive: modify TABLE instead of
    creating a fresh one'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a fresh table

  a lambda a form (anaphoric)
simple, doesn't recurse h-rej h--rej
nesting-aware, recurses h-rej* h--rej*

Destructive: modify original TABLE, return nil

  a lambda a form (anaphoric)
simple, doesn't recurse h-rej! h--rej!
nesting-aware, recurses h-rej*! h--rej*!
h-rej (fun table)

Return a table with pairs from TABLE for which FUN returns nil.

Function FUN is called with two arguments, key and value.

This function is similar to current ht-reject.

This is a side-effect-free function.
Its destructive counterpart is h-rej!.
Its anaphoric counterpart is h--rej.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (and (numberp value)
                                (= 2 (% value 10))))))
  (h-rej fun h51))
H=> (h* "a" 1 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-rej fun h31))
H=> (h* "Whiskers" "cat")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-rej fun h1))
H=> (h* "a" 1 "b" 2)
h--rej (form table)

Anaphoric version of h-rej.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is h--rej!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--rej (and (numberp value)
               (= 2 (% value 10)))
          h51))
H=> (h* "a" 1 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--rej (or (equal key   "Bob")
              (equal value "fish"))
          h31))
H=> (h* "Whiskers" "cat")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--rej (> value 3) h1))
H=> (h* "a" 1 "b" 2)
h-rej! (fun table)

Update TABLE by keeping only pairs for which FUN returns nil.

Function FUN is called with two arguments, key and value.

This function is similar to current ht-reject!.

This is a destructive function.
Its side-effect-free counterpart is h-rej.
Its anaphoric counterpart is h--rej!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (and (numberp value)
                                (= 2 (% value 10))))))
  (h-rej! fun h51)
  h51)
H=> (h* "a" 1 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-rej! fun h31)
  h31)
H=> (h* "Whiskers" "cat")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-rej! fun h1)
  h1)
H=> (h* "a" 1 "b" 2)
h--rej! (form table)

Anaphoric version of h-rej!.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is h--rej.

(let ((h51 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--rej! (and (numberp value)
                (= 2 (% value 10)))
           h51)
  h51)
H=> (h* "a" 1 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--rej! (or (equal key   "Bob")
               (equal value "fish"))
           h31)
  h31)
H=> (h* "Whiskers" "cat")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--rej! (> value 3) h1)
  h1)
H=> (h* "a" 1 "b" 2)
h-rej* (fun table)

Return a table with pairs from TABLE for which FUN returns nil.

Function FUN is called with two arguments, key and value.

Just like h-rej, but recurses wherever TABLE is nested.

This means that for any VALUE that is itself a hash table, instead
of returning it as it is when FUN returns nil, as h-rej would do,
VALUE will be the very h-rej* recursively applied to that hash
table with the same FUN.

This is a side-effect-free function.
Its destructive counterpart is h-rej*!.
Its anaphoric counterpart is h--rej*.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (or (ht? value)
                               (and (numberp value)
                                    (= 2 (% value 10)))))))
  (h-rej* fun h51))
H=> (h* "a" 1)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-rej* fun h31))
H=> (h* "Whiskers" "cat")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-rej* fun h1))
H=> (h* "a" 1 "b" 2)
h--rej* (form table)

Anaphoric version of h-rej*.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is h--rej*!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--rej* (or (ht? value)
               (and (numberp value)
                    (= 2 (% value 10))))
           h51))
H=> (h* "a" 1)

(let ((h31  (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--rej* (or (equal key   "Bob")
               (equal value "fish"))
           h31))
H=> (h* "Whiskers" "cat")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--rej* (> value 3) h1))
H=> (h* "a" 1 "b" 2)
h-rej*! (fun table)

Update TABLE by keeping only pairs for which FUN returns nil.

Function FUN is called with two arguments, key and value.

This is a destructive function.
Its side-effect-free counterpart is h-rej*.
Its anaphoric counterpart is h--rej*!.

(let ((h51  (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52)))
      (fun (lambda (_key value) (or (ht? value)
                               (and (numberp value)
                                    (= 2 (% value 10)))))))
  (h-rej*! fun h51)
  h51)
H=> (h* "a" 1)

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish"))
      (fun (lambda (key value) (or (equal key   "Bob")
                              (equal value "fish")))))
  (h-rej*! fun h31)
  h31)
H=> (h* "Whiskers" "cat")

(let ((h1  (h* "a" 1 "b" 2))
      (fun (lambda (_key value) (> value 3))))
  (h-rej*! fun h1)
  h1)
H=> (h* "a" 1 "b" 2)
h--rej*! (form table)

Anaphoric version of h-rej*!.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is h--rej*.

(let ((h51 (h* "a" 1 "b" 2 "d" (h* "d1" 41 "d2" 42 "e1" 51 "e2" 52))))
  (h--rej*! (or (ht? value)
                (and (numberp value)
                     (= 2 (% value 10))))
            h51)
  h51)
H=> (h* "a" 1)

(let ((h31 (h* "Bob" "cat" "Whiskers" "cat" "Bubbly" "fish")))
  (h--rej*! (or (equal key   "Bob")
                (equal value "fish"))
            h31)
  h31)
H=> (h* "Whiskers" "cat")

(let ((h1 (h* "a" 1 "b" 2)))
  (h--rej*! (> value 3) h1)
  h1)
H=> (h* "a" 1 "b" 2)
Reverse order

Functions to reverse the order in which the items are stored in
the table.

Summary:

  non-destructive destructive
simple, doesn't recurse h-reverse h-reverse!
nesting-aware, recurses h-reverse* h-reverse*!
h-reverse (table)

Create a hash table identical to TABLE with key order reversed.

(let ((htbl (h* 'a 1 'b (h* :c 3 :d 4 :e 5) 'f (h* :g 7 :h 8))))
  (h-reverse htbl))
H=> (h* 'f (h* :g 7
               :h 8)
        'b (h* :c 3
               :d 4
               :e 5)
        'a 1)

(let ((htbl (h* 'a 1 'b 2 'c 3)))
  (h-reverse htbl))
H=> (h* 'c 3 'b 2 'a 1)

(let ((htbl (h* 'a 1)))
  (h-reverse htbl))
H=> (h* 'a 1)

(let ((htbl (h-new)))
  (h-reverse htbl))
H=> (h-new)
h-reverse! (table)

Reverse the key order of hash table TABLE.

(let ((htbl (h* 'a 1 'b (h* :c 3 :d 4 :e 5) 'f (h* :g 7 :h 8))))
  (h-reverse! htbl)
  htbl)
H=> (h* 'f (h* :g 7 :h 8) 'b (h* :c 3 :d 4 :e 5) 'a 1)

(let ((htbl (h* 'a 1 'b 2 'c 3)))
  (h-reverse! htbl)
  htbl)
H=> (h* 'c 3 'b 2 'a 1)

(let ((htbl (h* 'a 1)))
  (h-reverse! htbl)
  htbl)
H=> (h* 'a 1)

(let ((htbl (h-new)))
  (h-reverse! htbl)
  htbl)
H=> (h-new)
h-reverse* (table)

Create a hash table identical to TABLE with key order reversed.

When TABLE is nested, do the same to all values that are hash
tables.

(let ((htbl (h* 'a 1 'b (h* :c 3 :d 4 :e 5) 'f (h* :g 7 :h 8))))
  (h-reverse* htbl))
H=> (h* 'f (h* :h 8
               :g 7)
        'b (h* :e 5
               :d 4
               :c 3)
        'a 1)

(let ((htbl (h* 'a 1 'b 2 'c 3)))
  (h-reverse* htbl))
H=> (h* 'c 3 'b 2 'a 1)

(let ((htbl (h* 'a 1)))
  (h-reverse* htbl))
H=> (h* 'a 1)

(let ((htbl (h-new)))
  (h-reverse* htbl))
H=> (h-new)
h-reverse*! (table)

Reverse the key order of hash table TABLE.

When TABLE is nested, do the same to all values that are hash
tables.

(let ((htbl (h* 'a 1 'b (h* :c 3 :d 4 :e 5) 'f (h* :g 7 :h 8))))
  (h-reverse*! htbl)
  htbl)
H=> (h* 'f (h* :h 8
               :g 7)
        'b (h* :e 5
               :d 4
               :c 3)
        'a 1)

(let ((htbl (h* 'a 1 'b 2 'c 3)))
  (h-reverse*! htbl)
  htbl)
H=> (h* 'c 3 'b 2 'a 1)

(let ((htbl (h* 'a 1)))
  (h-reverse*! htbl)
  htbl)
H=> (h* 'a 1)

(let ((htbl (h-new)))
  (h-reverse*! htbl)
  htbl)
H=> (h-new)
Change numerical value, including #'1+ and #'1-

Functions to alter values of keys when these values are numeric or
number-like.

Summary:

  Non-destructive Destructive
Generic h-put-num-with* h-put-num-with*!
Increase by 1 h-put-inc* h-put-inc*!
Decrease by 1 h-put-dec* h-put-dec*!
h-put-num-with* (fun table &rest keys)

Return copy of TABLE with FUN applied to numeric value of KEYS.

KEYS is a chain of keys to access the key of an internal hash table
if TABLE is nested — just as with h-get*. If it's a simple,
non-nested TABLE, or if the value is at the 'first level', enter
just one key.

FUN is a unary function that can be applied to numerical values.
These are some examples: 1+, 1-, sqrt, log10.

You may also create partial application ones, such as:

(-partial '+ 10)
(-partial '* 2)

or:

(lambda (x) (expt x 2))

The value must, of course, be a number — but the function is
flexible about what a number is. If it quacks like a number, we
increase it. So it may be found stored also as a string or keyword,
and it will be increased while maintaining the type. Moreover, it
tries to preserve original leading zeros when they were there in
strings, keywords, or symbols.

So "041", receiving #'1+, becomes "042", and so on.

When value is nil or key is not found, interpret it as being 0.

The function uses base 10.

This is a side-effect-free function.
Its destructive counterpart is h-put-num-with*!.

;;;; Integers
(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-num-with* #'1+ htbl 'a))
H=> (h* 'a 2 'b (h* :c 3 :d 4))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-num-with* #'1+ htbl 'b :d))
H=> (h* 'a 1 'b (h* :c 3 :d 5))

;;;; Floats
(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-num-with* #'1+ htbl 'a))
H=> (h* 'a 2.1 'b (h* :c 3.1 :d 3.2))

(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-num-with* (-partial '/ 32) htbl 'b :d))
H=> (h* 'a 1.1 'b (h* :c 3.1 :d 10.0))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-num-with* #'sqrt htbl 'b :d))
H=> (h* 'a 1 'b (h* :c 3 :d 2.0))

;;;; Strings
(let* ((htbl (h* 'a "006" 'b (h* :c "3" :d "4"))))
  (h-put-num-with* #'1+ htbl 'a))
H=> (h* 'a "007" 'b (h* :c "3" :d "4"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-num-with* #'sqrt htbl 'b :d))
H=> (h* 'a "1" 'b (h* :c "3" :d "2.0"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "003"))))
  (h-put-num-with* (-partial '* 10) htbl 'b :d))
H=> (h* 'a "1" 'b (h* :c "3" :d "030"))

;;;; Keywords
(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-num-with* (lambda (x) (expt (+ 3 x) 2)) ;; (x+3)²
                   htbl 'a))
H=> (h* 'a :16 'b (h* :c :3 :d :4))

(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :004))))
  (h-put-num-with* (-partial '+ 38) htbl 'b :d))
H=> (h* 'a :1 'b (h* :c :3 :d :042))

;;;; New values and nil
(let* ((htbl (h-new)))
  (h-put-num-with* #'1- htbl 'b))
H=> (h* 'b -1)

(let* ((htbl (h* "a" 1)))
  (h-put-num-with* #'1- htbl "b"))
H=> (h* "a" 1 "b" -1)

(let* ((htbl (h* "a" 1 "b" nil)))
  (h-put-num-with* #'1- htbl "b"))
H=> (h* "a" 1 "b" -1)

;;;; Not number-like
(let* ((htbl (h* 'a "A")))
  (h-put-num-with* #'1- htbl 'a))
!!> error
h-put-num-with*! (fun table &rest keys)

Apply FUN to the numeric value of KEYS in possibly nested hash TABLE.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-put-num-with*.

;;;; Integers
(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-num-with*! #'1+ htbl 'a)
  htbl)
H=> (h* 'a 2 'b (h* :c 3 :d 4))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-num-with*! #'1+ htbl 'b :d)
  htbl)
H=> (h* 'a 1 'b (h* :c 3 :d 5))

;;;; Floats
(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-num-with*! #'1+ htbl 'a)
  htbl)
H=> (h* 'a 2.1 'b (h* :c 3.1 :d 3.2))

(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-num-with*! (-partial '/ 32) htbl 'b :d)
  htbl)
H=> (h* 'a 1.1 'b (h* :c 3.1 :d 10.0))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-num-with*! #'sqrt htbl 'b :d)
  htbl)
H=> (h* 'a 1 'b (h* :c 3 :d 2.0))

;;;; Strings
(let* ((htbl (h* 'a "006" 'b (h* :c "3" :d "4"))))
  (h-put-num-with*! #'1+ htbl 'a)
  htbl)
H=> (h* 'a "007" 'b (h* :c "3" :d "4"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-num-with*! #'sqrt htbl 'b :d)
  htbl)
H=> (h* 'a "1" 'b (h* :c "3" :d "2.0"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "003"))))
  (h-put-num-with*! (-partial '* 10) htbl 'b :d)
  htbl)
H=> (h* 'a "1" 'b (h* :c "3" :d "030"))

;;;; Keywords
(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-num-with*! (lambda (x) (expt (+ 3 x) 2)) ;; (x+3)²
                    htbl 'a)
  htbl)
H=> (h* 'a :16 'b (h* :c :3 :d :4))

(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :004))))
  (h-put-num-with*! (-partial '+ 38) htbl 'b :d)
  htbl)
H=> (h* 'a :1 'b (h* :c :3 :d :042))

;;;; New values and nil
(let* ((htbl (h-new)))
  (h-put-num-with*! #'1- htbl 'b)
  htbl)
H=> (h* 'b -1)

(let* ((htbl (h* "a" 1)))
  (h-put-num-with*! #'1- htbl "b")
  htbl)
H=> (h* "a" 1 "b" -1)

(let* ((htbl (h* "a" 1 "b" nil)))
  (h-put-num-with*! #'1- htbl "b")
  htbl)
H=> (h* "a" 1 "b" -1)

;;;; Not number-like
(let* ((htbl (h* 'a "A")))
  (h-put-num-with*! #'1- htbl 'a)
  htbl)
!!> error
h-put-inc* (table &rest keys)

Return a copy of TABLE with 1 added to the value of KEYS.

KEYS is a chain of keys to access the key of an internal hash table
if TABLE is nested — just as with h-get*. If it's a simple,
non-nested TABLE, or if the value is at the 'first level', enter
just one key.

The value must, of course, be a number — but the function is
flexible about what a number is. If it quacks like a number, we
increase it. So it may be found stored also as a string or keyword,
and it will be increased while maintaining the type. Moreover, it
tries to preserve original leading zeros when they were there in
strings, keywords, or symbols.

So "041" becomes "042", and so on.

When value is nil or key is not found, interpret it as being 0.

The function uses base 10.

This is a side-effect-free function.
Its destructive counterpart is h-put-inc*!.

;;;; Integers
(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-inc* htbl 'a))
H=> (h* 'a 2 'b (h* :c 3 :d 4))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-inc* htbl 'b :d))
H=> (h* 'a 1 'b (h* :c 3 :d 5))

;;;; Floats
(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-inc* htbl 'a))
H=> (h* 'a 2.1 'b (h* :c 3.1 :d 3.2))

(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-inc* htbl 'b :d))
H=> (h* 'a 1.1 'b (h* :c 3.1 :d 4.2))

;;;; Strings
(-> "| id   | name  | age |
     |------+-------+-----|
     | id01 | Alice |  41 |
     | id02 | Bob   |  30 |
     | id03 | Emily |  21 |"
    h<-orgtbl  (h-put-inc* "id01" "age")  h->orgtbl)
O=> "| id   | name  | age |
     |------+-------+-----|
     | id01 | Alice |  42 |
     | id02 | Bob   |  30 |
     | id03 | Emily |  21 |"
;; (yes, I know that there are other ways to manipulate numbers in
;; org tables — but this works, too, and it's a nice example)

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-inc* htbl 'a))
H=> (h* 'a "2" 'b (h* :c "3" :d "4"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-inc* htbl 'b :d))
H=> (h* 'a "1" 'b (h* :c "3" :d "5"))

;;;; Keywords
(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-inc* htbl 'a))
H=> (h* 'a :2 'b (h* :c :3 :d :4))

(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-inc* htbl 'b :d))
H=> (h* 'a :1 'b (h* :c :3 :d :5))

;;;; New values and nil
(let* ((htbl (h-new)))
  (h-put-inc* htbl 'b))
H=> (h* 'b 1)

(let* ((htbl (h* "a" 1)))
  (h-put-inc* htbl "b"))
H=> (h* "a" 1 "b" 1)

(let* ((htbl (h* "a" 1 "b" nil)))
  (h-put-inc* htbl "b"))
H=> (h* "a" 1 "b" 1)

;;;; Not number-like
(let* ((htbl (h* 'a "A")))
  (h-put-inc* htbl 'a))
!!> error
h-put-inc*! (table &rest keys)

Add 1 to the value of KEYS in possibly nested hash TABLE.

See h-put-inc* for more information.

This function returns nil.

;;;; Integers
(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-inc*! htbl 'a)
  htbl)
H=> (h* 'a 2 'b (h* :c 3 :d 4))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-inc*! htbl 'b :d)
  htbl)
H=> (h* 'a 1 'b (h* :c 3 :d 5))

;;;; Floats
(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-inc*! htbl 'a)
  htbl)
H=> (h* 'a 2.1 'b (h* :c 3.1 :d 3.2))

(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-inc*! htbl 'b :d)
  htbl)
H=> (h* 'a 1.1 'b (h* :c 3.1 :d 4.2))

;;;; Strings
(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-inc*! htbl 'a)
  htbl)
H=> (h* 'a "2" 'b (h* :c "3" :d "4"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-inc*! htbl 'b :d)
  htbl)
H=> (h* 'a "1" 'b (h* :c "3" :d "5"))

;;;; Keywords
(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-inc*! htbl 'a)
  htbl)
H=> (h* 'a :2 'b (h* :c :3 :d :4))

(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-inc*! htbl 'b :d)
  htbl)
H=> (h* 'a :1 'b (h* :c :3 :d :5))

;;;; New values and nil
(let* ((htbl (h-new)))
  (h-put-inc*! htbl 'b)
  htbl)
H=> (h* 'b 1)

(let* ((htbl (h* "a" 1)))
  (h-put-inc*! htbl "b")
  htbl)
H=> (h* "a" 1 "b" 1)

(let* ((htbl (h* "a" 1 "b" nil)))
  (h-put-inc*! htbl "b")
  htbl)
H=> (h* "a" 1 "b" 1)

;;;; Not number-like
(let* ((htbl (h* 'a "A")))
  (h-put-inc*! htbl 'a)
  htbl)
!!> error
h-put-dec* (table &rest keys)

Return a copy of TABLE with 1 subtracted from the value of KEYS.

KEYS is a chain of keys to access the key of an internal hash table
if TABLE is nested — just as with h-get*. If it's a simple,
non-nested TABLE, or if the value is at the 'first level', enter
just one key.

The value must, of course, be a number — but the function is
flexible about what a number is. If it quacks like a number, we
decrease it. So it may be found stored also as a string or keyword,
and it will be increased while maintaining the type. Moreover, it
tries to preserve original leading zeros when they were there in
strings, keywords, or symbols.

So "043" becomes "042", and so on.

The function uses base 10.

This is a side-effect-free function.
Its destructive counterpart is h-put-dec*!.

;;;; Integers
(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-dec* htbl 'a))
H=> (h* 'a 0 'b (h* :c 3 :d 4))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-dec* htbl 'b :d))
H=> (h* 'a 1 'b (h* :c 3 :d 3))

;;;; Floats
(let* ((htbl (h* 'a 5.2 'b (h* :c 3.1 :d 3.2))))
  (h-put-dec* htbl 'a))
H=> (h* 'a 4.2 'b (h* :c 3.1 :d 3.2))

(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-dec* htbl 'b :d))
H=> (h* 'a 1.1 'b (h* :c 3.1 :d 2.2))

;;;; Strings
(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-dec* htbl 'a))
H=> (h* 'a "0" 'b (h* :c "3" :d "4"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-dec* htbl 'b :d))
H=> (h* 'a "1" 'b (h* :c "3" :d "3"))

;;;; Keywords
(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-dec* htbl 'a))
H=> (h* 'a :0 'b (h* :c :3 :d :4))

(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-dec* htbl 'b :d))
H=> (h* 'a :1 'b (h* :c :3 :d :3))

;;;; New values and nil
(let* ((htbl (h-new)))
  (h-put-dec* htbl 'b))
H=> (h* 'b -1)

(let* ((htbl (h* "a" 1)))
  (h-put-dec* htbl "b"))
H=> (h* "a" 1 "b" -1)

(let* ((htbl (h* "a" 1 "b" nil)))
  (h-put-dec* htbl "b"))
H=> (h* "a" 1 "b" -1)

;;;; Not number-like
(let* ((htbl (h* 'a "A")))
  (h-put-dec* htbl 'a))
!!> error
h-put-dec*! (table &rest keys)

Subtract 1 from the value of KEYS in possibly nested hash TABLE.

See h-put-dec* for more information.

This function returns nil.

;;;; Integers
(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-dec*! htbl 'a)
  htbl)
H=> (h* 'a 0 'b (h* :c 3 :d 4))

(let* ((htbl (h* 'a 1 'b (h* :c 3 :d 4))))
  (h-put-dec*! htbl 'b :d)
  htbl)
H=> (h* 'a 1 'b (h* :c 3 :d 3))

;;;; Floats
(let* ((htbl (h* 'a 5.2 'b (h* :c 3.1 :d 3.2))))
  (h-put-dec*! htbl 'a)
  htbl)
H=> (h* 'a 4.2 'b (h* :c 3.1 :d 3.2))

(let* ((htbl (h* 'a 1.1 'b (h* :c 3.1 :d 3.2))))
  (h-put-dec*! htbl 'b :d)
  htbl)
H=> (h* 'a 1.1 'b (h* :c 3.1 :d 2.2))

;;;; Strings
(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-dec*! htbl 'a)
  htbl)
H=> (h* 'a "0" 'b (h* :c "3" :d "4"))

(let* ((htbl (h* 'a "1" 'b (h* :c "3" :d "4"))))
  (h-put-dec*! htbl 'b :d)
  htbl)
H=> (h* 'a "1" 'b (h* :c "3" :d "3"))

;;;; Keywords
(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-dec*! htbl 'a)
  htbl)
H=> (h* 'a :0 'b (h* :c :3 :d :4))

(let* ((htbl (h* 'a :1 'b (h* :c :3 :d :4))))
  (h-put-dec*! htbl 'b :d)
  htbl)
H=> (h* 'a :1 'b (h* :c :3 :d :3))

;;;; New values and nil
(let* ((htbl (h-new)))
  (h-put-dec*! htbl 'b)
  htbl)
H=> (h* 'b -1)

(let* ((htbl (h* "a" 1)))
  (h-put-dec*! htbl "b")
  htbl)
H=> (h* "a" 1 "b" -1)

(let* ((htbl (h* "a" 1 "b" nil)))
  (h-put-dec*! htbl "b")
  htbl)
H=> (h* "a" 1 "b" -1)

;;;; Not number-like
(let* ((htbl (h* 'a "A")))
  (h-put-dec*! htbl 'a)
  htbl)
!!> error
Miscellaneous key, value, pair lookups

Other functions about checking keys, values, and pairs.

Has this key, this value, or this pair at some level?
h-has-key? (table key)

Return t if TABLE has KEY.

This function is just like current ht-contains?

(h-has-key? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
            "c")
=> t

(h-has-key? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
            "f")
=> nil

(h-has-key? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
            "d")
=> nil

(h-has-key? (h* "d" 4 "e" 5)
            "d")
=> t

(h-has-key? (h-new) "a")
=> nil

(let ((my-ht-1 (h* :a 1 :b 2))
      (my-ht-2 (h* :a 1 :b 2 :c nil))
      (my-ht-3 (h* :a 1 :b 2 :c 3))
      (my-ht-4 (h* :a 1 :b 2 :c "xht--not-found"))
      (my-ht-5 (h* :a 1 :b 2 :c 'xht--not-found))
      (my-ht-6 (h* :a 1 :b 2 :c (make-symbol "xht--not-found")))
      (my-ht-7 (h* :a 1 :b (h*)))
      (my-ht-8 (h* :a 1 :b (h* :c nil)))
      (my-ht-9 (h* :a 1 :b (h* :c 3)))
      (my-ht-A (h* :a 1 :b (h* :c "xht--not-found")))
      (my-ht-B (h* :a 1 :b (h* :c 'xht--not-found)))
      (my-ht-C (h* :a 1 :b (h* :c (make-symbol "xht--not-found")))))
  (--map (h-has-key? it :c)
         (list my-ht-1 my-ht-2 my-ht-3 my-ht-4 my-ht-5 my-ht-6
               my-ht-7 my-ht-8 my-ht-9 my-ht-A my-ht-B my-ht-C)))
=> '(nil t t t t t nil nil nil nil nil nil)
h-has-key*? (table key)

Whether possibly-nested TABLE has KEY at some level.

Similar to h-has-key?, but if TABLE is nested look for KEY also
in the internal hash tables of TABLE.

(h-has-key*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "c")
=> t

(h-has-key*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "f")
=> nil

(h-has-key*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "d")
=> t

(h-has-key*? (h* "d" 4 "e" 5)
             "d")
=> t

(h-has-key*? (h-new) "a")
=> nil

(let ((my-ht-1 (h* :a 1 :b 2))
      (my-ht-2 (h* :a 1 :b 2 :c nil))
      (my-ht-3 (h* :a 1 :b 2 :c 3))
      (my-ht-4 (h* :a 1 :b 2 :c "xht--not-found"))
      (my-ht-5 (h* :a 1 :b 2 :c 'xht--not-found))
      (my-ht-6 (h* :a 1 :b 2 :c (make-symbol "xht--not-found")))
      (my-ht-7 (h* :a 1 :b (h*)))
      (my-ht-8 (h* :a 1 :b (h* :c nil)))
      (my-ht-9 (h* :a 1 :b (h* :c 3)))
      (my-ht-A (h* :a 1 :b (h* :c "xht--not-found")))
      (my-ht-B (h* :a 1 :b (h* :c 'xht--not-found)))
      (my-ht-C (h* :a 1 :b (h* :c (make-symbol "xht--not-found")))))
  (--map (h-has-key*? it :c)
         (list my-ht-1 my-ht-2 my-ht-3 my-ht-4 my-ht-5 my-ht-6
               my-ht-7 my-ht-8 my-ht-9 my-ht-A my-ht-B my-ht-C)))
=> '(nil t t t t t nil t t t t t)
h-has-value? (table value)

Return t if TABLE has some key whose value is VALUE.

(h-has-value? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              2)
=> t

(h-has-value? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              7)
=> nil

(h-has-value? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              4)
=> nil

(h-has-value? (h* "d" 4 "e" 5)
              7)
=> nil

(h-has-value? (h-new) 1)
=> nil

(let ((my-ht-1 (h* :a 1 :b 2))
      (my-ht-2 (h* :a 1 :b 2 :c 3))
      (my-ht-3 (h* :a 3 :b 2 :c 1))
      (my-ht-4 (h* :a 3 :b 2 :c 3))
      (my-ht-5 (h* :a 1 :b (h* :c 3 :d 4)))
      (my-ht-6 (h* :a 1 :b (h* :c 4 :d 3)))
      (my-ht-7 (h* :a 1 :b (h* :d (h* :c 3))))
      (my-ht-8 (h* :a 1 :b (h* :d (h* :c 4))))
      (my-ht-9 (h* :a 3 :b (h* :d (h* :c 4)))))
  (--map (h-has-value? it 3)
         (list my-ht-1 my-ht-2 my-ht-3 my-ht-4 my-ht-5
               my-ht-6 my-ht-7 my-ht-8 my-ht-9)))
=> '(nil t t t nil nil nil nil t)
h-has-value*? (table value)

Whether possibly-nested TABLE has at some level key–VALUE pair.

Similar to h-has-value?, but if TABLE is nested look for VALUE
also in the internal hash tables of TABLE.

(h-has-value*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
               2)
=> t

(h-has-value*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
               4)
=> t

(h-has-value*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
               7)
=> nil

(h-has-value*? (h* "d" 4 "e" 5)
               7)
=> nil

(h-has-value*? (h-new) 1)
=> nil
(let ((my-ht-1 (h* :a 1 :b 2))
      (my-ht-2 (h* :a 1 :b 2 :c 3))
      (my-ht-3 (h* :a 3 :b 2 :c 1))
      (my-ht-4 (h* :a 3 :b 2 :c 3))
      (my-ht-5 (h* :a 1 :b (h* :c 3 :d 4)))
      (my-ht-6 (h* :a 1 :b (h* :c 4 :d 3)))
      (my-ht-7 (h* :a 1 :b (h* :d (h* :c 3))))
      (my-ht-8 (h* :a 1 :b (h* :d (h* :c 4))))
      (my-ht-9 (h* :a 3 :b (h* :d (h* :c 4)))))
  (--map (h-has-value*? it 3)
         (list my-ht-1 my-ht-2 my-ht-3 my-ht-4 my-ht-5
               my-ht-6 my-ht-7 my-ht-8 my-ht-9)))
=> '(nil t t t t t t nil t)
h-has-pair? (table key value)

Return t if TABLE has KEY and its value is VALUE.

(h-has-pair? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "a" 1)
=> t

(h-has-pair? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "a" 2)
=> nil

(h-has-pair? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "f" 6)
=> nil

(h-has-pair? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
             "d" 4)
=> nil

(h-has-pair? (h* "d" 4 "e" 5)
             "d" 4)
=> t

(h-has-pair? (h-new) "a" 1)
=> nil

(let ((my-ht-1 (h* :a 1 :b 2))
      (my-ht-2 (h* :a 1 :b 2 :c 3))
      (my-ht-3 (h* :a 3 :b 2 :c 1))
      (my-ht-4 (h* :a 3 :b 2 :c 3))
      (my-ht-5 (h* :a 1 :b (h* :c 3 :d 4)))
      (my-ht-6 (h* :a 1 :b (h* :c 4 :d 3)))
      (my-ht-7 (h* :a 1 :b (h* :d (h* :c 3))))
      (my-ht-8 (h* :a 1 :b (h* :d (h* :c 4))))
      (my-ht-9 (h* :a 3 :b (h* :d (h* :c 4)))))
  (--map (h-has-pair? it :c 3)
         (list my-ht-1 my-ht-2 my-ht-3 my-ht-4 my-ht-5
               my-ht-6 my-ht-7 my-ht-8 my-ht-9)))
=> '(nil t nil t nil nil nil nil nil)
h-has-pair*? (table key value)

Whether possibly-nested TABLE has at some level KEY–VALUE pair.

Similar to h-has-pair?, but if TABLE is nested look for KEY–VALUE
pair also in the internal hash tables of TABLE.

(h-has-pair*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              "a" 1)
=> t

(h-has-pair*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              "a" 2)
=> nil

(h-has-pair*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              "f" 6)
=> nil

(h-has-pair*? (h* "a" 1 "b" 2 "c" (h* "d" 4 "e" 5))
              "d" 4)
=> t

(h-has-pair*? (h* "d" 4 "e" 5)
              "d" 4)
=> t

(h-has-pair*? (h-new) "a" 1)
=> nil

(let ((my-ht-1 (h* :a 1 :b 2))
      (my-ht-2 (h* :a 1 :b 2 :c 3))
      (my-ht-3 (h* :a 3 :b 2 :c 1))
      (my-ht-4 (h* :a 3 :b 2 :c 3))
      (my-ht-5 (h* :a 1 :b (h* :c 3 :d 4)))
      (my-ht-6 (h* :a 1 :b (h* :c 4 :d 3)))
      (my-ht-7 (h* :a 1 :b (h* :d (h* :c 3))))
      (my-ht-8 (h* :a 1 :b (h* :d (h* :c 4))))
      (my-ht-9 (h* :a 3 :b (h* :d (h* :c 4)))))
  (--map (h-has-pair*? it :c 3)
         (list my-ht-1 my-ht-2 my-ht-3 my-ht-4 my-ht-5
               my-ht-6 my-ht-7 my-ht-8 my-ht-9)))
=> '(nil t nil t t nil t nil nil)
Has all these keys, these values, or these pairs?
h-has-keys? (table keys)

Return t if TABLE has all KEYS.

KEYS is either a single key or a list of keys.

(h-has-keys? (h* :a 1 :b 2 :c 3) '(:a :c)) => t
(h-has-keys? (h* :a 1 :b 2 :c 3) '(:a :d)) => nil
(h-has-keys? (h* :a 1 :b 2 :c 3) '(:a))    => t
(h-has-keys? (h* :a 1 :b 2 :c 3) '(:e))    => nil
(h-has-keys? (h* :a 1 :b 2 :c 3) '())      => t
(h-has-keys? (h* :a 1 :b 2 :c 3) :a)       => t
(h-has-keys? (h* :a 1 :b 2 :c 3) :e)       => nil
(h-has-keys? (h-new)             '(:a))    => nil
;; nope: either enter 1 key or a list of keys:
(h-has-keys? (h* :a 1 :b 2 :c 3) :a :b)
!!> wrong-number-of-arguments
h-has-values? (table values)

Return t if TABLE has all VALUES.

VALUES is either a single value or a list of values.

(h-has-values? (h* :a 1 :b 2 :c 3) '(1 2)) => t
(h-has-values? (h* :a 1 :b 2 :c 3) '(1 4)) => nil
(h-has-values? (h* :a 1 :b 2 :c 3) '(1))   => t
(h-has-values? (h* :a 1 :b 2 :c 3) '(5))   => nil
(h-has-values? (h* :a 1 :b 2 :c 3) '())    => t
(h-has-values? (h* :a 1 :b 2 :c 3) 1)      => t
(h-has-values? (h* :a 1 :b 2 :c 3) 5)      => nil
(h-has-values? (h-new)             '(1))   => nil
;; nope: either enter 1 value or a list of values:
(h-has-values? (h* :a 1 :b 2 :c 3) 1 2)
!!> wrong-number-of-arguments
h-has-pairs? (table pairs)

Return t if TABLE has all PAIRS.

PAIRS is either a single pair (a two-item list) or a list of such
two-item lists.

(h-has-pairs? (h* :a 1 :b 2 :c 3) '((:a 1) (:c 3))) => t
(h-has-pairs? (h* :a 1 :b 2 :c 3) '((:a 1) (:d 4))) => nil
(h-has-pairs? (h* :a 1 :b 2 :c 3) '((:a 1)))        => t
(h-has-pairs? (h* :a 1 :b 2 :c 3) '((:e 5)))        => nil
(h-has-pairs? (h* :a 1 :b 2 :c 3) '())              => t
(h-has-pairs? (h* :a 1 :b 2 :c 3) '(:a 1))          => t
(h-has-pairs? (h* :a 1 :b 2 :c 3) '(:e 5))          => nil
(h-has-pairs? (h-new)             '(:a 1))          => nil
;; nope: either enter 1 pair or a list of pairs:
(h-has-pairs? (h* :a 1 :b 2 :c 3) '(:a 1) '(:b 2))
!!> wrong-number-of-arguments
Multi-table operations

Functions that act on two or more hash tables simultaneously.

XHT loves short function names.

Sometimes, however, their meaning might not be obvious at first
sight. Thankfully, this is easy to fix — and by third sight they'll
already be familiar to you.

With this table, it'll take you but a minute to figure out the
meaning of the multi-table functions you'll see next:

Function Implying... Operation
h-mix Mixing the tables together Union
h-dif Difference between tables Difference
h-cmn Commonality between tables Intersection

Easy?

Let's see them.

Union

Functions that combine tables, either destructively or side-effects-free.

Note that:

  • An '*' means it works with nesting; its absence means only first level.
  • A '!' means destructive: it changes the original table; its absence
    means that a new table is created, free of side-effects to the original.
  • When input has multiple tables, the first one is 'updated' with the
    others, which are processed from left to right. When not destructive,
    a shallow copy of the first is updated.

    So you can always think of it as: values of table1 updated with those of
    table2; the result updated with those of table3; the result updated with
    those of table4; and so on. The difference is that when there's a '!'
    table1 will be itself modified; otherwise not.

  • Functions for side-effect ('!') return nil; otherwise, the (1st) table.
mix

The destructive h-mix! is similar to ht library's ht-update!, whereas
the side-effect-free h-mix is similar to ht-merge.

h-mix! (table &rest from-tables)

Update TABLE according to every key–value pair in FROM-TABLES.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-mix! table1 table2 table3 table4) will:

  • modify table1 with the data from table2; then
  • modify table1 with the data from table3; then
  • modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-mix.

This function only works at 'the first level' — no nesting.
Its also-destructive nesting-aware counterpart is h-mix*!.

For nesting-awareness but no side-effects, use h-mix*.

;;;; works like ht's ht-update! — not aware of nesting
(let* ((h1 (ht (:z 'a)
               (:y (ht (:x 4)
                       (:b 2)))))
       (h2 (ht (:y (ht (:b 3))))))
  (ht-update! h1 h2)
  h1)
H=>  (h* :z 'a
         :y (h* :b 3)) ;<-- all of it replaced rather than merged

(let* ((h1 (ht (:z 'a)
               (:y (ht (:x 4)
                       (:b 2)))))
       (h2 (ht (:y (ht (:b 3))))))
  (h-mix! h1 h2)
  h1)
H=>  (h* :z 'a
         :y (h* :b 3)) ;<-- all of it replaced rather than merged

;;;; typical
(let ((t1 (h* :a 1  :b (h* :c 3 :d 4)))
      (t2 (h* :e 5  :b 2))
      (t3 (h* :e 50 :f 6)))
  (h-mix! t1 t2 t3)
  t1)
H=> (h* :a 1 :b 2 :e 50 :f 6)

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b 2)))
  (h-mix! t1 t2)
  t1)
H=> (h* :a 1 :b 2 :e 5)

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b 2)))
  (h-mix! t1 t2)
  t2)
H=> (h* :e 5 :b 2) ;; t2 is intact

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b (h* :c 30 :f 6))))
  (h-mix! t1 t2)
  t1)
;; note: no :d 4 -- value replaced at level 2
H=> (h* :a 1 :e 5 :b (h* :c 30 :f 6))

;;;; some base cases
(let ((t1 (h* :a 1))
      (t2 (ht)))
  (h-mix! t1 t2)
  t1)
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (h* :a 1)))
  (h-mix! t1 t2)
  t1)
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (ht)))
  (h-mix! t1 t2)
  t1)
H=> (ht)
h-mix (table &rest from-tables)

Return a table that has all key–value pairs from the tables.

The tables (TABLE plus FROM-TABLES) are processed from left to
right, and no table is modified.

So (h-mix table1 table2 table3 table4) will:

  • create a clone of table1 (let's call it 'table0')
  • modify table0 with the data from table2; then
  • modify table0 with the data from table3; then
  • modify table0 with the data from table4;
  • return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is h-mix!.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is h-mix*.

For nesting-awareness with side-effects, use h-mix*!.

;;;; works like ht's ht-merge — not aware of nesting
(let* ((h1 (ht (:z 'a)
               (:y (ht (:x 4)
                       (:b 2)))))
       (h2 (ht (:y (ht (:b 3))))))
  (ht-merge h1 h2))
H=>  (h* :z 'a
         :y (h* :b 3)) ;<-- all of it replaced rather than merged

(let* ((h1 (ht (:z 'a)
               (:y (ht (:x 4)
                       (:b 2)))))
       (h2 (ht (:y (ht (:b 3))))))
  (h-mix h1 h2))
H=>  (h* :z 'a
         :y (h* :b 3)) ;<-- all of it replaced rather than merged

;;;; typical
(let ((t1 (h* :a 1  :b (h* :c 3 :d 4)))
      (t2 (h* :e 5  :b 2))
      (t3 (h* :e 50 :f 6)))
  (h-mix t1 t2 t3))
H=> (h* :a 1 :b 2 :e 50 :f 6)

(let* ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
       (t2 (h* :e 5 :b 2)))
  (h-mix t1 t2))
H=> (h* :a 1 :b 2 :e 5)

(let* ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
       (t2 (h* :e 5 :b 2)))
  (h-mix t1 t2)
  t1)
H=> (h* :a 1 :b (h* :c 3 :d 4)) ;; t1 is intact

(let* ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
       (t2 (h* :e 5 :b 2)))
  (h-mix t1 t2)
  t2)
H=> (h* :e 5 :b 2) ;; t2 is intact

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b (h* :c 30 :f 6))))
  (h-mix t1 t2))
;; note: no :d 4 -- value replaced at level 2
H=> (h* :a 1 :e 5 :b (h* :c 30 :f 6))

;;;; some base cases
(let ((t1 (h* :a 1))
      (t2 (ht)))
  (h-mix t1 t2))
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (h* :a 1)))
  (h-mix t1 t2))
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (ht)))
  (h-mix t1 t2))
H=> (ht)
h-mix*! (table &rest from-tables)

Update TABLE by adding every key–value pair in FROM-TABLES.

Unlike h-mix! and ht-update!, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the h-mix*! of them.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-mix! table1 table2 table3 table4) will:

  • modify table1 with the data from table2; then
  • modify table1 with the data from table3; then
  • modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-mix*.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is h-mix!.

For no nesting-awareness and no side-effects, use h-mix.

;;;; Unlike ht-update! or h-mix!, it's nesting-aware
(let* ((h1 (ht (:z 'a)
               (:y (ht (:x 4)
                       (:b 2)))))
       (h2 (ht (:y (ht (:b 3))))))
  (h-mix*! h1 h2)
  h1)
H=>  (h* :z 'a
         :y (h* :x 4
                :b 3)) ;<-- merged and updated

;;;; typical
(let ((t1 (h* :a 1  :b (h* :c 3  :d 4)))
      (t2 (h* :e 5  :b (h* :c 30 :f 6)))
      (t3 (h* :e 50 :g 7)))
  (h-mix*! t1 t2 t3)
  t1)
H=> (h* :a 1 :e 50
        :g 7 :b (h* :c 30 :d 4 :f 6))

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b 2)))
  (h-mix*! t1 t2)
  t1)
H=> (h* :a 1 :b 2 :e 5)

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b 2)))
  (h-mix*! t1 t2)
  t2)
H=> (h* :e 5 :b 2) ;; t2 is intact

(let ((t1 (h* :a 1 :b (h* :c 3  :d 4)))
      (t2 (h* :e 5 :b (h* :c 30 :f 6))))
  (h-mix*! t1 t2)
  t1)

;;all mixed!
H=> (h* :a 1 :e 5 :b (h* :c 30 :d 4 :f 6))

(let ((t1 (h* :a 1  :g 7 :b (h* :c 3  :d 4)))
      (t2 (h* :a 10 :e 5 :b (h* :c 30 :f 6))))
  (h-mix*! t1 t2)
  t1)

;;all mixed!
H=> (h* :a 10 :e 5 :g 7 :b (h* :c 30 :d 4 :f 6))

;;;; some base cases
(let ((t1 (h* :a 1))
      (t2 (ht)))
  (h-mix*! t1 t2)
  t1)
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (h* :a 1)))
  (h-mix*! t1 t2)
  t1)
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (ht)))
  (h-mix*! t1 t2)
  t1)
H=> (ht)
h-mix* (table &rest from-tables)

Return a table that has all key–value pairs from the tables.

Unlike h-mix and ht-merge, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the h-mix* of them.

The tables (TABLE plus FROM-TABLES) are processed from left to
right, and no table is modified.

So (h-mix* table1 table2 table3 table4) will:

  • create a clone of table1 (let's call it 'table0')
  • modify table0 with the data from table2; then
  • modify table0 with the data from table3; then
  • modify table0 with the data from table4;
  • return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is h-mix*!.

This function is nesting-aware.

Its also-side-effect-free but only-first-level counterpart is h-mix.

For no nesting-awareness but with side-effects, use h-mix!.

;;;; Unlike ht-merge or h-mix, it's nesting-aware
(let* ((h1 (ht (:z 'a)
               (:y (ht (:x 4)
                       (:b 2)))))
       (h2 (ht (:y (ht (:b 3))))))
  (h-mix* h1 h2))
H=>  (h* :z 'a
         :y (h* :x 4
                :b 3)) ;<-- merged and updated

;;;; typical
(let ((t1 (h* :a 1  :b (h* :c 3  :d 4)))
      (t2 (h* :e 5  :b (h* :c 30 :f 6)))
      (t3 (h* :e 50 :g 7)))
  (h-mix* t1 t2 t3))
H=> (h* :a 1 :e 50
        :g 7 :b (h* :c 30 :d 4 :f 6))

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b 2)))
  (h-mix* t1 t2))
H=> (h* :a 1 :b 2 :e 5)

(let ((t1 (h* :a 1 :b (h* :c 3 :d 4)))
      (t2 (h* :e 5 :b 2)))
  (h-mix* t1 t2)
  t2)
H=> (h* :e 5 :b 2) ;; t2 is intact

(let ((t1 (h* :a 1 :b (h* :c 3  :d 4)))
      (t2 (h* :e 5 :b (h* :c 30 :f 6))))
  (h-mix* t1 t2))

;;all mixed!
H=> (h* :a 1 :e 5 :b (h* :c 30 :d 4 :f 6))

(let ((t1 (h* :a 1  :g 7 :b (h* :c 3  :d 4)))
      (t2 (h* :a 10 :e 5 :b (h* :c 30 :f 6))))
  (h-mix* t1 t2))

;;all mixed!
H=> (h* :a 10 :e 5 :g 7 :b (h* :c 30 :d 4 :f 6))

;;;; some base cases
(let ((t1 (h* :a 1))
      (t2 (ht)))
  (h-mix* t1 t2))
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (h* :a 1)))
  (h-mix* t1 t2))
H=> (h* :a 1)

(let ((t1 (ht))
      (t2 (ht)))
  (h-mix* t1 t2))
H=> (ht)
Difference

Functions that subtract tables, either destructively or side-effects-free.

dif
h-dif! (table &rest from-tables)

Update TABLE by removing all matching key–value pairs in FROM-TABLES.

That is: the removal only happens when both the key and the value
in one of the tables in FROM-TABLES matches that found in TABLE.
If only the key is matched, there's no removal.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-dif! table1 table2 table3 table4) will:

  • modify table1 with the data from table2; then
  • modify table1 with the data from table3; then
  • modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-dif.

This function only works at 'the first level' — no nesting.
Its also-destructive nesting-aware counterpart is h-dif*!.

For nesting-awareness but no side-effects, use h-dif*.

;;;; won't work here — this is for h-dif*!
(let ((tbl1 (h* :a (h* :b (h* :c 3 :e 5 :d 4)
                       :g 7 :h 8)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :g 6)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :h 8))))
  (h-dif! tbl1 tbl2 tbl3)
  tbl1)
H=> (h* :a (h* :b (h* :c 3 :e 5 :d 4)
               :g 7 :h 8))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :c 3 :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :c 3 :d 4)))


;;;; some base cases
(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (ht)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (h* :a 1)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (h* :c 3)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 2)))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (h* :a 1 :c 3)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-dif! tbl1 tbl2)
  tbl1)
H=> (ht)
h-dif (table &rest from-tables)

Return a table of all key–value pairs in TABLE not in FROM-TABLES.

That is: all pairs in TABLE appear in the results, except those that also
appear in one of the tables in FROM-TABLES. If only the key is matched,
there's no removal.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-dif table1 table2 table3 table4) will:

  • create a clone of table1 (let's call it 'table0')
  • modify table0 with the data from table2; then
  • modify table0 with the data from table3; then
  • modify table0 with the data from table4;
  • return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is h-dif!.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is h-dif*.

For nesting-awareness with side-effects, use h-dif*!.

;;;; won't work here — this is for h-dif*
(let ((tbl1 (h* :a (h* :b (h* :c 3 :e 5 :d 4)
                       :g 7 :h 8)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :g 6)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :h 8))))
  (h-dif tbl1 tbl2 tbl3))
H=> (h* :a (h* :b (h* :c 3 :e 5 :d 4)
               :g 7 :h 8))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-dif tbl1 tbl2))
H=> (h* :a (h* :b (h* :c 3 :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-dif tbl1 tbl2))
H=> (h* :a (h* :b (h* :c 3 :d 4)))

;;;; some base cases
(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-dif tbl1 tbl2))
H=> (ht)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-dif tbl1 tbl2))
H=> (h* :a 1)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-dif tbl1 tbl2))
H=> (h* :c 3)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 2)))
  (h-dif tbl1 tbl2))
H=> (h* :a 1 :c 3)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-dif tbl1 tbl2))
H=> (ht)
h-dif*! (table &rest from-tables)

Update TABLE by removing all matching key–value pairs in FROM-TABLES.

That is: the removal only happens when both the key and the value
in one of the tables in FROM-TABLES matches that found in TABLE.
If only the key is matched, there's no removal.

Unlike h-dif!, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the h-dif*! of them.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-dif! table1 table2 table3 table4) will:

  • modify table1 with the data from table2; then
  • modify table1 with the data from table3; then
  • modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-dif*.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is h-dif!.

For no nesting-awareness and no side-effects, use h-dif.

;;;; typical
(let ((tbl1 (h* :a (h* :b (h* :c 3 :e 5 :d 4)
                       :g 7 :h 8)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :g 6)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :h 8))))
  (h-dif*! tbl1 tbl2 tbl3)
  tbl1)
H=> (h* :a (h* :b (h* :e 5)
               :g 7))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (ht)

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :c 3 :d 4)))


;;;; some base cases
(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (h* :a 1)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (h* :c 3)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 2)))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (h* :a 1 :c 3)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-dif*! tbl1 tbl2)
  tbl1)
H=> (ht)
h-dif* (table &rest from-tables)

Return a table of all key–value pairs in TABLE not in FROM-TABLES.

That is: all pairs in TABLE appear in the results, except those that also
appear in one of the tables in FROM-TABLES. If only the key is matched,
there's no removal.

Unlike h-dif, it updates nested levels: so if both values are
themselves hash tables, the new value will be the h-dif* of them.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-dif* table1 table2 table3 table4) will:

  • create a clone of table1 (let's call it 'table0')
  • modify table0 with the data from table2; then
  • modify table0 with the data from table3; then
  • modify table0 with the data from table4;
  • return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is h-dif*!.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is h-dif.

For no nesting-awareness but with side-effects, use h-dif!.

;;;; typical
(let ((tbl1 (h* :a (h* :b (h* :c 3 :e 5 :d 4)
                       :g 7 :h 8)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :g 6)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :h 8))))
  (h-dif* tbl1 tbl2 tbl3))
H=> (h* :a (h* :b (h* :e 5)
               :g 7))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-dif* tbl1 tbl2))
H=> (ht)

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-dif* tbl1 tbl2))
H=> (h* :a (h* :b (h* :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-dif* tbl1 tbl2))
H=> (h* :a (h* :b (h* :c 3 :d 4)))


;;;; some base cases
(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-dif* tbl1 tbl2))
H=> (h* :a 1)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-dif* tbl1 tbl2))
H=> (h* :c 3)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 2)))
  (h-dif* tbl1 tbl2))
H=> (h* :a 1 :c 3)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-dif* tbl1 tbl2))
H=> (ht)
Intersection

Functions that build a table with the pairs common to all tables, either
destructively or side-effects-free.

cmn

cmn as in: 'common' to all tables.

h-cmn! (table &rest from-tables)

Update TABLE by keeping only the intersection with FROM-TABLES.

That is: the key is kept only when the pair is found in all TABLES.
If only the key is matched, not kept.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-cmn! table1 table2 table3 table4) will:

  • modify table1 with the data from table2; then
  • modify table1 with the data from table3; then
  • modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-cmn.

This function only works at 'the first level' — no nesting.
Its also-destructive nesting-aware counterpart is h-cmn*!.

For nesting-awareness but no side-effects, use h-cmn*.

;;;; won't work here — this is for h-cmn*!
(let ((tbl1 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7 :i 9)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :f 6 :i 9))))
  (h-cmn! tbl1 tbl2 tbl3)
  tbl1)
H=> (h*)

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-cmn! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :c 3 :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-cmn! tbl1 tbl2)
  tbl1)
H=> (h*)

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-cmn! tbl1 tbl2)
  tbl1)
H=> (h*)

;;;; some base cases
(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-cmn! tbl1 tbl2)
  tbl1)
H=> (h*)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-cmn! tbl1 tbl2)
  tbl1)
H=> (h* :a 1)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-cmn! tbl1 tbl2)
  tbl1)
H=> (h* :a 1)
h-cmn (table &rest from-tables)

Return a table of all key–value pairs in TABLE also in FROM-TABLES.

That is: no pairs in TABLE appear in the results, except those that
appear in all the tables in FROM-TABLES. If only the key is
matched, there's no removal.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-cmn table1 table2 table3 table4) will:

  • create a clone of table1 (let's call it 'table0')
  • modify table0 with the data from table2; then
  • modify table0 with the data from table3; then
  • modify table0 with the data from table4;
  • return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is h-cmn!.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is h-cmn*.

For nesting-awareness with side-effects, use h-cmn*!.

;;;; won't work here — this is for h-cmn*
(let ((tbl1 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7 :i 9)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :f 6 :i 9))))
  (h-cmn tbl1 tbl2 tbl3))
H=> (h*)

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-cmn tbl1 tbl2))
H=> (h* :a (h* :b (h* :c 3 :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-cmn tbl1 tbl2))
H=> (h*)

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-cmn tbl1 tbl2))
H=> (h*)

;;;; some base cases
(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-cmn tbl1 tbl2))
H=> (h*)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-cmn tbl1 tbl2))
H=> (h* :a 1)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-cmn tbl1 tbl2))
H=> (h* :a 1)
h-cmn*! (table &rest from-tables)

Update TABLE by keeping only the intersection with FROM-TABLES.

That is: the key is kept only when the pair is found in all TABLES.
If only the key is matched, not kept.

Unlike h-cmn!, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the h-cmn*! of them.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-cmn! table1 table2 table3 table4) will:

  • modify table1 with the data from table2; then
  • modify table1 with the data from table3; then
  • modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is h-cmn*.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is h-cmn!.

For no nesting-awareness and no side-effects, use h-cmn.

;;;; typical
(let ((tbl1 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7 :i 9)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :f 6 :i 9))))
  (h-cmn*! tbl1 tbl2 tbl3)
  tbl1)
H=> (h* :a (h* :b (h*)
               :f 6))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-cmn*! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :c 3 :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-cmn*! tbl1 tbl2)
  tbl1)
H=> (h* :a (h* :b (h* :c 3)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-cmn*! tbl1 tbl2)
  tbl1)
H=> (h* :a (h*))

;;;; some base cases
(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-cmn*! tbl1 tbl2)
  tbl1)
H=> (h*)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-cmn*! tbl1 tbl2)
  tbl1)
H=> (h* :a 1)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-cmn*! tbl1 tbl2)
  tbl1)
H=> (h* :a 1)
h-cmn* (table &rest from-tables)

Return a table of all key–value pairs in TABLE also in FROM-TABLES.

That is: no pairs in TABLE appear in the results, except those that
appear in all the tables in FROM-TABLES. If only the key is
matched, there's no removal.

Unlike h-cmn, it updates nested levels: so if both values are
themselves hash tables, the new value will be the h-cmn* of them.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-cmn* table1 table2 table3 table4) will:

  • create a clone of table1 (let's call it 'table0')
  • modify table0 with the data from table2; then
  • modify table0 with the data from table3; then
  • modify table0 with the data from table4;
  • return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is h-cmn*!.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is h-cmn.

For no nesting-awareness but with side-effects, use h-cmn!.

;;;; typical
(let ((tbl1 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7 :i 9)))
      (tbl2 (h* :a (h* :b (h* :c 3)
                       :f 6 :g 7)))
      (tbl3 (h* :a (h* :b (h* :d 4 :e 1)
                       :f 6 :i 9))))
  (h-cmn* tbl1 tbl2 tbl3))

H=> (h* :a (h* :b (h*)
               :f 6))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3 :d 4)))))
  (h-cmn* tbl1 tbl2))
H=> (h* :a (h* :b (h* :c 3 :d 4)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b (h* :c 3)))))
  (h-cmn* tbl1 tbl2))
H=> (h* :a (h* :b (h* :c 3)))

(let ((tbl1 (h* :a (h* :b (h* :c 3 :d 4))))
      (tbl2 (h* :a (h* :b 2))))
  (h-cmn* tbl1 tbl2))
H=> (h* :a (h*))

;;;; some base cases
(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 2 :c 3)))
  (h-cmn* tbl1 tbl2))
H=> (h*)

(let ((tbl1 (h* :a 1 :c 3))
      (tbl2 (h* :a 1)))
  (h-cmn* tbl1 tbl2))
H=> (h* :a 1)

(let ((tbl1 (h* :a 1))
      (tbl2 (h* :a 1)))
  (h-cmn* tbl1 tbl2))
H=> (h* :a 1)
Comparison / Equality / Equivalence
1D: implicit (Vector, List, Lines) (indices as keys)

Not needed.

Vectors, lists, and strings can be compared with equal.
Examples:

(equal '(a x) '(a x))      ;=> t
(equal [a x] [a x])        ;=> t
(equal "a\nx\n" "a\nx\n")  ;=> t

And they can only be equal to exactly themselves.

Also note that we'll keep these two (one of which with extra
trailing newline) not being equal:

(equal "a\nx\n" "a\nx")    ;=> nil
1D: explicit (KVL, Cons Cell)

Functions that compare if two or more objects are equal.

Key–value pairs are of dimension 1: not nested.

No need for a function to compare cons cells: equal will do:

(equal '(a . 1) '(a . 1)) ;=> t
h-kvl= (kvl1 kvl2 &rest more-kvls)

Return t only if all KEY–VALUE LINES have same KEY–VALUE pairs.

It accepts any number ≥2 of KEY–VALUE LINES as input.

  ;;;; Identical
  (h-kvl=
   "Mary = 27\nJohn = 20\nAnne = 21"
   "Mary = 27\nJohn = 20\nAnne = 21")
  => t

  ;;;; Different row order
  (h-kvl=
   "Mary = 27\nJohn = 20\nAnne = 21"
   "Mary = 27\nAnne = 21\nJohn = 20")
  => t

  (h-kvl=  "\
Mary = 27
John = 20
Anne = 21"
           "\
Mary = 27
Anne = 21
John = 20")
  => t

  ;;;; More than two
  (h-kvl=
   "Mary = 27\nJohn = 20\nAnne = 21"
   "Mary = 27\nJohn = 20\nAnne = 21"
   "Mary = 27\nAnne = 21\nJohn = 20")
  => t

  ;;;; Empty lines
  (h-kvl=  "\
Mary = 27
John = 20
Anne = 21"
           "\
Mary = 27\n
Anne = 21\n\n
John = 20\n\n\n")
  => t

  (h-kvl= "\
Mary = 27

Anne = 21


John = 20


"
          "\
Mary = 27
John = 20
Anne = 21")
  => t

  ;;;; Repeated keys
  (h-kvl= "\
Mary = 27
John = 20
Anne = 21
Anne = 42
John = 31"
          "\
Mary = 27
Anne = 42
John = 31")
  => t

  ;;;; Different spacing
  (h-kvl=  "\
Mary Lee = 27
John     = 20
Ann      = 21"
           "\
Mary Lee   =  27
Ann        =  21
John       =  20"
           "\
Mary Lee = 27
Ann = 21
John = 20")
  => t

  ;;;; Leading spaces
  (h-kvl=  "\
  Mary Lee = 27
  John     = 20
  Ann      = 21"
           "\
     Mary Lee   =  27
     Ann        =  21
     John       =  20")
  => t

  ;;;; Same but non-default field delimiter
  (let ((h-kvl-sep-re " *: *"))
    (h-kvl=  "\
Mary Lee  :  27
John      :  20
Ann       :  21"
             "\
Mary Lee:27
Ann:21
John:20"))
  => t
≥1D unspecified (Hash table) (possibly nested)

Functions that compare if two or more hash tables are equivalent.
Key–value pairs are of unspecified dimension >1 (nested).

Many distinctions here, so a summary of what the operators mean
follows. These tests return t if:

Operator Condition for returning a truthy value
h== tables have same keys in THE SAME order, and the values:
  - when both are hash tables, are also h==.
  - when both are of some different type, they are equal.
h-pr== same as the previous, but stricter: properties*
  also match: same size, test function, etc.
h= tables have same keys in ANY order, and the values:
  - when both are hash tables, are also h=.
  - when both are of some different type, they are equal.
h-pr= same as the previous, but stricter: properties*
  also match: same size, test function, etc.
h_= tables have same keys in ANY order, and the values:
  - are equal, OR:
  -- when both are hash tables, are also h_=.
  -- when both are of some different type, they return t
  when tested with their respective equality function;
  so, for example, if they're both alists, comparison
  of them using h-alist= returns t; when both TSVs.
  comparison of them using h-tsv= returns t; etc.
h~= tables have same keys in ANY order, and the values:
  - are equal, OR:
  -- when both are hash tables, are also h~=.
  -- when both are of some different type, they
  return t when tested with h-it~, which in
  turn means that they can even be of different
  types but, when converted to a hash table,
  become h~=.

So there's a gradation of strictness of equivalence, where:

and:

And so h~= is the most permissive, where equivalences of each
item are themselves deeply tested.

The h== one may be useful, for example, for deciding whether
two hash tables would return the exact same results when mapping or
converting.

We could say that h= is to h-pr= as equal is to
equal-including-properties.

When in doubt, h= is likely the best choice, because:

  • It accepts different ordering of keys inside the tables. Order is
    not supposed to matter in hash tables, so this is most likely
    what you want: being stricter with h== or any of the -pr
    would deem unequal tables that for most regular purposes are
    equivalent.
  • When nested, it applies the same logic to any hash tables stored
    in it as values, ignoring order: suffices that their keys are the
    same and that the values are either equal or h=.
  • Any non–hash-table values are tested with equal, so you don't
    run the risk of false equivalence of lists that were not intended
    as plists (and therefore sensitive to ordering and to repeated
    items), and other such oddities.
h-pr== (table1 table2 &rest more-tables)

One of the ways to see if all hash tables TABLES are equivalent.

Compare properties? YES
Compare keys order? YES
Compare non–hash-table values with… #'equal

When the values are hash tables, they are compared with
this very function, recursively.

For more details, see the helper function xht--equal?, as well as
the section heading description in the code source.

See all: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h-pr== (h-new 65 'equal)   (h-new  5 'eq))       => nil
(h-pr== (h* :a 1 :b 2 :c 3) (h* :b 2 :c 3 :a 1))  => nil
(h-pr== (h* :a (h* :b 2 :c 3))
        (h* :a (h* :b 2 :c 3)))
=> t

;;;; A single test that illustrates all hash table equality functions
(let* ((h11  (h-new 65 'equal))
       (h12  (h-new  5 'eq))
       (h21  (h* :a 1 :b 2 :c 3))
       (h22  (h* :b 2 :c 3 :a 1))
       (h23  (h* :c 3 :a 1 :b 2))
       (h31  (h* :a (h* :b 2 :c 3)))
       (h32  (h* :a (h* :b 2 :c 3)))
       (h33  (h* :a (h* :c 3 :b 2)))
       (h41  (h* :a 1 :b '(:c 3 :d 4)))
       (h42  (h* :a 1 :b '(:d 4 :c 3)))
       (h43  (h* :a 1 :b '(:d 4 :c 3 :c 5)))
       (h44  (h* :a 1 :b '((:c . 3) (:d . 4))))
       (h51  (h* "a" "1" "b" '("c" "3" "d" "4" "c" "18")))
       (h52  (h* "a" "1" "b" (h* "d" "4" "c" "3")))
       (h61  (h* :a 1 :b '(:c 3 :d ((:e . 5) (:f . 6)))))
       (h62  (h* :a 1 :b '(:c 3 :d (:e 5 :f 6 :e 2))))
       (h63  (h* :a 1 :b `(:c 3 :d ,(h* :e 5 :f 6))))
       (otb7 "| id   | name  | age |
              |------+-------+-----|
              | id01 | Alice |  42 |
              | id02 | Bob   |  30 |
              | id03 | Emily |  21 |")
       (lol7 '(("id"   "name"  "age")
               ("id01" "Alice" "42")
               ("id02" "Bob"   "30")
               ("id03" "Emily" "21")))
       (htb7 (h* "id01" (h* "id"   "id01"
                            "name" "Alice"
                            "age"  "42")
                 "id02" (h* "id"   "id02"
                            "name" "Bob"
                            "age"  "30")
                 "id03" (h* "id"   "id03"
                            "name" "Emily"
                            "age"  "21")))
       (h71  (h* :a 1 :b otb7))
       (h72  (h* :b lol7 :a 1))
       (h73  (h* :b htb7 :a 1))
       (h81  (h* :a 1 :b `(:c ,otb7)))
       (h82  (h* :b `((:c . ,lol7)) :a 1))
       (h83  (h* :a 1 :b (h* :c htb7)))
       (combos      ; expected:   -pr== | -pr= | ==  | =   | _=  | ~=  |
        `((,h11 ,h12 ,h11     )   ; nil | nil  | t   | t   | t   | t   |
          (,h21 ,h22 ,h23     )   ; nil | t    | nil | t   | t   | t   |
          (,h31 ,h32          )   ; t   | t    | t   | t   | t   | t   |
          (,h31 ,h32 ,h33     )   ; nil | t    | nil | t   | t   | t   |
          (,h41 ,h42 ,h43     )   ; nil | nil  | nil | nil | t   | t   |
          (,h41 ,h42 ,h43 ,h44)   ; nil | nil  | nil | nil | nil | t   |
          (,h51 ,h52          )   ; nil | nil  | nil | nil | nil | t   |
          (,h61 ,h62 ,h63     )   ; nil | nil  | nil | nil | nil | nil |
          (,h71 ,h72 ,h73     )   ; nil | nil  | nil | nil | nil | t   |
          (,h81 ,h82 ,h83     ))) ; nil | nil  | nil | nil | nil | nil |
       (funs  '(h-pr==  h-pr=  h==  h=  h_=  h~=)))
  (-map (lambda (combo)
          (-map (lambda (fun)
                  (apply fun combo))
                funs))
        combos))
=> '((nil nil t   t   t   t)
     (nil t   nil t   t   t)
     (t   t   t   t   t   t)
     (nil t   nil t   t   t)
     (nil nil nil nil t   t)
     (nil nil nil nil nil t)
     (nil nil nil nil nil t)
     (nil nil nil nil nil nil)
     (nil nil nil nil nil t)
     (nil nil nil nil nil nil))
h-pr= (table1 table2 &rest more-tables)

One of the ways to see if all hash tables TABLES are equivalent.

Compare properties? YES
Compare keys order? NO
Compare non–hash-table values with… #'equal

When the values are hash tables, they are compared with this very
function, recursively.

For more details, see the helper function xht--equal?, as well as
the section heading description in the code source.

See all: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h-pr= (h-new 65 'equal)   (h-new  5 'eq))       => nil
(h-pr= (h* :a 1 :b 2 :c 3) (h* :b 2 :c 3 :a 1))  => t
(h-pr= (h* :a (h* :b 2 :c 3))
       (h* :a (h* :b 2 :c 3)))
=> t

;;;; 2 hash tables — "equal"
;;;;; regular
(h-pr= (ht (:a 1))
       (let ((H (make-hash-table))) (puthash :a 1 H) H))
=> nil ; equal≠eql
(h-pr= (ht ("a" 1))
       (let ((H (ht-create))) (ht-set! H "a" 1) H))
=> t
(h-pr= (ht  ("a" 1)) (ht ("a" 1)))                      => t
(h-pr= (ht  ("a" 1)("b" 2)) (ht ("a" 1)("b" 2)))        => t
(h-pr= (h* "a" 1  "b" 2) (h* "a" 1  "b" 2))             => t
;;;;; nested
(h-pr= (h* "a" (h* "b" 2)) (h* "a" (h* "b" 2)))         => t
(h-pr= (h* :a (h* :b (h* :c 3
                         :d 4)))
       (h* :a (h* :b (h* :c 3
                         :d 4))))
=> t

(h-pr= (h* :a (h* :b (h* :c 3
                         :d 4)))
       (h* :a (h* :b 2)))
=> nil

(h-pr= (h* :a (h* :b (h* :c 3
                         :d 4)))
       (h* :a (h* :b 2)))
=> nil

;;;; 3 hash tables — "equal"
(h-pr=  (ht ("a" 1)) (ht ("a" 1))  (ht ("a" 1)))        => t
(h-pr= (h* "a" 1) (h* "a" 1)  (h* "a" 1))               => t

;;;; 4 hash tables — "equal"
(h-pr= (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 1)))  => t
(h-pr= (ht (:a  1))  (ht (:a  1))  (ht (:a  1))  (ht (:a  1)))  => t
(h-pr= (ht ('a  1))  (ht ('a  1))  (ht ('a  1))  (ht ('a  1)))  => t

;;;; 2 hash tables — "different"
(h-pr= (ht ("a" 1))  (ht (:a  1)))                => nil
(h-pr= (ht (:a  1))  (ht ("a" 1)))                => nil
(h-pr= (ht (:a  1))  (ht ('a  1)))                => nil
(h-pr= (ht ("a" 1))  (ht ("b" 1)))                => nil
(h-pr= (ht ("a" 1))  (ht ("a" 2)))                => nil
(h-pr= (ht ("a" 1))  (ht ("a" 1) ("b" 1)))        => nil

;;;; 2+ hash tables — "different": equivalence of plist etc NOT tested
(h-pr= (h* :a '(:b 2 :c 3))
       (h* :a '(:c 3 :b 2))
       (h* :a '(:b 2 :c 3 :b 22)))
=> nil

(h-pr= (h* :a '(:b 2 :c 3))
       (h* :a '(:b 2 :c 3 :b 22)))
=> nil

(h-pr= (h* :a '((:b . 2) (:c . 3)))
       (h* :a '((:c . 3) (:b . 2))))
=> nil

;; different order, repeated (updated) entry
(h-pr= (h* :a "| id | name  | age |
               |----+-------+-----|
               | b  | bob   |  30 |
               | e  | emily |  21 |")

       (h* :a "| id | name  | age |
               |----+-------+-----|
               | e  | emily |  21 |
               | b  | bob   |  30 |
               | e  | emily |  12 |"))
=> nil

;;;; 3 hash tables — "different"
(h-pr= (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 2)))  => nil
(h-pr= (ht ("a" 1))  (ht ("a" 2))  (ht ("a" 1)))  => nil
(h-pr= (ht ("a" 2))  (ht ("a" 1))  (ht ("a" 1)))  => nil
;; <2 hash tables — error
(h-pr= (ht (:a 1))) !!> wrong-number-of-arguments
(h-pr=)             !!> wrong-number-of-arguments

;;;; Comparing properties
(h-pr= (ht) (h*) (h*))      => nil
(h-pr= (ht (:a 1)) (h* :a 1)) => nil
(h-pr= (ht-create) (h-new))   => t
;; different properties detected, plists "sameness" not detected.
(h-pr= (ht  (:a '(:b 2 :c 3)))
       (h* :a '(:c 3 :b 2)))
=> nil
h== (table1 table2 &rest more-tables)

One of the ways to see if all hash tables TABLES are equivalent.

Compare properties? NO
Compare keys order? YES
Compare non–hash-table values with… #'equal

When the values are hash tables, they are compared with
this very function, recursively.

For more details, see the helper function xht--equal?, as well as
the section heading description in the code source.

See all: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h== (h-new 65 'equal)   (h-new  5 'eq))       => t
(h== (h* :a 1 :b 2 :c 3) (h* :b 2 :c 3 :a 1))  => nil
(h== (h* :a (h* :b 2 :c 3))
     (h* :a (h* :b 2 :c 3)))
=> t
h= (table1 table2 &rest more-tables)

One of the ways to see if all hash tables TABLES are equivalent.

Compare properties? NO
Compare keys order? NO
Compare non–hash-table values with… #'equal

When the values are hash tables, they are compared with this very
function, recursively.

When in doubt, use this one.

This function can be thought of as a variadic version of
ht-equal?: it accepts any number ≥2 of tables as input.

Moreover, it returns t when comparing two indistinguishable nested
hash tables (of same key–value pairs), for which ht-equal? until
v2.4 returns nil:

(ht-equal? (ht (:a 1))
           (ht (:a 1)))           ;=> t

(ht-equal? (ht (:a (ht (:b 2))))
           (ht (:a (ht (:b 2))))) ;=> nil !!

For more details, see the helper function xht--equal?, as well as
the section heading description in the code source.

See all: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h= (h-new 65 'equal)   (h-new  5 'eq))        => t
(h= (h* :a 1 :b 2 :c 3) (h* :b 2 :c 3 :a 1))   => t
(h= (h* :a (h* :b 2 :c 3))
    (h* :a (h* :b 2 :c 3)))
=> t
(h= (h* :a 1 :b '(:c 3 :d 4))
    (h* :a 1 :b '(:d 4 :c 3))
    (h* :a 1 :b '(:d 4 :c 3 :c 5)))
=> nil

(h= (h* "a" "1" "b" '("c" "3" "d" "4" "c" "18"))
    (h* "a" "1" "b" (h* "d" "4" "c" "3")))
=> nil

(h= (h* :a 1 :b '(:c 3 :d ((:e . 5) (:f . 6))))
    (h* :a 1 :b '(:c 3 :d (:e 5 :f 6 :e 2)))
    (h* :a 1 :b `(:c 3 :d ,(h* :e 5 :f 6))))
=> nil

;;;; 2 hash tables — "equal"
;;;;; regular
(h= (ht (:a 1))
    (let ((H (make-hash-table))) (puthash :a 1 H) H))
=> t
(h= (ht ("a" 1))
    (let ((H (ht-create))) (ht-set! H "a" 1) H))
=> t
(h= (ht  ("a" 1)) (ht ("a" 1)))                      => t
(h= (ht  ("a" 1)("b" 2)) (ht ("a" 1)("b" 2)))        => t
(h= (h* "a" 1  "b" 2) (h* "a" 1  "b" 2))             => t
;;;;; nested
(h= (h* "a" (h* "b" 2)) (h* "a" (h* "b" 2)))         => t
(h= (h* :a (h* :b (h* :c 3
                      :d 4)))
    (h* :a (h* :b (h* :c 3
                      :d 4))))
=> t

(h= (h* :a (h* :b (h* :c 3
                      :d 4)))
    (h* :a (h* :b 2)))
=> nil

(h= (h* :a (h* :b (h* :c 3
                      :d 4)))
    (h* :a (h* :b 2)))
=> nil

;;;; 3 hash tables — "equal"
(h=  (ht ("a" 1)) (ht ("a" 1))  (ht ("a" 1)))          => t
(h= (h* "a" 1) (h* "a" 1)  (h* "a" 1))                 => t

;;;; 4 hash tables — "equal"
(h= (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 1)))  => t
(h= (ht (:a  1))  (ht (:a  1))  (ht (:a  1))  (ht (:a  1)))  => t
(h= (ht ('a  1))  (ht ('a  1))  (ht ('a  1))  (ht ('a  1)))  => t

;;;; 2 hash tables — "different"
(h= (ht ("a" 1))  (ht (:a  1)))                => nil
(h= (ht (:a  1))  (ht ("a" 1)))                => nil
(h= (ht (:a  1))  (ht ('a  1)))                => nil
(h= (ht ("a" 1))  (ht ("b" 1)))                => nil
(h= (ht ("a" 1))  (ht ("a" 2)))                => nil
(h= (ht ("a" 1))  (ht ("a" 1) ("b" 1)))        => nil

;;;; 2+ hash tables — "different": equivalence of plist etc NOT tested
(h= (h* :a '(:b 2 :c 3))
    (h* :a '(:c 3 :b 2))
    (h* :a '(:b 2 :c 3 :b 22)))
=> nil

(h= (h* :a '(:b 2 :c 3))
    (h* :a '(:b 2 :c 3 :b 22)))
=> nil

(h= (h* :a '((:b . 2) (:c . 3)))
    (h* :a '((:c . 3) (:b . 2))))
=> nil

;; different order, repeated (updated) entry
(h= (h* :a "| id | name  | age |
            |----+-------+-----|
            | b  | bob   |  30 |
            | e  | emily |  21 |")

    (h* :a "| id | name  | age |
            |----+-------+-----|
            | e  | emily |  21 |
            | b  | bob   |  30 |
            | e  | emily |  12 |"))
=> nil

;;;; 3 hash tables — "different"
(h= (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 2)))  => nil
(h= (ht ("a" 1))  (ht ("a" 2))  (ht ("a" 1)))  => nil
(h= (ht ("a" 2))  (ht ("a" 1))  (ht ("a" 1)))  => nil
;; <2 hash tables — error
(h= (ht (:a 1))) !!> wrong-number-of-arguments
(h=)             !!> wrong-number-of-arguments

;;;; Comparing properties
(h= (ht) (h*) (h*))      => t
(h= (ht (:a 1)) (h* :a 1)) => t
(h= (ht-create) (h-new))   => t
;; different properties not detected, plists "sameness" not detected.
(h= (ht  (:a '(:b 2 :c 3)))
    (h* :a '(:c 3 :b 2)))
=> nil
h_= (table1 table2 &rest more-tables)

One of the ways to see if all hash tables TABLES are equivalent.

Compare properties? NO
Compare keys order? NO
Compare non–hash-table values with… #'h-it=

When the values are hash tables, they are compared with this very
function, recursively.

For more details, see the helper function xht--equal?, as well as
the section heading description in the code source.

See all: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h_= (h* :a 1 :b '(:c 3 :d 4))
     (h* :a 1 :b '(:d 4 :c 3))
     (h* :a 1 :b '(:d 4 :c 3 :c 5)))
=> t

(h_= (h* "a" "1" "b" '("c" "3" "d" "4" "c" "18"))
     (h* "a" "1" "b" (h* "d" "4" "c" "3")))
=> nil

(h_= (h* :a 1 :b '(:c 3 :d ((:e . 5) (:f . 6))))
     (h* :a 1 :b '(:c 3 :d (:e 5 :f 6 :e 2)))
     (h* :a 1 :b `(:c 3 :d ,(h* :e 5 :f 6))))
=> nil


;;;; 2 hash tables — "equal"
;;;;; regular
(h_= (ht (:a 1))
     (let ((H (make-hash-table))) (puthash :a 1 H) H))
=> t
(h_= (ht ("a" 1))
     (let ((H (ht-create))) (ht-set! H "a" 1) H))
=> t
(h_= (ht  ("a" 1)) (ht ("a" 1)))                      => t
(h_= (ht  ("a" 1)("b" 2)) (ht ("a" 1)("b" 2)))        => t
(h_= (h* "a" 1  "b" 2) (h* "a" 1  "b" 2))             => t
;;;;; nested
(h_= (h* "a" (h* "b" 2)) (h* "a" (h* "b" 2)))         => t
(h_= (h* :a (h* :b (h* :c 3
                       :d 4)))
     (h* :a (h* :b (h* :c 3
                       :d 4))))
=> t

(h_= (h* :a (h* :b (h* :c 3
                       :d 4)))
     (h* :a (h* :b 2)))
=> nil

(h_= (h* :a (h* :b (h* :c 3
                       :d 4)))
     (h* :a (h* :b 2)))
=> nil

;;;; 3 hash tables — "equal"
(h_=  (ht ("a" 1)) (ht ("a" 1))  (ht ("a" 1)))        => t
(h_= (h* "a" 1) (h* "a" 1)  (h* "a" 1))               => t

;;;; 4 hash tables — "equal"
(h_= (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 1)))  => t
(h_= (ht (:a  1))  (ht (:a  1))  (ht (:a  1))  (ht (:a  1)))  => t
(h_= (ht ('a  1))  (ht ('a  1))  (ht ('a  1))  (ht ('a  1)))  => t

;;;; 2 hash tables — "SAME"
(h_= (ht ("a" 1))  (ht (:a  1)))          => nil
(h_= (ht (:a  1))  (ht ("a" 1)))          => nil
(h_= (ht (:a  1))  (ht ('a  1)))          => nil
(h_= (ht ("a" 1))  (ht ("b" 1)))          => nil
(h_= (ht ("a" 1))  (ht ("a" 2)))          => nil
(h_= (ht ("a" 1))  (ht ("a" 1) ("b" 1)))  => nil

;;;; 2+ hash tables — "SAME": equivalence of plist etc IS tested
(h_= (h* :a '(:b 2 :c 3))
     (h* :a '(:c 3 :b 2))
     (h* :a '(:b 2 :c 3 :b 22)))
=> t

(h_= (h* :a '(:b 2 :c 3))
     (h* :a '(:c 3 :b 2)))
=> t

(h_= (h* :a '(:b 2 :c 3))
     (h* :a '(:b 2 :c 3 :b 22)))
=> t

(h_= (h* :a '((:b . 2) (:c . 3)))
     (h* :a '((:c . 3) (:b . 2))))
=> t

;; different order, repeated (updated) entry
(h_= (h* :a "| id | name  | age |
             |----+-------+-----|
             | b  | bob   |  30 |
             | e  | emily |  21 |")

     (h* :a "| id | name  | age |
             |----+-------+-----|
             | e  | emily |  21 |
             | b  | bob   |  30 |
             | e  | emily |  12 |"))
=> t


;;;; 3 hash tables — "different"
(h_= (ht ("a" 1))  (ht ("a" 1))  (ht ("a" 2)))  => nil
(h_= (ht ("a" 1))  (ht ("a" 2))  (ht ("a" 1)))  => nil
(h_= (ht ("a" 2))  (ht ("a" 1))  (ht ("a" 1)))  => nil
;; <2 hash tables — error
(h_= (ht (:a 1))) !!> wrong-number-of-arguments
(h_=)             !!> wrong-number-of-arguments

;;;; Comparing properties
(h_= (ht) (h*) (h*))      => t
(h_= (ht (:a 1)) (h* :a 1)) => t
(h_= (ht-create) (h-new))   => t
;; different properties not detected, plists "sameness" detected.
(h_= (ht  (:a '(:b 2 :c 3)))
     (h* :a '(:c 3 :b 2)))
=> t
h~= (table1 table2 &rest more-tables)

One of the ways to see if all hash tables TABLES are equivalent.

Compare properties? NO
Compare keys order? NO
Compare non–hash-table values with… #'h-it~

When the values are hash tables, they are compared with this very
function, recursively.

For more details, see the helper function xht--equal?, as well as
the section heading description in the code source.

See all: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h~= (h* :a 1 :b '(:c 3 :d 4))
     (h* :a 1 :b '(:d 4 :c 3))
     (h* :a 1 :b '(:d 4 :c 3 :c 5)))
=> t

(h~= (h* "a" "1" "b" '("c" "3" "d" "4" "c" "18"))
     (h* "a" "1" "b" (h* "d" "4" "c" "3")))
=> t

(h~= (h* :a 1 :b '(:c 3 :d ((:e . 5) (:f . 6))))
     (h* :a 1 :b '(:c 3 :d (:e 5 :f 6 :e 2)))
     (h* :a 1 :b `(:c 3 :d ,(h* :e 5 :f 6))))
=> nil
h-props= (table1 table2 &rest more-tables)

Whether all hash tables TABLES have the same properties.

Note: but not necessarily the same key–value pairs, just the properties.

Compare properties? YES
Compare data? NO

See also: h-pr==, h-pr=, h==, h=, h_=, h~=.

(h-props= (h* :a 1) (h* :b 2) (h* :c 2))   => t
(h-props= (h* :a 1) (h* :b 2))             => t
;; different nominal size: 1≠2
(h-props= (h* :a 1) (h* :a 1 :b 2))        => nil
;; different nominal size: 1≠65
(h-props= (ht (:a 1)) (h* :a 1))           => nil
;; same nominal size: 65
(h-props= (ht (:a 1)) (ht (:a 1) (:b 2)))  => t
(h-props= (ht-create) (h-new))             => t
≥1D unspecified (Alist, Plist, JSON) (possibly nested)

Functions that compare if two or more alists, plists, or json objects are
equivalent. Key–value pairs are of unspecified dimension >1 (nested).

h-alist= (alist1 alist2 &rest more-alists)

Return t only if all ALISTs have same KEY–VALUE pairs.

It accepts any number ≥2 of ALISTs as input.

See h-lol= for more information on equivalence, which also
applies here.

'(("b" ("c" . 3))
  ("a" . 1)
  ("a" . 4))
A=>   '(("a" . 1) ("b"  ("c" . 3)))

'(("a" . 1)("b" . 2)) A=> '(("b" . 2)("a" . 1))
'(("a" . 1)("b" . 2)) A=> '(("b" . 2)("a" . 1)("a" . 4))
'(("a" . 1))          A=> '(("a" . 1))
h-plist= (plist1 plist2 &rest more-plists)

Return t only if all PLISTs have same KEY–VALUE pairs.

It accepts any number ≥2 of PLISTs as input.

See h-lol= for more information on equivalence, which also
applies here.

'("a" 1 "b" ("c" 3))  P=>  '("b" ("c" 3) "a" 1)
'("a" 1 "b" 2)        P=>  '("b" 2 "a" 1)
'("a" 1 "b" 2)        P=>  '("b" 2 "a" 1 "a" 4)
'("a" 1)              P=>  '("a" 1)
h-json= (json1 json2 &rest more-jsons)

Return t only if all JSONs have same KEY–VALUE pairs.

It accepts any number ≥2 of JSONs as input.

See h-lol= for more information on equivalence, which also
applies here.

(h-json=   "{
              \"a\": 1,
              \"b\": {
                \"c\": 3
              }
            }"
           "{
              \"b\": {
                \"c\": 3
              },
              \"a\": 1
            }")
=> t
(h-json=  "{
             \"a\": 1,
             \"b\": 2
           }"
          "{
             \"b\": 2,
             \"a\": 1
           }")
=> t

(h-json= "{\n  \"a\": 1\n  \n}"
         "{\n  \"a\": 1\n  \n}" => t)
2D (LOL, Org table, TSV, CSV, SSV)

Functions that compare if two or more objects are equal.
Key–value pairs are of dimension 2: tabular.

h-lol= (lol1 lol2 &rest more-lols)

Return t only if all LISTS OF LISTS have same KEY–VALUE pairs.

It accepts any number ≥2 of LISTS OF LISTS as input.

Note that regular lists and vectors are the same if they have the
same elements at the same indices, so they can be compared with
plain equal.

Two LISTS OF LISTS, however, may, for our purposes, be equal while
being nominally different.

For example, these two:

'((:id  :name  :age)   '((:id  :name  :age)
  (1    alice  42)       (2    bob    30)
  (2    bob    30)       (1    alice  42))
  (1    alice  21))

As with alists and plists, the top-most element of same ID wins,
and the order between different items doesn't matter. Each unique
ID returns the same information. The same applied when we convert
from Org Table, TSV, CSV, SSV, or JSON.

;; Row order
'(("ID" "NAME" "AGE")
  ("id03" "Anne" "21")
  ("id02" "John" "20")
  ("id01" "Mary" "27"))
L=> '(("ID" "NAME" "AGE")
      ("id01" "Mary" "27")
      ("id02" "John" "20")
      ("id03" "Anne" "21"))

;; Repeated keys
'(("ID" "NAME" "AGE")
  ("id02" "John" "20")
  ("id01" "Mary" "27"))
L=> '(("ID" "NAME" "AGE")
      ("id02" "John" "20")
      ("id01" "Mary" "27")
      ("id01" "Mary" "21"))
h-orgtbl= (orgtbl1 orgtbl2 &rest more-orgtbls)

Return t only if all ORG TABLES have same KEY–VALUE pairs.

It accepts any number ≥2 of ORG TABLES as input.

See h-lol= for more information on equivalence, which also
applies here.

;; Row order
"| ID   | NAME | AGE |
 |------+------+-----|
 | id03 | Anne |  21 |
 | id02 | John |  20 |
 | id01 | Mary |  27 |"
O=>  "| ID   | NAME | AGE |
      |------+------+-----|
      | id01 | Mary |  27 |
      | id02 | John |  20 |
      | id03 | Anne |  21 |"

;; Repeated keys
"| ID   | NAME | AGE |
 |------+------+-----|
 | id01 | Mary |  27 |
 | id02 | John |  20 |"
O=>  "| ID   | NAME | AGE |
      |------+------+-----|
      | id02 | John |  20 |
      | id01 | Mary |  27 |
      | id01 | Mary |  21 |"

;; Unformatted
"| ID   |        NAME |    AGE
 |------
 | id01  | Mary | 27
 | id02      | John      |  20 |"
O=>  "| ID   | NAME | AGE |
      |------+------+-----|
      | id02 | John |  20 |
      | id01 | Mary |  27 |
      | id01 | Mary |  21 |"
h-tsv= (tsv1 tsv2 &rest more-tsvs)

Return t only if all TSVs have same KEY–VALUE pairs.

It accepts any number ≥2 of TSVs as input.

See h-lol= for more information on equivalence, which also
applies here.

;; Row order
"ID\tNAME\tAGE\nid03\tAnne\t21\nid02\tJohn\t20\nid01\tMary\t27"
T=>  "ID\tNAME\tAGE\nid01\tMary\t27\nid02\tJohn\t20\nid03\tAnne\t21"

;; Repeated keys
"ID\tNAME\tAGE\nid01\tMary\t27\nid02\tJohn\t20"
T=>  "ID\tNAME\tAGE\nid02\tJohn\t20\nid01\tMary\t27"
h-csv= (csv1 csv2 &rest more-csvs)

Return t only if all CSVs have same KEY–VALUE pairs.

It accepts any number ≥2 of CSVs as input.

See h-lol= for more information on equivalence, which also
applies here.

;; Row order
"ID,NAME,AGE\nid03,Anne,21\nid02,John,20\nid01,Mary,27"
C=>  "ID,NAME,AGE\nid01,Mary,27\nid02,John,20\nid03,Anne,21"

;; Repeated keys
"ID,NAME,AGE\nid01,Mary,27\nid02,John,20"
C=>  "ID,NAME,AGE\nid02,John,20\nid01,Mary,27"
h-ssv= (ssv1 ssv2 &rest more-ssvs)

Return t only if all SSVs have same KEY–VALUE pairs.

It accepts any number ≥2 of SSVs as input.

See h-lol= for more information on equivalence, which also
applies here.

"ID  NAME  AGE\nid03  Anne  21\nid02  John  20\nid01  Mary  27"
S=>  "ID  NAME  AGE\nid01  Mary  27\nid02  John  20\nid03  Anne  21"

;; Repeated keys
"ID  NAME  AGE\nid01  Mary  27\nid02  John  20"
S=>  "ID  NAME  AGE\nid02  John  20\nid01  Mary  27"
Unknown thing

Functions that compare if two or more objects are equal.
Objects are unspecified.

h-type (obj)

Detect object OBJ's type-for-conversion. Return a keyword.

This isn't the same as type-of; it's more concerned with
types of interest for conversions and lookups.

So, for example, it'd return :tsv for a TSV, while type-of
would return 'string.

Testing is done in a selected order to best guess some potentially
ambiguous types by trying to match the most specific of them first.

  • So LOL is tested for before alist and plist, which are also
    tested before list.
  • Moreover, if, while testing, the value of h-kvl-sep-re matches
    TAB, then h-kvl? could return t if all lines have a single TAB.
    Since by default this variable matches " *= *", it'll be
    interpreted that it's being let-bound, and that the OBJ is a KVL
    rather than a two-column TSV. Likewise in case the value of
    h-kvl-sep-re matches a comma (preference over CSV) or simply
    two or more spaces (preference over SSV).

    It's worth reminding that the difference between a two-column *sv
    and a KVL of matching delimiter is that the latter has no headers
    (one-dimensional: just key and value), whereas the former has a
    header (two-dimensional: key, field, and value)

(h-type (ht  ("a" 1)("b" 2)))            => :ht
(h-type '("a" . 1))                      => :cons-pair
(h-type '("a" 1 :b 2))                   => :plist
(h-type [1 2 3])                         => :vector
(h-type '((:id  :name) ("01" "Alice")))  => :lol
(h-type "id,name\n01,Alice")             => :csv
h-type= (obj1 obj2 &rest more-objs)

Are all OBJECTS of same type?

For example, are they all hash tables? Or all org tables?
Or all lists of lists? Or all alists? Or all TSVs? JSONs?

Return type.

This isn't the same as type-of; it's more concerned with
types of interest for this conversion and lookup.

So, for example, it'd return :tsv for a TSV, while type-of
would return 'string.

(h-type= '("a" . 1) '("a" 1 :b 2))                  => nil
(h-type= (ht  ("a" 1)("b" 2)) (h* :c 10))           => :ht
(h-type= "id,name\n01,Alice" "id\tname\n01\tAlice") => nil
h-it= (obj1 obj2 &rest more-objs)

Are all OBJECTS, all of same type, reducible to each other?

Try to guess what they are.

The following tests are applied, and if any of them return t for
every pair of objects, they are considered same-type equals:
equal h-tsv= h-json=
h= h-csv= h-alist=
h-lol= h-ssv= h-plist=
h-orgtbl=.
If so, return type. Otherwise, return nil.

(h-it= '("a" . 1) '("a" 1 :b 2))                    => nil
(h-it= (ht  ("a" 1)("b" 2)) (h* :c 10))             => nil
(h-it= "id,name\n01,Alice" "id\tname\n01\tAlice")   => nil
(h-it= '(:a 1) '((:a . 1) (:a . 2)))                => nil
(h-it= '(:a 1) '(:a 1 :a 2))                        => :plist

(h-it= (ht  ("a" 1)("b" 2)) (h* "a" 1 "b" 2))       => :ht
(h-it= (h* :a (h* :b (h* :d 4
                         :c 3)))
       (h* :a (h* :b (h* :c 3
                         :d 4))))
=> :ht

(h-it= (h* :a (h* :b (h* :c 3 :d 4 :c 5)))
       (h* :a (h* :b (h* :c 5 :d 4))))
=> :ht

(h-it=  '((:a (:b (:d . 4)
                  (:c . 5))))
        '((:a (:b (:c . 5)
                  (:d . 4)
                  (:c . 3)))))
=> :alist

(h-it= (h* :a (h* :b (h* :c 3 :d 4 :c 5)))
       (h* :a (h* :b (h* :c 5 :d 4))))
=> :ht


;; Must be of same type
(h-it= (h* "a" (h* "b" (h* "c" 3 "d" 4)))
       (h->json*
        (h* "a" (h* "b" (h* "c" 3 "d" 4)))))
=> nil
h-it~ (obj1 obj2 &rest more-objs)

Are all OBJECTS equivalent according to XHT tests?

They are if, and only if, either they are equal or, when converted
to a hash table, h~= returns t for the pair. This means that
repeated keys (in alists, plists, TSVs etc) are dealt with, and
that, after that, ordering is irrelevant.

(h-it~ '("a" . 1) '("a" 1 :b 2))                    => nil
(h-it~ (ht  ("a" 1)("b" 2)) (h* :c 10))             => nil
(h-it~ "id,name\n01,Alice" "id\tname\n01\tAlice")   => t
(h-it~ '(:a 1) '((:a . 1) (:a . 2)))                => t
(h-it~ '(:a 1) '(:a 1 :a 2))                        => t
(h-it~ (ht  ("a" 1)("b" 2)) (h* "a" 1 "b" 2))       => t
(h-it~ (h* :a (h* :b (h* :d 4
                         :c 3)))
       (h* :a (h* :b (h* :c 3
                         :d 4))))
=> t

(h-it~ (h* :a (h* :b (h* :c 3 :d 4 :c 5)))
       (h* :a (h* :b (h* :c 5 :d 4))))
=> t

(h-it~  '((:a (:b (:d . 4)
                  (:c . 5))))
        '((:a (:b (:c . 5)
                  (:d . 4)
                  (:c . 3)))))
=> t

(h-it~ (h* :a (h* :b (h* :c 3 :d 4 :c 5)))
       (h* :a (h* :b (h* :c 5 :d 4))))
=> t


;; Different types
(h-it~ (h* "a" (h* "b" (h* "c" 3 "d" 4)))
       (h->json*
        (h* "a" (h* "b" (h* "c" 3 "d" 4)))))
=> t
(h-it~ (h* "a" (h* "b" (h* "c" 3 "d" 4)))
       "{
          \"a\": {
            \"b\": {
              \"c\": 3,
              \"d\": 4
            }
          }
        }")
=> t
Conversion

Functions to convert from/to hash tables.

To hash tables:
h<-kvl h<-lines
h<-vector h<-list
h<-lol h<-orgtbl
h<-alist h<-plist h<-alist* h<-plist* h<-json*
h<-tsv h<-csv h<-ssv

From hash tables:
h->kvl h->lines
h->vector h->list
h->lol h->orgtbl
h->alist h->plist h->alist* h->plist* h->json*
h->tsv h->csv h->ssv

You can navigate functions easily with:
M-. and M-, (elisp-slime-nav-mode)

Note that when results on both sides evaluate to hash tables, we use
for equality comparison the H=> symbol (which uses h=).

For getting (alist-get, plist-get, h-get, etc.):

alist, plist: Top-most values override bottom-most values
lol, org-table, *sv: Top-most values override bottom-most values
kvl: Bottom-most values override top-most values
vector, list, lines: Uniqueness guaranteed by index

Their nature:

kvl: no nesting, keys are given
lines: no nesting, keys = line number (starting at 0)
vector, list: may be nested¹, keys = indices
alist, plist, json: may be nested², keys are given
lol, org-table, *sv: tabular, assumed no nesting (no lols inside lols)

¹ treatment of nested not implemented for vectors and lists.
² treatment of nested implemented for alists, plists, and jsons —
both ways.

Hash table to hash table

Functions that read a hash table and return a hash table.

For shallow copying, h-copy is offered as an alias to
copy-hash-table. However, this function is not at all appropriate
for dealing with nested hash tables. See h-clone* for more.

h-clone* (table &optional sz ts we rs rt)

Create a deep copy of possibly-nested hash table TABLE.

Optional arguments after TABLE are SZ, TS, WE, RS, and RT — which
correspond to, respectively, size, test, weakness, rehash-size,
rehash-threshold. When nil, they match that of TABLE.

The result has the same elements and structure of TABLE, but any
nested (internal) hash tables are recursively replaced by new ones.
This allows you to modify the internal hash tables without altering
the original one.

This is not possible with copy-hash-table, whose copy is shallow
and any internal tables that might be present in the copy will
point to the very same objects of the original one. Likewise with
ht-copy, which uses copy-hash-table, and h-copy, which is an
alias to the latter.

You also shouldn't use shallow copying if you intend to apply
destructive functions such as sort, nconc, or nreverse to any
list values of the copied table, as this will likely modify the
original. To avoid that, either use non-destructive alternatives
such as -sort and reverse; or h-clone* the original instead
of using h-copy.

See also: h<-ht.

;;;; Like h-copy
(-> (h-new 10 'eq)
    (h-put :a 1)
    (h-put :b 2)
    h-clone*)
Hp==> (h-st* 10 'eq :a 1 :b 2)

(-> (h-new 10 'eq)
    (h-put :a 1)
    (h-put* :b :c 3)
    h-clone*)
Hp==> (h-st* 10 'eq :a 1 :b (h* :c 3))

;;;; Unlike h-copy, it does not modify the original's nested ones
;;;;; Compare:

(let* ((tbl (h-st* 10 'eq :a 1 :b (h* :c 3)))
       (new (h-copy tbl)))
  (h-put*! new :a 2)
  (h-put*! new :b :c 5)
  (h-put*! new :b :d 4)
  tbl)
Hp==> (h-st* 10 'eq
             :a 1
             :b (h* :c 5
                    :d 4)) ;<-- the original's nested is modified!

(let* ((tbl (h-st* 10 'eq :a 1 :b (h* :c 3)))
       (new (h-clone* tbl)))
  (h-put*! new :a 2)
  (h-put*! new :b :c 5)
  (h-put*! new :b :d 4)
  tbl)
Hp==> (h-st* 10 'eq
             :a 1
             :b (h* :c 3)) ;<-- the original's nested is preserved

;;;;; I didn't use h-put* above because h-put* is non-destructive:

(let* ((tbl (h-st* 10 'eq :a 1 :b (h* :c 3)))
       (new (h-copy tbl)))
  (-> new
      (h-put* :b :d 4)
      (h-put* :b :c 5)))
Hp==> (h-st* 10 'eq
             :a 1
             :b (h* :c 5
                    :d 4)) ;<-- new one from the operation, but...

(let* ((tbl (h-st* 10 'eq :a 1 :b (h* :c 3)))
       (new (h-copy tbl)))
  (-> new
      (h-put* :b :d 4)
      (h-put* :b :c 5))
  tbl)
Hp==> (h-st* 10 'eq
             :a 1
             :b (h* :c 3)) ;<-- ... the original is preserved

;;;;; In fact we don't need h-copy with h-put or h-put*.
;;;;; It uses h-clone* behind the scenes — so it'd h-clone* tbl,
;;;;; which would stay unmodified.

(let* ((tbl (h-st* 10 'eq :a 1 :b (h* :c 3))))
  (-> tbl
      (h-put* :b :d 4)
      (h-put* :b :c 5))
  tbl)
Hp==> (h-st* 10 'eq
             :a 1
             :b (h* :c 3))
h<-ht (table &optional sz ts we rs rt)

Given hash table TABLE, return it or a clone, depending on params.

Optional arguments after TABLE are SZ, TS, WE, RS, and RT — which
correspond to, respectively, size, test, weakness, rehash-size,
rehash-threshold. When nil, they match that of TABLE.

If any of the optional arguments passed differs from the TABLE's
corresponding properties, h-clone* the table with these
parameters. Otherwise just return TABLE.

This is a non-destructive function: TABLE isn't changed.

See also: h-clone*.

(h<-ht (ht-create 'eq) nil 'equal)  Hp=> (h-new nil 'equal)
(h<-ht (ht-create 'eq) 20  'equal)  Hp=> (h-new 20  'equal)
(h<-ht (ht (:a 1) (:b 2)))          Hp=> (ht (:a 1) (:b 2))
(h<-ht (h* :a 1 :b 2))              Hp=> (h* :a 1 :b 2)
(h<-ht (ht))                        Hp=> (ht-create)
h-2d<-1d (table &optional f1 f2 size test)

Make 2D the 1D hash table TABLE by adding header fields F1, F2.

If F1 is nil, use the string "key". If F2 is nil, use "value".

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

Alias: h-1d->2d.

(h-2d<-1d (ht (0 "alice") (1 "bob")))
H=> (ht (0 (ht ("key" 0) ("value" "alice")))
        (1 (ht ("key" 1) ("value" "bob"))))

(h-2d<-1d (ht (0 "alice") (1 "bob")) :idx :name)
H=> (ht (0 (ht (:idx 0) (:name "alice")))
        (1 (ht (:idx 1) (:name "bob"))))
h-1d<-2d (table2d &optional size test)

Make 1D (KVL-like) the 2D hash table TABLE2D.

Infer current header with h-2d-header.
If header isn't of length 2, signal error.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

Alias: h-2d->1d.

(h-1d<-2d (ht (0 (ht ("key" 0) ("value" "alice")))
              (1 (ht ("key" 1) ("value" "bob")))))
H=> (ht (0 "alice") (1 "bob"))
(h-1d<-2d (ht (0 "alice") (1 "bob"))) !!> error
(h-1d<-2d (ht (0 "alice")))           !!> error
(h-1d<-2d (ht-create))                !!> error
1D: implicit (indices as keys)

Functions in this category convert from other formats to hash table or
vice-versa. Key–value pairs are of dimension 1: not nested. Indices serve as
keys.

Vectors
h<-vector (vector &optional size test)

Convert vector VECTOR to a hash table using VECTOR's indices as keys.

Keys are by default of integer type, not strings, and start at 0.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

(h<-vector  ["alice" "bob"])        H=> (h* 0 "alice" 1 "bob")
(h<-vector  [])                     H=> (ht)
h->vector (table)

Convert hash table TABLE to vector.

Do the opposite of h<-vector, which see. Return vector.

Select only those keys that are natural numbers (an integer ≥0) or
strings that could be converted to a natural number, which it
automatically does.

Any holes in the number sequence from 0 to max(keys) are filled
in the vector with nil.

If the table has keys that reduce to the same integer, such as
having both 3 and "3" as keys, behavior is unpredictable.

(h->vector (h* 1 "alice" 5 "emily"))
=> [nil "alice" nil nil nil "emily"]

(h->vector (h* 'x "bob" 0 "alice" :y "emily")) => ["alice"]

(h->vector (h* -1  "alice"  1  "bob"))         => [nil "bob"]

(h->vector (h* "1" "alice" "2" "bob"))         => [nil "alice" "bob"]
(h->vector (h*  1  "alice" "2" "bob"))         => [nil "alice" "bob"]
(h->vector (h* "1" "alice"  2  "bob"))         => [nil "alice" "bob"]
(h->vector (h*  1  "alice"  2  "bob"))         => [nil "alice" "bob"]

(h->vector (h* "0" "alice" "1" "bob"))         => ["alice" "bob"]
(h->vector (h*  0  "alice" "1" "bob"))         => ["alice" "bob"]
(h->vector (h* "0" "alice"  1  "bob"))         => ["alice" "bob"]
(h->vector (h*  0  "alice"  1  "bob"))         => ["alice" "bob"]
(h->vector (ht   (0  "alice")))                => ["alice"]

(h->vector (ht))                               => []
(h->vector (ht-create))                        => []
Lists
h<-list (list &optional size test)

Convert LIST to a hash table using LIST's indices as keys.

Keys are by default of integer type, not strings, and start at 0.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

(h<-list '())                    H=> (ht)
(h<-list '("alice" "bob"))       H=> (h* 0 "alice" 1 "bob")
h->list (table)

Convert hash table TABLE to list.

Select only those keys that are natural numbers (an integer ≥0)
or strings that could be converted to a natural number, which it
automatically does.

Any holes in the number sequence from 0 to max(keys) are filled
in the list with nil.

If the table has keys that reduce to the same integer, such as
having both 3 and "3" as keys, behavior is unpredictable.

(h->list (h* 1 "alice" 5 "emily"))
=> '(nil "alice" nil nil nil "emily")

(h->list (h* 'x "bob" 0 "alice" :y "emily")) => '("alice")

(h->list (h* "1" "alice" "2" "bob"))       => '(nil "alice" "bob")
(h->list (h*  1  "alice" "2" "bob"))       => '(nil "alice" "bob")
(h->list (h* "1" "alice"  2  "bob"))       => '(nil "alice" "bob")
(h->list (h*  1  "alice"  2  "bob"))       => '(nil "alice" "bob")

(h->list (h* "0" "alice" "1" "bob"))       => '("alice" "bob")
(h->list (h*  0  "alice" "1" "bob"))       => '("alice" "bob")
(h->list (h* "0" "alice"  1  "bob"))       => '("alice" "bob")
(h->list (h*  0  "alice"  1  "bob"))       => '("alice" "bob")
(h->list (ht   (0  "alice")))              => '("alice")

(h->list (ht))                             => '()
(h->list (ht-create))                      => '()
Lines
h<-lines (str &optional size test)

Convert string STR to a hash table using line numbers as keys.

Keys are by default of integer type, not strings, and start at 0.

The choice of zero for the first line number (instead of 1) was due
to transitivity: a plain conversion from lines to hash table to
vector to lines should yield the initial string. The alternatives
seemed worse.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

(h<-lines "alice\nbob")    H=> (h* 0 "alice" 1 "bob")
(h<-lines "  \n   \n")     H=> (h* 0 "  " 1 "   " 2 "")
(h<-lines "")              H=> (h* 0 "")
(h<-lines "alice\nbob" 5) Hp=> (h-s* 5 0 "alice" 1 "bob")
(h<-lines "" 5 'eq)       Hp=> (h-st* 5 'eq 0 "")
(h<-lines '(a b c))        !!> error
(h<-lines nil)             !!> error
h->lines (table)

Convert hash table TABLE whose keys are numbers to string.

Select only those keys that are natural numbers (an integer ≥0)
or strings that could be converted to a natural number, which it
automatically does.

Any holes in the number sequence from 0 to max(keys) are filled
in the string with empty lines.

If the table has keys that reduce to the same integer, such as
having both 3 and "3" as keys, behavior is unpredictable.

(h->lines (h* 1 "alice" 5 "emily")) => "\nalice\n\n\n\nemily"
(h->lines (h* 'x "bob" 0 "alice" :y "emily")) => "alice"

(h->lines (h* -1  "alice"  1  "bob"))         => "\nbob"

(h->lines (h* "1" "alice" "2" "bob"))         => "\nalice\nbob"
(h->lines (h*  1  "alice" "2" "bob"))         => "\nalice\nbob"
(h->lines (h* "1" "alice"  2  "bob"))         => "\nalice\nbob"
(h->lines (h*  1  "alice"  2  "bob"))         => "\nalice\nbob"

(h->lines (h* "0" "alice" "1" "bob"))         => "alice\nbob"
(h->lines (h*  0  "alice" "1" "bob"))         => "alice\nbob"
(h->lines (h* "0" "alice"  1  "bob"))         => "alice\nbob"
(h->lines (h*  0  "alice"  1  "bob"))         => "alice\nbob"
(h->lines (ht   (0  "alice")))                => "alice"

(h->lines (ht   (0  "")))                     => ""
(h->lines (ht))                               => ""
(h->lines (h-new))                            => ""
1D: explicit

Functions in this category convert from other formats to hash table or
vice-versa. Strings with one key–value pair per line are of dimension 1: not
nested. Likewise cons pairs.

Key–Value Lines
h<-kvl (kvl &optional size test obsolete)

Convert key–value lines KVL to a hash table.

Optional arguments SIZE and TEST may be passed.

A KVL is the equivalent of a non-nested alist translated to a
simple flat config file as used in Unix-like systems.

It should look, for example, like this:

--- data begins --->
key1 = val1
key2 = val2
key3 = val3
<--- data ends -----

One pair per line. There is no header. If there were, it'd be taken
as another key–value line.

The field delimiter (separator) to be used is given by the value of
the regular expression in the variable h-kvl-sep-re, which see.
By default it's equal sign surrounded or not by spaces. The
variable can be temporarily let-bound when change is desirable.

Comments are non-destructively stripped before conversion. Comments
are anything matching h-kvl-comment-re (which see), which can
also be temporarily let-bound when some other comment regex is
expected in KVL.

Note that sections (as used in .ini files and some types of .conf)
aren't implemented or dealt with here. KVLs are to be considered
flat. So lines with [Section name] are not considered as nodes, and
no tree-like structure will be generated. These section lines, if
present, are ignored by the default h-kvl-section-re (which see)
as if they were comments, since this may be useful if the file
you're processing has sections only for information purposes and
the keys inside them are unique. If this is not the case, then this
function is not suitable for this conversion. An option would be to
pre-convert the input to JSON using some other tool, and then apply
h<-json* to the result.

Also note that in an alist, when two elements have equal key, the
first is used. With KVL data, we'll consider that the last row
wins. This is because in an alist a new pair is pushed to the top,
whereas KVL files are usually (but not always, as there are varying
standards) read from top to bottom.

An OBSOLETE form of calling this function is
(h<-kvl kvl &optional sep size test)
where the optional arg SEP specified the field delimiter. This use
is deprecated. The delimiter is now passed by the variable
h-kvl-sep-re, which should be let-bound when the default is not
the one expected in the KVL to be converted.

  ;;;; When expected separator is " *= *"
  (let ((kvl4 "Mary = 27\nJohn = 20\nAnne = 21\nAnne = 42\nJohn = 31\n"))
    (h<-kvl kvl4))
  H=> (h* "Mary" "27"
          "John" "31"
          "Anne" "42")

  ;;;; When expected separator is different
  (let ((kvl4 "Mary 27\nJohn 20\nAnne 21\nAnne 42\nJohn 31\n")
        (h-kvl-sep-re " "))
    (h<-kvl kvl4))
  H=> (h* "Mary" "27"
          "John" "31"
          "Anne" "42")

  (let ((h-kvl-sep-re ":"))
    (h<-kvl "Alice:42\nBob:30"))
  H=> (h* "Alice" "42"
          "Bob"   "30")

  (let ((h-kvl-sep-re " *: *"))
    (h<-kvl "\
Alice : 42
Bob   : 30"))
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; Leading and trailing whitespace
  (h<-kvl "  Alice = 42
             Bob   = 30  ")
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; Empty lines
  (h<-kvl "
       Alice  =  42

         Bob  =  30
")
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; Comments
  (h<-kvl "\
# Some people here
Alice = 42  # Alice's real age!
Bob   = 30")
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; [Sections] in simple (flat) .conf files
  (h<-kvl "[People]
           Alice = 42
           Bob   = 30")
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; When expected section regex is different
  (let ((h-kvl-section-re "^[ \t]*{.*}[ \t]*$"))
    (h<-kvl "\
{People}
Alice = 42
Bob   = 30
Emily = 21"))
  H=> (h* "Alice" "42"
          "Bob"   "30"
          "Emily" "21")

  ;;;; When expected comment regex is different
  (let ((h-kvl-comment-re " *;.*"))
    (h<-kvl "\
;; Some people
Alice = 42  ; Alice's real age!
Bob   = 30"))
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; Handling a rather unusual KVL
  (let ((h-kvl-sep-re " *~ *")
        (h-kvl-comment-re " *;.*")
        (h-kvl-section-re "^[ \t]*{.*}[ \t]*$"))
    (h<-kvl "
              {Some people}

              ;; We start with Alice:
              Alice ~ 42  ; Alice's real age!

              ;; Bob below:
                Bob ~ 30
"))
  H=> (h* "Alice" "42"
          "Bob"   "30")

  ;;;; Empty things create empty hash tables
  (h<-kvl "")            Hp=> (h-new 1)
  (h<-kvl "  ")          Hp=> (h-new 1)
  (h<-kvl "\t")          Hp=> (h-new 1)

  ;;;; Obsolete calling convention — DEPRECATED
  (with-no-warnings
    (let ((kvl4 "Mary 27\nJohn 20\nAnne 21\nAnne 42\nJohn 31\n"))
      (h<-kvl kvl4 " ")))
  H=> (h* "Mary" "27"
          "John" "31"
          "Anne" "42")

  (with-no-warnings
    (let ((kvl4 "Mary 27\nJohn 20\nAnne 21\nAnne 42\nJohn 31\n"))
      (h<-kvl kvl4 " " 5)))
  H=> (h* "Mary" "27"
          "John" "31"
          "Anne" "42")

  (with-no-warnings
    (let ((kvl4 "Mary 27\nJohn 20\nAnne 21\nAnne 42\nJohn 31\n"))
      (h<-kvl kvl4 " " 5 'equal)))
  H=> (ht ("Mary" "27")
          ("John" "31")
          ("Anne" "42"))
h->kvl (table &optional sep)

Convert hash table TABLE to SEP-separated key–value lines.

The optional separator SEP is a literal string, and could be, for
example, " = " or ":" or " ". When nil, default to "=".

  ;;;; Regular
  (h->kvl (h* "Mary" "27"
              "John" "31"
              "Anne" "42"))
  =>  "Mary=27\nJohn=31\nAnne=42\n"

  ;; and KVLs with or without trailing \n are [[#h-kvl=][h-kvl=]] equivalent:
  (h-kvl= "Mary=27\nJohn=31\nAnne=42\n"
          "Mary=27\nJohn=31\nAnne=42")
  => t

  ;; so:
  (h->kvl (h* "Mary" "27"
              "John" "31"
              "Anne" "42"))
  K=> "Mary=27\nJohn=31\nAnne=42"

  ;;;; Empty hash table creates an empty string
  (h->kvl (h*))  => ""

  ;;;; Different separator
  (h->kvl (h* "Mary" "27"
              "John" "31"
              "Anne" "42")
          " : ")
  =>  "\
Mary : 27
John : 31
Anne : 42
"

  (h->kvl (ht ("Mary""27")("John""31")("Anne""42")) "\t")
  K=> "Mary\t27\nJohn\t31\nAnne\t42"

  ;;;; Result's type is string, regardless of origin's types
  (h->kvl (ht ('Mary  27) ('John  31) ('Anne  42)) " = ")
  => "Mary = 27\nJohn = 31\nAnne = 42\n"

  (h->kvl (ht ("Mary""27")("John""31")("Anne""42")) " = ")
  => "Mary = 27\nJohn = 31\nAnne = 42\n"
Cons Pairs
h<-cons-pair (cons-pair &optional size test)

Convert CONS-PAIR to hash table.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

(h<-cons-pair '("a" . 1))     H==> (h* "a" 1)
(h<-cons-pair '(:a . 1))      H==> (h* :a 1)
(h<-cons-pair '(a . 1))       H==> (h* 'a 1)
(h<-cons-pair '(a . 1) 5 'eq) H==> (h-st* 5 'eq 'a 1)
(h<-cons-pair '(a 1))          !!> error
(h<-cons-pair '(a))            !!> error
(h<-cons-pair '())             !!> error
h->cons-pair (table)

Convert hash table TABLE to cons pair.

(h->cons-pair (h* "a" 1))       => '("a" . 1)
(h->cons-pair (h* :a 1))        => '(:a . 1)
(h->cons-pair (h* 'a 1))        => '(a . 1)
(h->cons-pair (h* :a 1 :b 2))  !!> error
(h->cons-pair (h*))            !!> error
≥1D unspecific (possibly nested)

Functions in this category convert from other formats to hash table or
vice-versa. Key–value pairs are of undetermined dimension >1 (nested).

Association lists
from alist
  • h<-alist (alist &optional size test)

    Convert ALIST to hash table.

    Similar to ht<-alist, except that it preserves original order and
    SIZE can also be passed as an argument. When SIZE is nil, it
    defaults to the result of applying xht--init-size to half the
    plist's length.

    For the meaning of TEST, see h-new.

    Like ht<-alist, this function is not aware of nested alists. Its
    nesting-aware counterpart is h<-alist*.

    ;;;; Won't convert nested: values remain as alists
    (h<-alist '((a (b (c (d . 4))))))
    H_=> (h* 'a '((b (c (d . 4)))))
    
    (h<-alist '((a . 1)
                (b (c . 3) (d . 4))
                (e . 5)
                (f (g . 7) (h . 8))
                (e . 2)))
    H_=> (h* 'a 1
             'e 5
             'b '((c . 3) (d . 4))
             'f '((g . 7) (h . 8)))
    
    (h<-alist '(("a" . 1)
                ("b" . (("c" . 3)
                        ("d" . 4)))))
    H_=> #s(hash-table test equal data
                       ("b" (("c" . 3)
                             ("d" . 4))
                        "a" 1))
    
    (h<-alist   '(("a" . 1)
                  ("b" . (("c" . 3)
                          ("d" . 4)))))
    H_=> (ht<-alist '(("a" . 1)
                      ("b" . (("c" . 3)
                              ("d" . 4)))))
    
    (h<-alist '((a . 1) (b (c . 3))))      H_=> (h* 'a 1 'b '((c . 3)))
    
    ;;;; Basic (expected: as h<-alist)
    (h<-alist '((a . 1) (b . 2) (a . 3)))  H=> (h* 'a 1 'b 2)
    (h<-alist '((a . 1) (b . 2)))          H=> (h* 'a 1 'b 2)
    (h<-alist '((a . 1)))                  H=> (h* 'a 1)
    (h<-alist '((a . 1) (b)))              H=> (h* 'a 1 'b nil)
    (h<-alist '())                         H=> (h*)
    (h<-alist '(a))                        !!> error
    (h<-alist [a 1])                       !!> error
    (h<-alist '((a . 1) b))                !!> error
    (h<-alist '())                         H=> (ht)
    (h<-alist '(()))                       H=> (ht (nil nil))
    (h<-alist '(("a" . 1) ("b" . 2)))
    H=> #s(hash-table test equal data ("b" 2 "a" 1))
    
    (h<-alist '(("a" . 1) ("b" . 2)))
    H=> (ht<-alist '(("a" . 1) ("b" . 2)))
    
    (h<-alist '(("a" . 1))) H=> (ht<-alist '(("a" . 1)))
    (h<-alist '(()))        H=> (ht<-alist '(()))
    (h<-alist '())          H=> (ht<-alist '())
    
  • h<-alist* (alist &optional size test)

    Convert possibly nested ALIST to hash table.

    If the ALIST is nested, each alist found as value will be
    recursively converted into a hash table as well.

    SIZE is the nominal initial size of the table to be created.
    When SIZE is nil, it defaults to the result of applying
    xht--init-size to the alist's length.

    For the meaning of TEST, see h-new.

    This function is aware of nested alists. Its non–nesting-aware
    counterpart is h<-alist.

    ;;;; Typical: converts nested
    (h<-alist* '((a (b (c (d . 4))))))
    H=> (h* 'a (h* 'b (h* 'c (h* 'd 4))))
    
    (h<-alist* '((a . 1)
                 (b (c . 3) (d . 4))
                 (e . 5)
                 (f (g . 7) (h . 8))
                 (e . 2)))
    H=> (h* 'f (h* 'h 8
                   'g 7)
            'b (h* 'd 4
                   'c 3)
            'e 5
            'a 1)
    
    (h<-alist* '(("a" . 1)
                 ("b" . (("c" . 3)
                         ("d" . 4)))))
    H=> #s(hash-table test equal data
                      ("b" #s(hash-table test equal data ("d" 4 "c" 3))
                       "a" 1))
    
    (h<-alist*   '(("a" . 1)
                   ("b" . (("c" . 3)
                           ("d" . 4)))))
    H=> (ht<-alist `(("a" . 1)
                     ("b" . ,(ht<-alist '(("c" . 3)
                                          ("d" . 4))))))
    
    (h<-alist* '((a . 1) (b (c . 3))))      H=> (h* 'a 1 'b (h* 'c 3))
    
    ;;;; Basic (expected: as h<-alist)
    (h<-alist* '((a . 1) (b . 2) (a . 3)))  H=> (h* 'a 1 'b 2)
    (h<-alist* '((a . 1) (b . 2)))          H=> (h* 'a 1 'b 2)
    (h<-alist* '((a . 1)))                  H=> (h* 'a 1)
    (h<-alist* '((a . 1) (b)))              H=> (h* 'a 1 'b nil)
    (h<-alist* '())                         H=> (h*)
    (h<-alist* '(a))                        !!> error
    (h<-alist* '((a . 1) b))                !!> error
    (h<-alist* [a 1])                       !!> error
    (h<-alist* '())                         H=> (ht)
    (h<-alist* '(()))                       H=> (ht (nil nil))
    (h<-alist* '(("a" . 1) ("b" . 2)))
    H=> #s(hash-table test equal data ("b" 2 "a" 1))
    
    (h<-alist* '(("a" . 1) ("b" . 2)))
    H=> (ht<-alist '(("a" . 1) ("b" . 2)))
    
    (h<-alist* '(("a" . 1))) H=> (ht<-alist '(("a" . 1)))
    (h<-alist* '(()))        H=> (ht<-alist '(()))
    (h<-alist* '())          H=> (ht<-alist '())
    
to alist
  • h->alist (table)

    Convert hash table TABLE to alist.

    Similar to current ht->alist, with the difference that it
    preserves the hash table's order of keys.

    Like ht->alist, this function is not aware of nested alists. Its
    nesting-aware counterpart is h->alist*.

    ;;;; nested: doesn't recurse
    (h->alist (ht ("a" "1")
                  ("b" (ht (:c 3)
                           ('d "4")))))
    A=> `(("a" . "1")
          ("b" . ,(ht (:c 3)
                      ('d "4"))))
    
    (h->alist
     (h*  'id         1234
          'payload    (h* 'url     "https://example.com"
                          'title   "Example Domain"
                          'content "This domain is...")))
    A=> `((id      .  1234)
          (payload . ,(h* 'url     "https://example.com"
                          'title   "Example Domain"
                          'content "This domain is...")))
    ;; this alist example ^ is from elisp-demos
    
    ;;;; not-nested
    (h->alist (ht ("a" 1) ("b" 2))) A=> '(("a" . 1) ("b" . 2))
    (h->alist (h*))     => '()
    (h->alist (h-new))  => '()
    
  • h->alist* (table)

    Convert hash table TABLE to alist, maybe recursing.

    If the ALIST is nested, each alist found as value will be
    recursively converted into a hash table as well.

    This function is aware of nested alists. Its non–nesting-aware
    counterpart is h->alist.

    ;;;; nested: recurses
    (h->alist* (ht ("a" "1")
                   ("b" (ht (:c 3)
                            ('d "4")))))
    A=> '(("a" . "1")
          ("b" . ((:c . 3)
                  (d  . "4"))))
    
    (h->alist*
     (h*  'id   1234
          'payload (h* 'url     "https://example.com"
                       'title   "Example Domain"
                       'content "This domain is...")))
    A=> '((id . 1234)
          (payload (url     . "https://example.com")
                   (title   . "Example Domain")
                   (content . "This domain is...")))
    ;; this alist example ^ is from elisp-demos
    
    ;;;; not-nested
    (h->alist* (ht ("a" 1) ("b" 2))) A=> '(("a" . 1) ("b" . 2))
    (h->alist* (h*))     => '()
    (h->alist* (h-new))  => '()
    
Property lists
from plist
  • h<-plist (plist &optional size test)

    Convert PLIST to hash table.

    Similar to ht<-plist, except that it preserves original order and
    SIZE can also be passed as an argument. When SIZE is nil, it
    defaults to the result of applying xht--init-size to half the
    plist's length.

    For the meaning of TEST, see h-new.

    This function is not aware of nested plists. Its nesting-aware
    counterpart is h<-plist*.

    ;;;; Won't convert nested: values remain as plists
    (h<-plist '(:a (:b (:c (:d 4)))))
    H_=> (h* :a '(:b (:c (:d 4))))
    
    (h<-plist  '(:a 1 :b (:c 3 :d 4) :e 5 :f (:g 7 :h 8) :e 2))
    H_=> (h* :f '(:h 8 :g 7)
             :b '(:d 4 :c 3)
             :e 5
             :a 1)
    
    (h<-plist  '(:a 1 :b (:c 3)))      H_=> (h* :a 1 :b '(:c 3))
    
    ;;;; Basic
    (h<-plist  '(:a 1 :b 2 :c 3 :b 4)) H=> (h* :a 1 :b 2 :c 3)
    (h<-plist  '(:a 1 :b 2 :a 3))      H=> (h* :a 1 :b 2)
    (h<-plist  '(:a 1 :b 2))           H=> (h* :a 1 :b 2)
    (h<-plist  '(:a 1))                H=> (h* :a 1)
    (h<-plist  '())                    H=> (h*)
    (h<-plist  '(:a 1 :b))             H=> (h* :a 1)
    (h<-plist  '(:a))                  H=> (h*)
    (h<-plist  [a 1])                  !!> error
    
  • h<-plist* (plist &optional size test)

    Convert possibly nested PLIST to hash table.

    If the PLIST is nested, each plist found as value will be
    recursively converted into a hash table as well.

    SIZE is the nominal initial size of the table to be created.
    When SIZE is nil, it defaults to the result of applying
    xht--init-size to half the plist's length.

    For the meaning of TEST, see h-new.

    This function is aware of nested plists. Its non–nesting-aware
    counterpart is h<-plist.

    ;;;; Typical: converts nested
    (h<-plist* '(:a (:b (:c (:d 4)))))
    H=> (h* :a (h* :b (h* :c (h* :d 4))))
    
    (h<-plist* '(:a 1 :b (:c 3 :d 4) :e 5 :f (:g 7 :h 8) :e 2))
    H=> (h* :f (h* :h 8
                   :g 7)
            :b (h* :d 4
                   :c 3)
            :e 5
            :a 1)
    
    (h<-plist* '(:a 1 :b (:c 3)))      H=> (h* :a 1 :b (h* :c 3))
    
    ;;;; Basic (expected: as h<-plist)
    (h<-plist* '(:a 1 :b 2 :c 3 :b 4)) H=> (h* :a 1 :b 2 :c 3)
    (h<-plist* '(:a 1 :b 2 :a 3))      H=> (h* :a 1 :b 2)
    (h<-plist* '(:a 1 :b 2))           H=> (h* :a 1 :b 2)
    (h<-plist* '(:a 1))                H=> (h* :a 1)
    (h<-plist* '())                    H=> (h*)
    (h<-plist* '(:a 1 :b))             H=> (h* :a 1)
    (h<-plist* '(:a))                  H=> (h*)
    (h<-plist* [a 1])                  !!> error
    
to plist
  • h->plist (table)

    Convert hash table TABLE to plist.

    Similar to current ht->plist, with the difference that it
    preserves the hash table's order of keys.

    This function is not aware of nested plists. Its nesting-aware
    counterpart is h->plist*.

    ;;;; nested: doesn't recurse
    (h->plist (ht ("a" "1")
                  ("b" (ht (:c 3)
                           ('d "4")))))
    P=> `("a"  "1"
          "b"  ,(ht (:c 3)
                    ('d "4")))
    
    (h->plist
     (h* 'id      1234
         'payload (h* 'url     "https://example.com"
                      'title   "Example Domain"
                      'content "This domain is...")))
    P=> `(id 1234 payload ,(h* 'url     "https://example.com"
                               'title   "Example Domain"
                               'content "This domain is..."))
    ;; this example was adapted from elisp-demos
    
    ;;;; not-nested
    (h->plist (ht ("a" 1) ("b" 2))) P=> '("a" 1 "b" 2)
    (h->plist (h*))     => '()
    (h->plist (h-new))  => '()
    
  • h->plist* (table)

    Convert hash table TABLE to plist, maybe recursing.

    If the PLIST is nested, each plist found as value will be
    recursively converted into a hash table as well.

    This function is aware of nested plists. Its non–nesting-aware
    counterpart is h->plist.

    ;;;; nested: recurses
    (h->plist* (ht ("a" "1")
                   ("b" (ht (:c 3)
                            ('d "4")))))
    P=> '("a" "1"
          "b" (:c 3 d "4"))
    
    (h->plist*
     (h* 'id      1234
         'payload (h* 'url     "https://example.com"
                      'title   "Example Domain"
                      'content "This domain is...")))
    P=> '(id 1234 payload (url
                           "https://example.com"
                           title
                           "Example Domain"
                           content
                           "This domain is..."))
    ;; this example was adapted from elisp-demos
    
    ;;;; not-nested
    (h->plist* (ht ("a" 1) ("b" 2))) P=> '("a" 1 "b" 2)
    (h->plist* (h*))     => '()
    (h->plist* (h-new))  => '()
    
JSON
h<-json* (json &optional size test)

Convert possibly nested JSON to hash table.

If JSON is a string enclosed in [] instead of {}, the first step of
the conversion will output a vector instead of directly a hash
table. The vector will be inspected, and:

  • If every item is a hash table, guess from it the ID for creating
    a 2D hash table, and return such table.
  • Otherwise, treat it as vector and convert to hash table as usual:
    indices as numeric keys, items as values.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

;;;; Simple, nested
(h<-json* "{
            \"Amy\": \"dog\",
            \"Bob\": {
               \"cat\": 2,
               \"fish\": 11
              }
           }")
H=> (h* "Amy" "dog"
        "Bob" (h* "cat" 2 "fish" 11))

;;;; Vector
(h<-json* "[\"k\",4,2,\"x\",\"e\"]")
H=> (h* 0 "k"
        1 4
        2 2
        3 "x"
        4 "e")

;;;; This next JSON string is an example from p-baleine's jq.el (github)
(let ((input "
  [
    {
      \"name\": \"Ness\",
      \"age\": 12,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
    },
    {
      \"name\": \"Paula\",
      \"age\": 11,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
    }
  ]"))
  (h<-json* input))
H=> (h* "Ness"  (h* "name"   "Ness"
                    "age"    12
                    "origin" (h* "country" "Eagleland"
                                 "town"    "Onett"))
        "Paula" (h* "name"   "Paula"
                    "age"    11
                    "origin" (h* "country" "Eagleland"
                                 "town"    "Twoson")))

;; p-baleine has an example of how to use jq.el to collect the towns,
;; returning:   '("Onett" "Twoson")
;;
;; Solution offered:
;;  (cl-loop for x iter-by (jq input ".[] | .origin.town") collect x)
;;
;; It assumes you have jq installed and a few other things.
;;
;; How could we accomplish the same with XHT?
;;
;; First, a longer solution, which uses h--lmap with Dash's -let
;; destructuring:
(let ((input "
  [
    {
      \"name\": \"Ness\",
      \"age\": 12,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
    },
    {
      \"name\": \"Paula\",
      \"age\": 11,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
    }
  ]"))
  (--> input h<-json*
       (h--lmap (-let [(&hash key (&hash "origin" (&hash "town"))) it]
                  town)
                it)))
=> '("Onett" "Twoson")

;; Here's a better way:
(let ((input "
  [
    {
      \"name\": \"Ness\",
      \"age\": 12,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
    },
    {
      \"name\": \"Paula\",
      \"age\": 11,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
    }
  ]"))
  (--> input h<-json* (h--lmap (h-get* it key "origin" "town") it)))
=> '("Onett" "Twoson")

;; So h-get* is more straightforward.

;; But no need to 'it key', since 'value is let-bound. So we have a
;; one-char shorter solution with h-get* — eight, actually, since we
;; won't need the -->.

(let ((input "
  [
    {
      \"name\": \"Ness\",
      \"age\": 12,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
    },
    {
      \"name\": \"Paula\",
      \"age\": 11,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
    }
  ]"))
  (h--lmap (h-get* value "origin" "town")
           (h<-json* input)))
=> '("Onett" "Twoson")

;; But there's a still shorter one, using h-let:
(let ((input "
  [
    {
      \"name\": \"Ness\",
      \"age\": 12,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
    },
    {
      \"name\": \"Paula\",
      \"age\": 11,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
    }
  ]"))
  (h--lmap (h-let value
             .origin.town)
           (h<-json* input)))
=> '("Onett" "Twoson")

;; So with h--lmap, the value of every pair is let-bound to the variable
;; 'value — which is itself a hash-table, so we can apply h-let to it
;; and fetch .origin.town.

;; Compare:
;;  (cl-loop for x iter-by (jq input ".[] | .origin.town") collect x)
;; with
;;  (--> input h<-json* (h--lmap (h-get* it key "origin" "town") it))
;; and
;;  (h--lmap (h-get* value "origin" "town") (h<-json* input))
;; and
;;  (h--lmap (h-let value .origin.town) (h<-json* input))

;;;; Note about back-and-forth conversion
;; Notice that the below is the string resulting from that conversion
;; from h->json*, and that its result is not equal to the original
;; hash table. It's NOT guaranteed that:
;;   (->> htbl  h->json*  h<-json*)   H=>   htbl
;; because of translation of keys and non-numeric values into strings;
;; translation of nested values into hash tables; of list-values into
;; vector-values; etc.
(h<-json* "{
             \"a\": {
               \"b\": [
                 \"k\",
                 4,
                 2,
                 \"x\",
                 \"e\"
               ]
             },
             \"c\": {
               \"d\": {
                 \"y\": 2,
                 \"z\": 3
               }
             }
           }")
H=> (h* "a" (h* "b" ["k" 4 2 "x" "e"])
        "c" (h* "d" (h* "y" 2
                        "z" 3)))

(h<-json* "{}") H=> (h-new)
h->json* (table &optional no-pp sort)

Convert hash table TABLE to JSON, maybe recursing.

If NO-PP is nil, return formatted (pretty-printed string).
If NO-PP is non-nil, return as is (compact string).

If SORT is non-nil, return it sorted. Otherwise don't sort it.

(h->json* (h* "Amy" "dog"
              "Bob" (h* "cat" 2 "fish" 11)))
J=> "{
       \"Amy\": \"dog\",
       \"Bob\": {
           \"cat\": 2,
          \"fish\": 11
        }
     }"

(h->json* (h* 'a (h* 'b '(k 4 2 x e))
              :c (h* :d '((y . 2) (z . 3)))))
J=> "{
       \"a\": {
         \"b\": [
           \"k\",
           4,
           2,
           \"x\",
           \"e\"
         ]
       },
       \"c\": {
         \"d\": {
           \"y\": 2,
           \"z\": 3
         }
       }
     }"

(h->json* (ht)) J=> "{}"
2D (tabular)

Functions in this category convert from other formats to hash table or
vice-versa. Key–value pairs are of dimension 2: tabular.

Lisp tables (lists of lists)
h<-lol (lol &optional size test)

Create a hash table with initial values according to list of lists LOL.

  • The first element of the LOL is a list of keys.
  • The other elements of the LOL are their respective values.
  • The values of the first column of the LOL are used as unique IDs:
    keys to the first hash table.
  • The value of each unique ID is a hash table composed of all
    key–value pairs associated with this unique ID.

As with alists, in case of repeated unique IDs the one uppermost in
the list has preference for returning values.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

(h<-lol '((:a  :b :c)
          (x   2  3)
          ("y" 5  6)
          (x   7  8)))
H=> (h* 'x  (h* :a 'x  :b 2  :c 3)
        "y" (h* :a "y" :b 5  :c 6))
;;
(h<-lol '((:a  :b :c)
          (x   2  3)
          ("y" 5  6)
          (x   7  8)))
H=> (ht ('x  (ht (:a 'x)  (:b 2) (:c 3)))
        ("y" (ht (:a "y") (:b 5) (:c 6))))
h->lol (table2d &optional reverse)

Do the opposite of h<-lol, which see. Return list of lists.

Input is TABLE2D.

If REVERSE is non-nil, reverse the display order of all rows after
header. Notice that the internal order of items depends on where
the data came from and whether it has been updated.

(h->lol (ht ('x  (ht (:a 'x)  (:b 2) (:c 3)))
            ("y" (ht (:a "y") (:b 5) (:c 6)))))
L=> '((:a  :b :c)
      (x   2  3)
      ("y" 5  6))

(h->lol (ht ('id01 (ht ("ID" 'id01) ("NAME" "Mary") ("AGE" 27)))
            ('id02 (ht ("ID" 'id02) ("NAME" "John") ("AGE" 20)))
            ('id03 (ht ("ID" 'id03) ("NAME" "Anne") ("AGE" 21)))))
L=> '(("ID" "NAME" "AGE")
      (id01 "Mary" 27)
      (id02 "John" 20)
      (id03 "Anne" 21))

;; ID should be detected when not the first in the hash table's value,
;; to be the first item of the lists:
(h->lol (h* "Alice" (h* "editor" "Emacs"
                        "age" 42
                        "name" "Alice")
            "Bob"   (h* "editor" "Vim"
                        "age" 30
                        "name" "Bob")
            "Emily" (h* "editor" "Nano"
                        "age" 21
                        "name" "Emily")))
L=> '(("name"  "editor" "age")
      ("Alice" "Emacs"  42)
      ("Bob"   "Vim"    30)
      ("Emily" "Nano"   21))

(h->lol (h* "Alice" (h* "editor" "Emacs"
                        "name" "Alice"
                        "age" 42)
            "Bob"   (h* "editor" "Vim"
                        "name" "Bob"
                        "age" 30)
            "Emily" (h* "editor" "Nano"
                        "name" "Emily"
                        "age" 21)))
L=> '(("name"  "editor" "age")
      ("Alice" "Emacs"  42)
      ("Bob"   "Vim"    30)
      ("Emily" "Nano"   21))


;;;;;; Repeated keys are dealt with
(h->lol (ht ('x (ht (:a 'x) (:b 2) (:c 3)))
            ('y (ht (:a 'y) (:b 8) (:c 9)))
            ('z (ht (:a 'z) (:b 0) (:c 1)))
            ('y (ht (:a 'y) (:b 5) (:c 6)))))
L=> '((:a :b :c) ;notice: 'y is updated ^here
      (x  2  3)
      (y  5  6)
      (z  0  1))

;;;;; Reversed
(h->lol (ht ('x  (ht (:a 'x)  (:b 2) (:c 3)))
            ("y" (ht (:a "y") (:b 5) (:c 6))))
        'reverse)
L=> '((:a  :b :c)
      ("y" 5  6)
      (x   2  3))

(h->lol (ht ('id01 (ht ("ID" 'id01) ("NAME" "Mary") ("AGE" 27)))
            ('id02 (ht ("ID" 'id02) ("NAME" "John") ("AGE" 20)))
            ('id03 (ht ("ID" 'id03) ("NAME" "Anne") ("AGE" 21))))
        'reverse)
L=> '(("ID" "NAME" "AGE")
      (id03 "Anne" 21)
      (id02 "John" 20)
      (id01 "Mary" 27))
Org tables
h<-orgtbl (orgtbl &optional size test)

Create a hash table with initial values according to org table ORGTBL.

Does to an org table what h<-lol does to Lisp tables (lists of lists).

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of xht--init-size, which see.

For the meaning of TEST, see h-new.

(--> "| id | name  | age | editor |
      |----+-------+-----+--------|
      | 01 | Alice |  42 | Emacs  |
      | 02 | Bob   |  30 | Vim    |
      | 03 | Emily |  21 | Nano   |"
     h<-orgtbl
     (-let [(&hash "01" (&hash "name" "editor")) it]
       (s-lex-format "${name} uses ${editor}!")))
=> "Alice uses Emacs!"

(h<-orgtbl "| ID   | NAME | AGE |
            |------+------+-----|
            | id01 | Mary |  27 |
            | id02 | John |  20 |
            | id03 | Anne |  21 |
            | id03 | Anne |  42 |
            | id02 | John |  31 |")
H=> (ht ("id01" (ht ("ID"   "id01")
                    ("NAME" "Mary")
                    ("AGE"  "27")))
        ("id02" (ht ("ID"   "id02")
                    ("NAME" "John")
                    ("AGE"  "20")))
        ("id03" (ht ("ID"   "id03")
                    ("NAME" "Anne")
                    ("AGE"  "21"))))
h->orgtbl (table2d &optional reverse)

Do the opposite of h<-orgtbl, which see. Return org table as string.

Input is TABLE2D.

If REVERSE is non-nil, reverse the display order of all rows after
header. Notice that the internal order of items depends on where
the data came from and whether it has been updated.

(h->orgtbl (ht ("id01" (ht ("ID"   "id01")
                           ("NAME" "Mary")
                           ("AGE"  "27")))
               ("id02" (ht ("ID"   "id02")
                           ("NAME" "John")
                           ("AGE"  "20")))
               ("id03" (ht ("ID"   "id03")
                           ("NAME" "Anne")
                           ("AGE"  "21")))))
=>  (replace-regexp-in-string
     "\n +" "\n"  "| ID   | NAME | AGE |
                   |------+------+-----|
                   | id01 | Mary |  27 |
                   | id02 | John |  20 |
                   | id03 | Anne |  21 |")


(h->orgtbl (ht ("id01" (ht ("ID"   "id01")
                           ("NAME" "Mary")
                           ("AGE"  "27")))
               ("id02" (ht ("ID"   "id02")
                           ("NAME" "John")
                           ("AGE"  "20")))
               ("id03" (ht ("ID"   "id03")
                           ("NAME" "Anne")
                           ("AGE"  "21")))))
O=>  "| ID   | NAME | AGE |
      |------+------+-----|
      | id01 | Mary |  27 |
      | id02 | John |  20 |
      | id03 | Anne |  21 |"

;; ID should be detected when not the first in the hash table's value,
;; to be the first column of the table:
(h->orgtbl (h* "Alice" (h* "editor" "Emacs"
                           "age" 42
                           "name" "Alice")
               "Bob"   (h* "editor" "Vim"
                           "age" 30
                           "name" "Bob")
               "Emily" (h* "editor" "Nano"
                           "age" 21
                           "name" "Emily")))
O=> "| name  | editor | age |
     |-------+--------+-----|
     | Alice | Emacs  |  42 |
     | Bob   | Vim    |  30 |
     | Emily | Nano   |  21 |"

(h->orgtbl (h* "Alice" (h* "editor" "Emacs"
                           "name" "Alice"
                           "age" 42)
               "Bob"   (h* "editor" "Vim"
                           "name" "Bob"
                           "age" 30)
               "Emily" (h* "editor" "Nano"
                           "name" "Emily"
                           "age" 21)))
O=> "| name  | editor | age |
     |-------+--------+-----|
     | Alice | Emacs  |  42 |
     | Bob   | Vim    |  30 |
     | Emily | Nano   |  21 |"

;;;;;; Repeated keys are dealt with
(h->orgtbl (ht ("id01" (ht ("ID"   "id01")
                           ("NAME" "Mary")
                           ("AGE"  "27")))
               ("id02" (ht ("ID"   "id02")
                           ("NAME" "John")
                           ("AGE"  "31")))
               ("id03" (ht ("ID"   "id03")
                           ("NAME" "Anne")
                           ("AGE"  "42")))
               ;; these two update the previous:
               ("id02" (ht ("ID"   "id02")
                           ("NAME" "John")
                           ("AGE"  "20")))
               ("id03" (ht ("ID"   "id03")
                           ("NAME" "Anne")
                           ("AGE"  "21")))))
O=> "| ID   | NAME | AGE |
     |------+------+-----|
     | id01 | Mary |  27 |
     | id02 | John |  20 |
     | id03 | Anne |  21 |"

(h->orgtbl (ht ('x (ht (:a 'x) (:b 2) (:c 3)))
               ('y (ht (:a 'y) (:b 8) (:c 9)))
               ('z (ht (:a 'z) (:b 0) (:c 1)))
               ;; notice: 'y is updated here:
               ('y (ht (:a 'y) (:b 5) (:c 6)))))
O=> "| a | b | c |
     |---+---+---|
     | x | 2 | 3 |
     | y | 5 | 6 |
     | z | 0 | 1 |"

;;;;; Reversed
(h->orgtbl (ht ("id01" (ht ("ID"   "id01")
                           ("NAME" "Mary")
                           ("AGE"  "27")))
               ("id02" (ht ("ID"   "id02")
                           ("NAME" "John")
                           ("AGE"  "20")))
               ("id03" (ht ("ID"   "id03")
                           ("NAME" "Anne")
                           ("AGE"  "21"))))
           'reverse)
=>  (replace-regexp-in-string
     "\n +" "\n"  "| ID   | NAME | AGE |
                   |------+------+-----|
                   | id03 | Anne |  21 |
                   | id02 | John |  20 |
                   | id01 | Mary |  27 |")

(h->orgtbl (ht ("id01" (ht ("ID"   "id01")
                           ("NAME" "Mary")
                           ("AGE"  "27")))
               ("id02" (ht ("ID"   "id02")
                           ("NAME" "John")
                           ("AGE"  "20")))
               ("id03" (ht ("ID"   "id03")
                           ("NAME" "Anne")
                           ("AGE"  "21"))))
           'reverse)
O=>  "| ID   | NAME | AGE |
      |------+------+-----|
      | id01 | Mary |  27 |
      | id02 | John |  20 |
      | id03 | Anne |  21 |"
TSV, CSV, SSV
Without using org (preferred)
  • h<-tsv (tsv &optional size test)

    Create a hash table with initial values according to TSV.

    SIZE is the nominal initial size of the table to be created.
    If nil, it defaults to the result of xht--init-size, which see.

    For the meaning of TEST, see h-new.

      ;;;; With TABs and newlines
      (h<-tsv "id\tname\tage\na1\tJane\t27\na2\tJohn\t25\n")
      H=> (h* "a1" (h* "id" "a1"
                       "name" "Jane"
                       "age" "27")
              "a2" (h* "id" "a2"
                       "name" "John"
                       "age" "25"))
    
      ;;;; With TABs and literal newlines
      (h<-tsv "\
    id\tname\tage
    a1\tJane\t27
    a2\tJohn\t25")
      H=> (h* "a1" (h* "id" "a1"
                       "name" "Jane"
                       "age" "27")
              "a2" (h* "id" "a2"
                       "name" "John"
                       "age" "25"))
    
      ;;;; With literal TABs and literal newlines
      (h<-tsv "\
    id  name    age
    a1  Jane    27
    a2  John    25")
      H=> (h* "a1" (h* "id" "a1"
                       "name" "Jane"
                       "age" "27")
              "a2" (h* "id" "a2"
                       "name" "John"
                       "age" "25"))
    
      ;;;; With empty lines
      (h<-tsv "\
    id  name    age
    
    a1  Jane    27
    
    a2  John    25
    ")
      H=>  (h* "a1" (h* "id" "a1"
                        "name" "Jane"
                        "age" "27")
               "a2" (h* "id" "a2"
                        "name" "John"
                        "age" "25"))
    
  • h->tsv (table2d &optional reverse)

    Do the opposite of h<-tsv, which see. Return tsv.

    Input is TABLE2D.

    If REVERSE is non-nil, reverse the display order of all rows after
    header. Notice that the internal order of items depends on where
    the data came from and whether it has been updated.

      (h->tsv (h* "a1" (h* "id" "a1" "name" "Jane" "age" "27")
                  "a2" (h* "id" "a2" "name" "John" "age" "25")))
      =>  "\
    id\tname\tage
    a1\tJane\t27
    a2\tJohn\t25
    "
    
      (h->tsv (h* "a1" (h* "id" "a1" "name" "Jane" "age" "27")
                  "a2" (h* "id" "a2" "name" "John" "age" "25")))
      T=>  "\
    id\tname\tage
    a1\tJane\t27
    a2\tJohn\t25"
    
      (h->tsv (h* "a1" (h* "id" "a1" "name" "Jane" "age" "42")
                  ;; key updated:
                  "a1" (h* "id" "a1" "name" "Jane" "age" "27")
                  "a2" (h* "id" "a2" "name" "John" "age" "25")))
      T=>  "\
    id\tname\tage
    a1\tJane\t27
    a2\tJohn\t25"
    
  • h<-ssv (ssv &optional size test)

    Create a hash table with initial values according to SSV.

    SIZE is the nominal initial size of the table to be created.
    If nil, it defaults to the result of xht--init-size, which see.

    For the meaning of TEST, see h-new.

      ;;;; With newlines
      (h<-ssv "id  name  age\na1  Jane  27\na2  John  25\n")
      H=> (h* "a1" (h* "id" "a1"
                       "name" "Jane"
                       "age" "27")
              "a2" (h* "id" "a2"
                       "name" "John"
                       "age" "25"))
    
      ;;;; With irregular spacing
      (h<-ssv "\
    id   name    age
    a1     Jane     27
    a2            John  25")
      H=> (h* "a1" (h* "id" "a1"
                       "name" "Jane"
                       "age" "27")
              "a2" (h* "id" "a2"
                       "name" "John"
                       "age" "25"))
    
      ;;;; With empty lines
      (h<-ssv "
    id  name  age
    
    a1  Jane  27
    
    a2  John  25
    ")
      H=>  (h* "a1" (h* "id" "a1"
                        "name" "Jane"
                        "age" "27")
               "a2" (h* "id" "a2"
                        "name" "John"
                        "age" "25"))
    
  • h->ssv (table2d &optional reverse)

    Do the opposite of h<-ssv, which see. Return ssv.

    Input is TABLE2D.

    If REVERSE is non-nil, reverse the display order of all rows after
    header. Notice that the internal order of items depends on where
    the data came from and whether it has been updated.

      (h->ssv (h* "a1" (h* "id" "a1" "name" "Janet" "age" "27")
                  "a2" (h* "id" "a2" "name" "Ann"   "age" "25")))
      =>  "\
    id  name   age
    a1  Janet  27
    a2  Ann    25
    "
    
      (h->ssv (h* "a1" (h* "id" "a1" "name" "Janet" "age" "27")
                  "a2" (h* "id" "a2" "name" "Ann"   "age" "25")))
      S=>  "\
    id  name   age
    a1  Janet  27
    a2  Ann    25"
    
      (h->ssv (h* "a1" (h* "id" "a1" "name" "Janet" "age" "42")
                  ;; key updated:
                  "a1" (h* "id" "a1" "name" "Janet" "age" "27")
                  "a2" (h* "id" "a2" "name" "Ann" "age" "25")))
      S=>  "\
    id  name   age
    a1  Janet  27
    a2  Ann    25"
    
  • h->csv ()

    Do the opposite of h<-csv, which see. Return csv.

    NOTE: This function has not yet been implemented.

Unknown thing

Functions that read an unknown object, try to guess what it is, then
convert it to a hash table.

h<-it (thing &rest rest)

Convert THING to hash table. Try to guess what it is.

When in doubt, it prefers higher dimension. So if type is alist,it
uses h<-alist*, not h<-alist. Likewise h<-plist* instead of
h-plist. If you prefer the latter, be explicit, passing the exact
function instead.

And since it uses h-type for type-detection, it tries to match:

  • lol, alist, and plist before list — so when you prefer plain
    lists, be explicit, passing h<-list instead.
  • *sv before lines — so when you prefer plain lines, be explicit,
    passing h<-lines instead.

REST are arguments that might be taken by the respective conversion
functions.

;;; 2D
;;;; list of lists
(h<-it '(("ID" "Name" "Age")
         ("A"  "Amy"  "20")
         ("B"  "Bob"  "30")))
H=> (h* "A" (h* "ID" "A" "Name" "Amy" "Age" "20")
        "B" (h* "ID" "B" "Name" "Bob" "Age" "30"))

;;;; orgtbl
(h<-it "| ID | Name | Age |
        |----+------+-----|
        | A  | Amy  |  20 |
        | B  | Bob  |  30 |")
H=> (h* "A" (h* "ID" "A" "Name" "Amy" "Age" "20")
        "B" (h* "ID" "B" "Name" "Bob" "Age" "30"))

;;;; tsv
(h<-it "ID\tName\tAge\nA\tAmy\t20\nB\tBob\t30")
H=> (h* "A" (h* "ID" "A" "Name" "Amy" "Age" "20")
        "B" (h* "ID" "B" "Name" "Bob" "Age" "30"))

;;;; csv
(h<-it "ID,Name,Age\nA,Amy,20\nB,Bob,30")
H=> (h* "A" (h* "ID" "A" "Name" "Amy" "Age" "20")
        "B" (h* "ID" "B" "Name" "Bob" "Age" "30"))

;;;; ssv
(h<-it "ID  Name  Age\nA  Amy  20\nB  Bob  30")
H=> (h* "A" (h* "ID" "A" "Name" "Amy" "Age" "20")
        "B" (h* "ID" "B" "Name" "Bob" "Age" "30"))

;;; Nested (unspecified dimension)
;;;; alist
(h<-it '(("Amy" . "dog")
         ("Bob" . (("cat"  . 2)
                   ("fish" . 11)))))
H=> (h* "Amy" "dog"
        "Bob" (h* "cat" 2 "fish" 11))

(h<-it '(("a" . "b") ("c" . "d")) 5)
Hp=> (h-st* 5 'equal
            "a" "b"
            "c" "d")

;;;; plist
(h<-it '("Amy" "dog"
         "Bob" ("cat" 2 "fish" 11)))
H=> (h* "Amy" "dog"
        "Bob" (h* "cat" 2 "fish" 11))

;;;; json
(h<-it "{
         \"Amy\": \"dog\",
         \"Bob\": {
            \"cat\": 2,
            \"fish\": 11
           }
        }")
H=> (h* "Amy" "dog"
        "Bob" (h* "cat" 2 "fish" 11))

;;; 1D explicit
;;;; kvl
(h<-it "Amy = 20\nBob = 30")
H=> (h* "Amy" "20" "Bob" "30")

(h<-it "a = b\nc = d" 5)
Hp=> (h-st* 5 'equal
            "a" "b"
            "c" "d")

(let ((h-kvl-sep-re ":"))
  (h<-it "Amy:20\nBob:30"))
H=> (h* "Amy" "20" "Bob" "30")

;;;; cons
(h<-it '("Amy" . 20))    H=> (h* "Amy" 20)

;;; 1D implicit
;;;; list
(h<-it '(a b c))         H=> (h* 0 'a 1 'b 2 'c)

;;;; vector
(h<-it [a b c])          H=> (h* 0 'a 1 'b 2 'c)

;;;; lines
(h<-it "a\nb\nc")        H=> (h* 0 "a" 1 "b" 2 "c")

;;; Self
;;;; ht
(h<-it (h* :a 1))        H=> (h* :a 1)
(h<-it (ht))             H=> (h*)

;;; Empty
;;;; list
(h<-it '())              H=> (h*)
(h<-it () 5 'eq)        Hp=> (h-st* 5 'eq)

;;;; vector
(h<-it [])               H=> (h*)
(h<-it [] 5 'eq)        Hp=> (h-st* 5 'eq)

;;;; string
(h<-it "")               H=> (h*)
(h<-it "" 5 'eq)        Hp=> (h-st* 5 'eq)

;;;; hash table
(h<-it (h*))             H=> (h*)
(h<-it (ht))             H=> (h*)
(h<-it (h-new))          H=> (h*)
(h<-it (ht-create))      H=> (h*)
(h<-it (h-st* 5 'eq))   Hp=> (h-st* 5 'eq)

;;; Error
;;;; Number
(h<-it 42)               !!> error

;;;; Symbol
(h<-it 'a)               !!> error
Predicates

Miscellaneous predicates, mostly about checking type for conversion
purposes.

Type

For hash tables, use h?, which is the same as ht?: an alias to Emacs'
internal hash-table-p.

h-kvl? (str &optional obsolete)

Could string STR be a KVL?

Only if, after stripped of any comments and section headers, and
trimmed of any leading and trailing whitespace, every line has
exactly one common separator. Comments and section headers are
matched with h-kvl-comment-re and h-kvl-section-re,
respectively, which see.

An OBSOLETE form is
(h-kvl? str &optional sep)
where the optional arg SEP specified the field delimiter. This use
is deprecated. The delimiter is now passed by the variable
h-kvl-sep-re (which see), which should be let-bound when the
default value is not expected.

  ;;;; When expected separator is the default: " *= *"
  (h-kvl? "a=b\nc=d")         => t
  (h-kvl? "a = b\nc = d")     => t
  (h-kvl? "aa = bb\nc  = d")  => t

  ;;;; More than one separator? Then it's not KVL.
  (h-kvl? "a=b=c\nd=e=f")     => nil
  (h-kvl? "a=b=c")            => nil

  ;;;; No separator? Also not KVL.
  (h-kvl? "abc")              => nil

  ;;;; Leading and trailing whitespace is ignored
  (h-kvl? "  Alice = 42
             Bob   = 30  ")
  => t

  ;;;; Empty lines are ignored
  (h-kvl? "
       Alice  =  42

         Bob  =  30
")
  => t

  ;;;; Comments are ignored
  (h-kvl? "\
# Some people here
Alice = 42  # Alice's real age!
Bob   = 30")
  => t

  ;;;; [Sections] are ignored: to deal with simple (flat) .conf files
  (h-kvl? "[People]
           Alice = 42
           Bob   = 30")
  => t

  ;;;; When expected separator is different, let-bind this variable
  (let ((h-kvl-sep-re ":"))
    (h-kvl? "Alice:42\nBob:30"))
  => t

  (let ((h-kvl-sep-re " *: *"))
    (h-kvl? "\
Alice : 42
Bob   : 30"))
  => t

  (let ((h-kvl-sep-re ":"))
    (h-kvl? "Alice:42:Emacs\nBob:30:Vim"))
  ;; this one ^ looks like a headerless DSV instead.
  => nil

  ;;;; When expected comment regex is different, let-bind this variable
  (let ((h-kvl-comment-re " *;.*"))
    (h-kvl? "\
;; Some people
Alice = 42  ; Alice's real age!
Bob   = 30"))
  => t

  ;;;; When expected section regex is different, let-bind this variable
  (let ((h-kvl-section-re "^[ \t]*{.*}[ \t]*$"))
    (h-kvl? "\
{People}
Alice = 42
Bob   = 30
Emily = 21"))
  => t

  ;;;; Mixed bag for handling a rather unusual KVL
  (let ((h-kvl-sep-re     " *~ *")
        (h-kvl-comment-re " *;.*")
        (h-kvl-section-re "^ *{.*} *$"))
    (h-kvl? "
              {Some people}

              ;; We start with Alice:
              Alice ~ 42  ; Alice's real age!

              ;; And then Bob:
                Bob ~ 30  ; (I think)
"))
  => t

  ;;;; Although the empty string can be thought of a KVL with no keys...
  (h-kvl?  "") => t
  ;;;; ...and likewise other xht types...
  (h-tsv?  "") => t
  (h-csv?  "") => t
  (h-ssv?  "") => t

  ;;;; ...note that [[#h-type][h-type]] and =type-of= will return something else:
  (h-type  "") => :empty
  (type-of "") => 'string

  ;;;; Obsolete calling convention — DEPRECATED
  (with-no-warnings
    (h-kvl? "Alice:42\nBob:30" ":"))
  => t
h-alist? (list)

Non-nil if and only if LIST is a non-nil alist with simple keys.

(h-alist? '((1 . 2) (3 (4 . 5) (6 . 7)))) => t   ; nested
(h-alist? '((1 . 2) (3 . 4)))             => t   ; simple
(h-alist? '((1) (2 . 3)))                 => t   ; nil element
(h-alist? '(1 (2 .3)))                    => nil ; all elems must be lists
(h-alist? '(1 2 3 4 5))                   => nil ; just a list
(h-alist? '(a 1 b 2))                     => nil ; just a list, or a plist
(h-alist? "Me, I'm an alist!")            => nil ; nope, you're a string
(h-alist? '())                            => nil
h-plist? (list)

Non-nil if and only if LIST is a non-nil plist with non-nil atom keys.

(h-plist? '(1 2 3 (4 5 6 7)))   => t   ; nested
(h-plist? '(1 2 3 4))           => t   ; simple
(h-plist? '(a 1 b 2))           => t   ;
(h-plist? '(1 2 3 4 5))         => nil ; odd number
(h-plist? "Me, I'm a plist!")   => nil ; nope, you're a string
(h-plist? '())                  => nil
h-lol? (obj)

Return t if OBJ is a list of lists.

(h-lol? '(("id" "name"  "age")
          ("b"  "bob"   "30")
          ("e"  "emily" "21")))
=> t
(h-lol? '((a b) (c d)))         => t
(h-lol? '((a . b) (c . d)))     => nil ; alist
(h-lol? '(a 1 b 2))             => nil ; just a list, or a plist
(h-lol? "Me, I'm a lol!")       => nil ; nope, you're a string
(h-lol? '())                    => nil
h-orgtbl? (str)

Could string STR be an Org Table?

(h-orgtbl? "| id | name  | age |
            |----+-------+-----|
            | b  | bob   |  30 |
            | e  | emily |  21 |")
=> t

;; formatting not needed
(h-orgtbl? "| id | name | age |
            |----------|
            | b | bob | 30 |
            | e | emily | 21 |")
=> t

;; formatting not needed
(h-orgtbl? "| id | name | age |\n|----------|\n| b | bob | 30 |
           | e | emily | 21 |")
=> t

(h-orgtbl? '((a b) (c d)))         !!> error
(h-orgtbl? "Me, I'm an org table!") => nil ; nope, you're a string
(h-orgtbl? "")                      => nil
h-json? (str)

Could string STR be a JSON object?

Return nil if, after trimming, it isn't enclosed in {} or [].
Return t if inside is either empty or returns non-nil when
converted with json-read-from-string ignoring errors.

(h-json? "{\n  \"c\": 30,\n  \"e\": 5\n}") => t
(h-json? "[\"k\",4,2,\"x\",\"e\"]") => t
(h-json? "{
            \"a\": 1,
            \"b\": {
              \"c\": 3
            }
          }")
=> t
(h-json? "{   \n }") => t
(h-json? "[   \n ]") => t
(h-json? "(   \n )") => nil
(h-json? "{ }")      => t
(h-json? "{}")       => t
(h-json? "  ")       => nil
(h-json? "")         => nil

;;;; example from p-baleine's jq.el (github)
(h-json? "
  [
    {
      \"name\": \"Ness\",
      \"age\": 12,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Onett\" }
    },
    {
      \"name\": \"Paula\",
      \"age\": 11,
      \"origin\": { \"country\": \"Eagleland\", \"town\": \"Twoson\" }
    }
  ]
  ")
=> t
h-tsv? (str)

Could string STR be a TSV?

Only if all non-commented, non-blank lines have at least one tab
not in the beginning of the line. Commented means `^ *#` in the
line.

Note that if the TSV has two fields, and therefore exactly one tab
per line, it can't be distinguished from a KVL-with-tab using tab
as separator, since it can't be detected whether the first line is
a TSV header or a KVL entry. So functions such as h<-it, h-it=,
h-type, and others may guess wrongly.

;;;; Notice that the first two also return t for KVL
;;;; (can't be determined without context)
(h-tsv? "a\tb")             => t
(h-tsv? "a\tb\nc\td")       => t
(h-tsv? "a\tb\tc\nd\te\tf") => t
(h-tsv? "a = b\nc = d")     => nil
(h-tsv? "a\tb\tc")          => t
(h-tsv? "abc")              => nil
h-csv? (str)

Could string STR be a CSV?

Only if all non-commented, non-blank lines have at least one comma
not in the beginning of the line. Commented means `^ *#` in the
line.

  (h-csv? "one,two,three\na,b,e\nf,g,h") => t
  (h-csv? "one,two,three\na,\"b, c, and d\",e\nf,g,h") => t
  (h-csv? "name,hello,bye
Alice,\"Hello, there!\",Bye!
Bob,\"Hi.\",\"Goodbye, again\"")
  => t

  ;;;; The next two are actually TSVs whose entries have commas:
  (h-csv? "one\ttwo\tthree\na\t\"b, c, and d\"\te\nf\tg\th") => nil
  (h-csv? "one\ttwo\tthree\na\tb, c, and d\te\nf\tg\th")     => nil

  (h-csv? "a,b")              => t
  (h-csv? "a,b\nc,d")         => t
  (h-csv? "a,b,c\nd,e,f")     => t
  (h-csv? "a  b  c\nd  e  f") => nil
  (h-csv? "a\tb\nc\td")       => nil
  (h-csv? "a,b,c")            => t
  (h-csv? "abc")              => nil
h-ssv? (str)

Could string STR be an SSV?

Only if all non-commented, non-blank lines have at least TWO
whitespaces or a tab not in the beginning of the line. Commented
means `^ *#` in the line.

(h-ssv? "a\tb")             => t
(h-ssv? "a\tb\nc\td")       => t
(h-ssv? "a\tb\tc\nd\te\tf") => t
(h-ssv? "a  b  c\nd  e  f") => t
(h-ssv? "a  b\nc  d")       => t
(h-ssv? "a\tb\tc")          => t
(h-ssv? "abc")              => nil
Dimension
Properties and dimension

Functions to look up properties and dimension of hash tables (and of other
objects convertible to hash tables).

See also: xht--props=, xht--props-and-data=, h-props=

Hash tables
h-prop (table prop)

Return the value of property PROP of hash table TABLE.

Valid values for PROP are:

  • size
  • test
  • weakness
  • rehash-size
  • rehash-threshold

PROP may be given as a symbol, string or keyword.
So any of these work: 'test "test" :test

Note that 'size' is the nominal size, which defaults to 65
when created. For the actual size, use ht-size.

(h-prop (h* :a 1)          'size) => 1
(h-prop (ht (:a 1))        'size) => 65
(h-prop (ht (:a 1))        'test) => 'equal
(h-prop (ht-create 'eq)    'test) => 'eq
(h-prop (ht-create 'eq)    'size) => 65
(h-prop (h-new 10)         'size) => 10
(h-prop (h-new 12 'eql)    'test) => 'eql
(h-prop (ht)               'size) => 65
(h-prop (ht)           'weakness) => nil
(h-prop (ht)        'rehash-size) => 1.5
(h-prop (ht)   'rehash-threshold) => 0.8
h-props (table)

Return a hash table containing properties of hash table TABLE.

The property 'data isn't included. The reason is that while all
other properties can be compared directly, the value of 'data is a
plist, and plists with different ordering can be equivalent. This
would create an exception for functions using this function. For
comparing just the data from two hash tables, use h=.

(h-props (h* :a 1))
H=> (h* 'size             1
        'test             'equal
        'weakness         nil
        'rehash-size      1.5
        'rehash-threshold 0.8)

(h-props (h-st* 20 'eq :a 1))
H=> (h* 'size             20
        'test             'eq
        'weakness         nil
        'rehash-size      1.5
        'rehash-threshold 0.8)

(h-props (ht (:a 1)))
H=> (h* 'size             65
        'test             'equal
        'weakness         nil
        'rehash-size      1.5
        'rehash-threshold 0.8)

(h-props (make-hash-table))
H=> (h* 'size             65
        'test             'eql
        'weakness         nil
        'rehash-size      1.5
        'rehash-threshold 0.8)

(h-props (h-new 10 'equal t 1.4))
H=> (h* 'size             10
        'test             'equal
        'weakness         'key-and-value
        'rehash-size      1.4
        'rehash-threshold 0.8)
h-dim (table)

Determine dimension of hash table TABLE.

Return:

  • error if not a hash table.
  • 0 if empty hash table.
  • 1 if simple key–value: no value is itself a hash table.
  • 1.5 if some values are hash tables of dimension 1, and some
    values aren't hash tables.
  • 2 if h-2d? returns t
  • 2.5 if every value is a hash table of dimension at least 2,
    but some are higher.
  • 3 if every value is a hash table of dimension 2.

... etc.

Generally: 1+ (minimum common dimension of hash table values) +
(if any ht has dim > mcd(dims), then 0.5, else 0)

Regarding conversions to hash table, when:

  • from KVL, vector, list: should be of dim = 1.
  • from lol, orgtbl, *sv: should be of dim = 2.
  • from alist, plist: 1 when simple, and usually 1.5 when nested.
    (but could be 2+ with the same logic)
(h-dim 42)                        !!> error
(h-dim (ht))                       => 0
(h-dim (ht (0 "alice")))           => 1
(h-dim (ht (0 "alice") (1 "bob"))) => 1
(h-dim (ht (0 "alice")
           (1 (ht (:a "A") (:b "B")))))
=> 1.5
(h-dim (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :id "b" :cap "B"))))
=> 2
(h-dim (h* "a" (h* :id "a" :cap "A")
           "b" (h* :id "b" :cap (h* "c" "C"
                                    "d" "D"))))
=> 2.5
h-1d? (obj)

Return t if OBJ is a 1D hash table, nil otherwise.

It's 1D if, and only if:

  1. it's a hash table AND
  2. none of the values are hash tables.
(h-1d? (ht))                                 => nil
(h-1d? (ht ("a" nil)))                       => t
(h-1d? (ht ("a" 1) ("b" 2)))                 => t
(h-1d? (ht ("a" (h* :id "a" :cap "A"))))     => nil
(h-1d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :id "b" :cap "B"))))
=> nil
(h-1d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :id 'b  :cap "B"))))
=> nil
(h-1d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :cap "B" :id "b"))
           ("c" (h* :id "c"))))
=> nil
(h-1d? (ht ("a" (h* :id "a" :same "a"))
           ("b" (h* :id "b" :same "b"))))
=> nil
h-2d? (obj)

Return t if OBJ is a 2D hash table, nil otherwise.

Return t if, and only if:

  1. it's a hash table AND
  2. all values are regular hash tables AND
  3. none of the keys of these values (the "header labels") are
    themselves hash tables AND
  4. at least one of these keys has as property: its value is equal
    to OBJ's key for all of OBJ's keys. In other words, an id ID
    exists if, and only if, (h-get* key id) returns key for all
    keys.
(h-2d? (ht))                                 => nil
(h-2d? (ht ("a" nil)))                       => nil
(h-2d? (ht ("a" 1) ("b" 2)))                 => nil
(h-2d? (ht ("a" (h* :id "a" :cap "A"))))     => t
(h-2d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :id "b" :cap "B"))))
=> t
(h-2d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :id 'b  :cap "B"))))
=> nil
(h-2d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :cap "B" :id "b"))
           ("c" (h* :id "c"))))
=> t
(h-2d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :cap "B" :id "b"))
           ("c" (h* :id "C"))))
=> nil
(h-2d? (ht ("a" (h* :id "a" :cap "A"))
           ("b" (h* :cap "B" :id "b"))
           ("c" (h* :low "c"))))
=> nil
(h-2d? (ht ("a" (h* :cap    "A"  :id "a" ))
           ("b" (h* :cap    "B"  :id "b"))
           ("c" (h* :double "cc" :id "c"))))
=> t
(h-2d? (ht ("a" (h* :id "a" :same "a"))
           ("b" (h* :id "b" :same "b"))))
=> t
h-nested? (obj)

Return t if OBJ is a nested hash table, nil otherwise.

It's nested if, and only if:

  1. it's a hash table AND
  2. at least one value is a hash table.
(h-nested? (ht))                                 => nil
(h-nested? (ht ("a" nil)))                       => nil
(h-nested? (ht ("a" 1) ("b" 2)))                 => nil
(h-nested? (ht ("a" (h* :id "a" :cap "A"))))     => t
(h-nested? (ht ("a" (h* :id "a" :cap "A"))
               ("b" (h* :id "b" :cap "B"))))
=> t
(h-nested? (ht ("a" (h* :id "a" :cap "A"))
               ("b" (h* :id 'b  :cap "B"))))
=> t
(h-nested? (ht ("a" (h* :id "a" :cap "A"))
               ("b" (h* :cap "B" :id "b"))
               ("c" (h* :id "c"))))
=> t
(h-nested? (ht ("a" (h* :id "a" :cap "A"))
               ("b" (h* :cap "B" :id "b"))
               ("c" (h* :id "C"))))
=> t
(h-nested? (ht ("a" (h* :id "a" :cap "A"))
               ("b" (h* :cap "B" :id "b"))
               ("c" (h* :low "c"))))
=> t
(h-nested? (ht ("a" (h* :cap    "A"  :id "a" ))
               ("b" (h* :cap    "B"  :id "b"))
               ("c" (h* :double "cc" :id "c"))))
=> t
(h-nested? (ht ("a" (h* :id "a" :same "a"))
               ("b" (h* :id "b" :same "b"))))
=> t
Generalization to other types
h-it-dim (obj)

Determine dimension of object OBJ.

This is a generalization of h-dim (which see) to other objects.

;;;; non-hash tables
 (h-it-dim '((0 . "alice") (1 . "bob")))             => 1   ;alist
 (h-it-dim '(0 "alice" 1 "bob"))                     => 1   ;plist
 (h-it-dim "{\n  \"a\": 1,\n  \"b\": 2\n}")          => 1   ;json
 (h-it-dim "alice = emacs\nbob = vim\n")             => 1   ;kvl
 (h-it-dim '("alice" "bob"))                         => 1   ;list
 (h-it-dim ["alice" "bob"])                          => 1   ;vector

 (h-it-dim '((0 . "alice") (1 (2 . "bob"))))         => 1.5 ;alist
 (h-it-dim '(0 "alice" 1 (2 "bob")))                 => 1.5 ;plist
 (h-it-dim "{
                \"0\": \"alice\",
                \"1\": {
                  \"a\": \"A\",
                \"b\": \"B\"
                }
              }")
 => 1.5   ;json

 (h-it-dim "name\teditor\nalice\temacs\nbob\tvim\n") => 2 ;tsv
 (h-it-dim "name  editor\nalice  emacs\nbob  vim\n") => 2 ;ssv
 (h-it-dim "name,editor\nalice,emacs\nbob,vim\n")    => 2 ;csv
 (h-it-dim "| name  | editor |
            |-------+--------|
            | alice | emacs  |
            | bob   | vim    |")
 => 2 ;orgtbl

 (h-it-dim '(("name"  "editor")
             ("alice" "emacs")
             ("bob"   "vim")))
 => 2 ;lol

 (h-it-dim '(("bob"
              ("editor" . "vim")
              ("name"   . "bob"))
             ("alice"
              ("editor" . "emacs")
              ("name"   . "alice"))))
 => 2 ;alist

 (h-it-dim '("bob"   ("editor" "vim"   "name" "bob")
             "alice" ("editor" "emacs" "name" "alice")))
 => 2 ;plist

 (h-it-dim "{
              \"alice\": {
                \"editor\": \"emacs\",
                \"name\": \"alice\"
              },
              \"bob\": {
                \"editor\": \"vim\",
                \"name\": \"bob\"
              }
            }")
 => 2 ;json

 ;;;; hash tables (same as h-dim)
 (h-it-dim (ht (0 "alice") (1 "bob")))         => 1 ;hash-table
 (h-it-dim (ht (0 "alice")
               (1 (ht (:a "A") (:b "B")))))
 => 1.5
 (h-it-dim (ht ("a" (h* :id "a" :cap "A"))
               ("b" (h* :id "b" :cap "B"))))
 => 2
 (h-it-dim (h* "a" (h* :id "a" :cap "A")
               "b" (h* :id "b" :cap (h* "c" "C"
                                        "d" "D"))))
 => 2.5

 ;;;; error
 (h-it-dim 42)                        !!> error
h-it-empty? (obj)

Return t if OBJ is empty, nil otherwise.

This is a generalization of h-empty? (which see) to other objects.

 ;;;; non-hash tables

(h-it-empty? '())     => t   ;list, alist, plist, lol
(h-it-empty? [])      => t   ;vector
(h-it-empty? "")      => t   ;tsv, csv, ssv, kvl, lines
(h-it-empty? "| |")   => t   ;orgtbl
(h-it-empty? "{ }")   => t   ;json

(h-it-empty? '((0 . "alice") (1 . "bob")))             => nil ;alist
(h-it-empty? '(0 "alice" 1 "bob"))                     => nil ;plist
(h-it-empty? "{\n  \"a\": 1,\n  \"b\": 2\n}")          => nil ;json
(h-it-empty? "alice = emacs\nbob = vim\n")             => nil ;kvl
(h-it-empty? '("alice" "bob"))                         => nil ;list
(h-it-empty? ["alice" "bob"])                          => nil ;vector

(h-it-empty? '((0 . "alice") (1 (2 . "bob"))))         => nil ;alist
(h-it-empty? '(0 "alice" 1 (2 "bob")))                 => nil ;plist
(h-it-empty? "{
             \"0\": \"alice\",
             \"1\": {
               \"a\": \"A\",
             \"b\": \"B\"
             }
           }")
=> nil   ;json

(h-it-empty? "name\teditor\nalice\temacs\nbob\tvim\n") => nil ;tsv
(h-it-empty? "name  editor\nalice  emacs\nbob  vim\n") => nil ;ssv
(h-it-empty? "name,editor\nalice,emacs\nbob,vim\n")    => nil ;csv
(h-it-empty? "| name  | editor |
              |-------+--------|
              | alice | emacs  |
              | bob   | vim    |")
=> nil ;orgtbl

(h-it-empty? '(("name"  "editor")
               ("alice" "emacs")
               ("bob"   "vim")))
=> nil ;lol

(h-it-empty? '(("bob"
                ("editor" . "vim")
                ("name"   . "bob"))
               ("alice"
                ("editor" . "emacs")
                ("name"   . "alice"))))
=> nil ;alist

(h-it-empty? '("bob"   ("editor" "vim"   "name" "bob")
               "alice" ("editor" "emacs" "name" "alice")))
=> nil ;plist

(h-it-empty? "{
             \"alice\": {
               \"editor\": \"emacs\",
               \"name\": \"alice\"
             },
             \"bob\": {
               \"editor\": \"vim\",
               \"name\": \"bob\"
             }
           }")
=> nil ;json

;;;; hash tables (same as h-empty?)
(h-it-empty? (h*))      => t   ;hash table
(h-it-empty? (ht))      => t   ;hash table
(h-it-empty? (h-new))   => t   ;hash table

(h-it-empty? (ht ("a" nil)))                       => nil
(h-it-empty? (ht ("a" 1) ("b" 2)))                 => nil
(h-it-empty? (ht ("a" (h* :id "a" :cap "A"))))     => nil
(h-it-empty? (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :id "b" :cap "B"))))
=> nil
(h-it-empty? (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :id 'b  :cap "B"))))
=> nil
(h-it-empty? (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :cap "B" :id "b"))
                 ("c" (h* :id "c"))))
=> nil
(h-it-empty? (ht ("a" (h* :id "a" :same "a"))
                 ("b" (h* :id "b" :same "b"))))
=> nil

;;;; error
(h-it-empty? 42)  !!> error
h-it-1d? (obj)

Return t if OBJ is of dimension 1, nil otherwise.

This is a generalization of h-1d? (which see) to other objects.

 ;;;; non-hash tables
(h-it-1d? '((0 . "alice") (1 . "bob")))             => t   ;alist
(h-it-1d? '(0 "alice" 1 "bob"))                     => t   ;plist
(h-it-1d? "{\n  \"a\": 1,\n  \"b\": 2\n}")          => t   ;json
(h-it-1d? "alice = emacs\nbob = vim\n")             => t   ;kvl
(h-it-1d? '("alice" "bob"))                         => t   ;list
(h-it-1d? ["alice" "bob"])                          => t   ;vector

(h-it-1d? '((0 . "alice") (1 (2 . "bob"))))         => nil ;alist
(h-it-1d? '(0 "alice" 1 (2 "bob")))                 => nil ;plist
(h-it-1d? "{
             \"0\": \"alice\",
             \"1\": {
               \"a\": \"A\",
             \"b\": \"B\"
             }
           }")
=> nil   ;json

(h-it-1d? "name\teditor\nalice\temacs\nbob\tvim\n") => nil ;tsv
(h-it-1d? "name  editor\nalice  emacs\nbob  vim\n") => nil ;ssv
(h-it-1d? "name,editor\nalice,emacs\nbob,vim\n")    => nil ;csv
(h-it-1d? "| name  | editor |
           |-------+--------|
           | alice | emacs  |
           | bob   | vim    |")
=> nil ;orgtbl

(h-it-1d? '(("name"  "editor")
            ("alice" "emacs")
            ("bob"   "vim")))
=> nil ;lol

(h-it-1d? '(("bob"
             ("editor" . "vim")
             ("name"   . "bob"))
            ("alice"
             ("editor" . "emacs")
             ("name"   . "alice"))))
=> nil ;alist

(h-it-1d? '("bob"   ("editor" "vim"   "name" "bob")
            "alice" ("editor" "emacs" "name" "alice")))
=> nil ;plist

(h-it-1d? "{
             \"alice\": {
               \"editor\": \"emacs\",
               \"name\": \"alice\"
             },
             \"bob\": {
               \"editor\": \"vim\",
               \"name\": \"bob\"
             }
           }")
=> nil ;json

;;;; hash tables (same as h-1d?)
(h-it-1d? (ht))                                 => nil
(h-it-1d? (ht ("a" nil)))                       => t
(h-it-1d? (ht ("a" 1) ("b" 2)))                 => t
(h-it-1d? (ht ("a" (h* :id "a" :cap "A"))))     => nil
(h-it-1d? (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id "b" :cap "B"))))
=> nil
(h-it-1d? (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id 'b  :cap "B"))))
=> nil
(h-it-1d? (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "c"))))
=> nil
(h-it-1d? (ht ("a" (h* :id "a" :same "a"))
              ("b" (h* :id "b" :same "b"))))
=> nil

;;;; error
(h-it-1d? 42)  !!> error
h-it-2d? (obj)

Return t if OBJ is of dimension 2, nil otherwise.

This is a generalization of h-2d? (which see) to other objects.

 ;;;; non-hash tables
(h-it-2d? '((0 . "alice") (1 . "bob")))             => nil ;alist
(h-it-2d? '(0 "alice" 1 "bob"))                     => nil ;plist
(h-it-2d? "{\n  \"a\": 1,\n  \"b\": 2\n}")          => nil ;json
(h-it-2d? "alice = emacs\nbob = vim\n")             => nil ;kvl
(h-it-2d? '("alice" "bob"))                         => nil ;list
(h-it-2d? ["alice" "bob"])                          => nil ;vector

(h-it-2d? '((0 . "alice") (1 (2 . "bob"))))         => nil ;alist
(h-it-2d? '(0 "alice" 1 (2 "bob")))                 => nil ;plist
(h-it-2d? "{
             \"0\": \"alice\",
             \"1\": {
               \"a\": \"A\",
             \"b\": \"B\"
             }
           }")
=> nil   ;json

(h-it-2d? "name\teditor\nalice\temacs\nbob\tvim\n") => t   ;tsv
(h-it-2d? "name  editor\nalice  emacs\nbob  vim\n") => t   ;ssv
(h-it-2d? "name,editor\nalice,emacs\nbob,vim\n")    => t   ;csv
(h-it-2d? "| name  | editor |
           |-------+--------|
           | alice | emacs  |
           | bob   | vim    |")
=> t ;orgtbl

(h-it-2d? '(("name"  "editor")
            ("alice" "emacs")
            ("bob"   "vim")))
=> t ;lol

(h-it-2d? '(("bob"
             ("editor" . "vim")
             ("name"   . "bob"))
            ("alice"
             ("editor" . "emacs")
             ("name"   . "alice"))))
=> t ;alist

(h-it-2d? '("bob"   ("editor" "vim"   "name" "bob")
            "alice" ("editor" "emacs" "name" "alice")))
=> t ;plist

(h-it-2d? "{
             \"alice\": {
               \"editor\": \"emacs\",
               \"name\": \"alice\"
             },
             \"bob\": {
               \"editor\": \"vim\",
               \"name\": \"bob\"
             }
           }")
=> t ;json

;;;; hash tables (same as h-2d?)
(h-it-2d? (ht))                                 => nil
(h-it-2d? (ht ("a" nil)))                       => nil
(h-it-2d? (ht ("a" 1) ("b" 2)))                 => nil
(h-it-2d? (ht ("a" (h* :id "a" :cap "A"))))     => t
(h-it-2d? (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id "b" :cap "B"))))
=> t
(h-it-2d? (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id 'b  :cap "B"))))
=> nil
(h-it-2d? (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "c"))))
=> t
(h-it-2d? (ht ("a" (h* :id "a" :same "a"))
              ("b" (h* :id "b" :same "b"))))
=> t

;;;; error
(h-it-2d? 42)  !!> error
h-it-nested? (obj)

Return t if OBJ is nested, nil otherwise.

This is a generalization of h-nested? (which see) to other
objects.

 ;;;; non-hash tables
(h-it-nested? '((0 . "alice") (1 . "bob")))             => nil ;alist
(h-it-nested? '(0 "alice" 1 "bob"))                     => nil ;plist
(h-it-nested? "{\n  \"a\": 1,\n  \"b\": 2\n}")          => nil ;json
(h-it-nested? "alice = emacs\nbob = vim\n")             => nil ;kvl
(h-it-nested? '("alice" "bob"))                         => nil ;list
(h-it-nested? ["alice" "bob"])                          => nil ;vector

(h-it-nested? '((0 . "alice") (1 (2 . "bob"))))         => t   ;alist
(h-it-nested? '(0 "alice" 1 (2 "bob")))                 => t   ;plist
(h-it-nested? "{
                 \"0\": \"alice\",
                 \"1\": {
                   \"a\": \"A\",
                 \"b\": \"B\"
                 }
               }")
=> t     ;json

(h-it-nested? "name\teditor\nalice\temacs\nbob\tvim\n") => t   ;tsv
(h-it-nested? "name  editor\nalice  emacs\nbob  vim\n") => t   ;ssv
(h-it-nested? "name,editor\nalice,emacs\nbob,vim\n")    => t   ;csv
(h-it-nested? "| name  | editor |
               |-------+--------|
               | alice | emacs  |
               | bob   | vim    |")
=> t ;orgtbl

(h-it-nested? '(("name"  "editor")
                ("alice" "emacs")
                ("bob"   "vim")))
=> t ;lol

(h-it-nested? '(("bob"
                 ("editor" . "vim")
                 ("name"   . "bob"))
                ("alice"
                 ("editor" . "emacs")
                 ("name"   . "alice"))))
=> t ;alist

(h-it-nested? '("bob"   ("editor" "vim"   "name" "bob")
                "alice" ("editor" "emacs" "name" "alice")))
=> t ;plist

(h-it-nested? "{
                 \"alice\": {
                   \"editor\": \"emacs\",
                   \"name\": \"alice\"
                 },
                 \"bob\": {
                   \"editor\": \"vim\",
                   \"name\": \"bob\"
                 }
               }")
=> t ;json

;;;; hash tables (same as h-nested?)
(h-it-nested? (ht))                                 => nil
(h-it-nested? (ht ("a" nil)))                       => nil
(h-it-nested? (ht ("a" 1) ("b" 2)))                 => nil
(h-it-nested? (ht ("a" (h* :id "a" :cap "A"))))     => t
(h-it-nested? (ht ("a" (h* :id "a" :cap "A"))
                  ("b" (h* :id "b" :cap "B"))))
=> t
(h-it-nested? (ht ("a" (h* :id "a" :cap "A"))
                  ("b" (h* :id 'b  :cap "B"))))
=> t
(h-it-nested? (ht ("a" (h* :id "a" :cap "A"))
                  ("b" (h* :cap "B" :id "b"))
                  ("c" (h* :id "c"))))
=> t
(h-it-nested? (ht ("a" (h* :id "a" :cap "A"))
                  ("b" (h* :cap "B" :id "b"))
                  ("c" (h* :id "C"))))
=> t
(h-it-nested? (ht ("a" (h* :id "a" :cap "A"))
                  ("b" (h* :cap "B" :id "b"))
                  ("c" (h* :low "c"))))
=> t
(h-it-nested? (ht ("a" (h* :cap    "A"  :id "a" ))
                  ("b" (h* :cap    "B"  :id "b"))
                  ("c" (h* :double "cc" :id "c"))))
=> t
(h-it-nested? (ht ("a" (h* :id "a" :same "a"))
                  ("b" (h* :id "b" :same "b"))))
=> t
;;;; error
(h-it-nested? 42)  !!> error
Writing and reading
Write to file

Functions that write to file.

h-write! (filename obj &optional mustbenew)

Write OBJ to file FILENAME.

OBJ may represent any of the input and output data structures of
the conversion functions.

  • If they are of string type (as kvl, *sv, org tables), they are
    written as they are — strings.
  • Anything else (numbers, lists, vectors, hash tables) is written
    as it looks like — as an s-expression.

Filename is created if nonexistent, and overwritten if existent —
but the parent directory, if it doesn't exist, is only created if
the 'f library is installed and loaded.

The optional arg MUSTBENEW, if non-nil, insists on a check for an
existing file with the same name. If MUSTBENEW is excl, that
means you get an error if the file already exists; never overwrite.
If MUSTBENEW is neither nil nor excl, that means ask for
confirmation before overwriting, but do go ahead and overwrite the
file if you confirm.

Note that hash table objects are written using their
#s(hash-table…) raw representation. For finer control of how hash
tables are written, including the option of using (h*…) or (ht…)
representations, see the specialized h-write-ht-like!.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp (h* "a" 1 "b" 2))
      (with-temp-buffer (insert-file-contents temp)
                        (read (buffer-string)))
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '("a" 1 "b" 2))
      (with-temp-buffer (insert-file-contents temp)
                        (read (buffer-string)))
    (delete-file temp)))
=> '("a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '(("a" . 1) ("b" . 2)))
      (with-temp-buffer (insert-file-contents temp)
                        (read (buffer-string)))
    (delete-file temp)))
=> '(("a" . 1) ("b" . 2))

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "| Name  | Other |\n|-------+-------|\n| Alice | Emacs |")
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "| Name  | Other |\n|-------+-------|\n| Alice | Emacs |"

;; Writing returns nil
(let ((temp (make-temp-file "h-ert-test--")))
  (prog1
      (h-write! temp '(("a" . 1) ("b" . 2)))
    (delete-file temp)))
=> nil
h-write-ht-like! (filename format ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as FORMAT.

For more about FILENAME and MUSTBENEW, see h-write!.

HT-LIKE may come in any of the following autodetected formats:

As Lisp objects:

  1. #s(hash-table…) = regular representation
  2. (ht…) form
  3. (h*…), (h-s*…), (h-t*…), or (h-st*…) form

or any of the above formatted as string (presumably with %S)

FORMAT is how you want your hash table to be written to FILENAME,
and may be any one of the following:

  1. htbl-cm :: #s(hash-table…) = compact representation
  2. h*-cm :: (h*…) in compact format
  3. ht-cm :: (ht…) in compact format
  4. htbl-pp :: #s(hash-table…) = in pretty-printed format
  5. h*-pp :: (h*…) in pretty-printed format
  6. ht-pp :: (ht…) in pretty-printed format

For convenience, you may drop the -pp and it will be implied:

  1. htbl :: #s(hash-table…) = in pretty-printed format
  2. h* :: (h*…) in pretty-printed format
  3. ht :: (ht…) in pretty-printed format

(It's implicit that these are strings, since we're writing to a
file, so we can dispense '-str' suffixes)

Any of these FORMAT options may be passed as either a string, a
keyword, or a symbol — as you prefer. So, for example, any among
"h*-pp", :h*-pp, or 'h*-pp works.

When the table being read has an equality test different than
'equal and either h*-cm or h*-pp is chosen, the output form will be
made into (h-st*…), for which size and test are explicit, thereby
preserving the original test equality information. (This can't be
done with the (ht…) form, for which the equality test, if not
'equal, would be implicitly converted to 'equal.)

Which of the five should you choose?

  • Either of the pretty-printed h-formats are much more readable by
    humans and can be easily browsed in the command line with the
    'less' command line utility. Disadvantages:
    1. conversion to and from h-formats costs a bit of time, which
      might become noticeable when dealing with larger tables.
    2. less portable, depends on this library.
    3. occupies a tiny bit more of disk space (about which you
      probably don't care) because of the added whitespace.
  • The compact regular form and the compact h-forms often occupy a
    single line (unless their keys or values have line breaks), and
    as such aren't suitable for using with 'less' — it'll flow
    straight to the end. And if you read any of them into a buffer,
    you may run into Emacs problem of displaying buffers with very
    long lines.
  • The regular form has the advantage of being universal, which may
    be preferable if the file is shared or if for some reason you
    don't have access to this and/or the 'ht libraries. But it's
    harder for human reading, especially when nested, because it's
    displayed unformatted in one line, and because of its metadata.
  • The regular form is also faster. If you're dealing with huge hash
    tables, it'll be faster to write and read regular compact forms
    than converting them.
  • The h-forms may have more security issues when reading files from
    untrustworthy sources. Consider that this:

    #s(hash-table test equal data (:a (+ 2 3)))
    

    has as value for :a the quoted expression '(+ 2 3), whereas any
    of these:

    (h*  :a (+ 2 3))
    (ht (:a (+ 2 3)))
    

    will see it evaluated to 5 if you read with h-read-h a file
    containing it. This seems unlikely to be a problem if you control
    the reading and writing. In any case, you can inspect it by using
    h-read-list instead — which will assume it's just a plain list
    and return it to you quoted, without evaluating it. If it's a
    huge expression you may want to apply pp to it. Compare the
    results of:

    (h-read-h        "(ht (:a (+ 2 3)))")
    (h-read-list     "(ht (:a (+ 2 3)))")
    (pp (h-read-list "(ht (:a (+ 2 3)))"))
    

Any of the h-forms can be easily read and re-converted to any
hash-table–like format with h-format, which see.

For convenience, partial-application functions are available for
the different formats:

The pp ones are aliased, respectively, to the shorter:

  • h-write-htbl!, h-write-h*! and h-write-ht!.

See also: h-read-h and h-read.

 ;;;; Hash tables stored in any form: regular, or as h-form
 ;;;;; Regular — should work just like h-read-htbl
 (let ((temp (make-temp-file "h-ert-test--")))
   (prog2
       (h-write-ht-like! temp :htbl-cm (h* "a" 1 "b" 2))
       (with-temp-buffer (insert-file-contents temp)
                         (buffer-string))
     (delete-file temp)))
 => "#s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data (\"a\" 1 \"b\" 2))"

 ;;;;; As h-forms
 (let ((temp (make-temp-file "h-ert-test--")))
   (prog2 ;; same as the previous — different write format
       (h-write-ht-like! temp :ht-cm (h* "a" 1 "b" 2))
       (with-temp-buffer (insert-file-contents temp)
                         (buffer-string))
     (delete-file temp)))
 => "(ht (\"a\" 1) (\"b\" 2))"

 (let ((temp (make-temp-file "h-ert-test--")))
   (prog2 ;; same as the previous — different write format
       (h-write-ht-like! temp :h*-cm (h* "a" 1 "b" 2))
       (with-temp-buffer (insert-file-contents temp)
                         (buffer-string))
     (delete-file temp)))
 => "(h* \"a\" 1 \"b\" 2)"
h-write-htbl-cm! (filename ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as 'htbl-cm.

For more about this output format, see h-write-ht!.
For more about FILENAME and MUSTBENEW, see h-write!.

 (let ((temp (make-temp-file "h-ert-test--")))
   (prog2 ;; same as =h-write-ht!= example — but writes as hash table
       (h-write-htbl-cm! temp "(ht (\"a\" 1) (\"b\" 2))")
       (with-temp-buffer (insert-file-contents temp)
                         (buffer-string))
     (delete-file temp)))
 => "#s(hash-table size 65 test equal\
rehash-size 1.5 rehash-threshold 0.8 data (\"a\" 1 \"b\" 2))"

 (let ((temp (make-temp-file "h-ert-test--")))
   (prog2 ;; same as the previous — but reads hash-table as string
       (h-write-htbl-cm! temp "#s(hash-table size 2 test equal
                                  rehash-size 1.5 rehash-threshold 0.8
                                  data (\"a\" 1 \"b\" 2))")
       (with-temp-buffer (insert-file-contents temp)
                         (buffer-string))
     (delete-file temp)))
 => "#s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data (\"a\" 1 \"b\" 2))"

 (let ((temp (make-temp-file "h-ert-test--")))
   (prog2 ;; same as the previous — but reads hash-table proper
       (h-write-htbl-cm! temp #s(hash-table
                                 size 2 test equal
                                 rehash-size 1.5 rehash-threshold 0.8
                                 data ("a" 1 "b" 2)))
       (with-temp-buffer (insert-file-contents temp)
                         (buffer-string))
     (delete-file temp)))
 => "#s(hash-table size 2 test equal\
rehash-size 1.5 rehash-threshold 0.8 data (\"a\" 1 \"b\" 2))"
h-write-htbl-pp! (filename ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as 'htbl-pp.

For more about this output format, see h-write-ht!.
For more about FILENAME and MUSTBENEW, see h-write!.

;; To make sure the string is EXACTLY the same when pretty-printing,
;; since different Emacsen may have different configs for indentation:
(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line)
       (temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write-htbl-pp! temp "(ht (\"a\" 1) (\"b\" 2))")
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              (\"a\" 1 \"b\" 2))\n"

(let* (fill-prefix
       indent-region-function
       (indent-line-function #'lisp-indent-line)
       (temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — but reads hash-table as string
      (h-write-htbl-pp! temp "#s(hash-table size 2 test equal
                                 rehash-size 1.5 rehash-threshold 0.8
                                 data (\"a\" 1 \"b\" 2))")
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "#s(hash-table size 2 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              (\"a\" 1 \"b\" 2))\n"

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — but reads hash-table proper
      (h-write-htbl-pp! temp #s(hash-table
                                size 2 test equal
                                rehash-size 1.5 rehash-threshold 0.8
                                data ("a" 1 "b" 2)))
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "#s(hash-table size 2 test equal rehash-size 1.5 rehash-threshold 0.8 data\n              (\"a\" 1 \"b\" 2))\n"
h-write-h*-cm! (filename ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as 'h*-cm.

For more about this output format, see h-write-ht!.
For more about FILENAME and MUSTBENEW, see h-write!.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — shorter
      (h-write-h*-cm! temp (h* "a" 1 "b" 2))
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h* \"a\" 1 \"b\" 2)"

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — table eval'd with (ht…)
      (h-write-h*-cm! temp (ht ("a" 1) ("b" 2)))
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h* \"a\" 1 \"b\" 2)"

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — different input format (quoted)
      (h-write-h*-cm! temp '(ht ("a" 1) ("b" 2)))
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h* \"a\" 1 \"b\" 2)"

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — more quotes
      (h-write-h*-cm! temp ''''''(ht ("a" 1) ("b" 2)))
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h* \"a\" 1 \"b\" 2)"

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — different input format (string)
      (h-write-h*-cm! temp "''''''(ht (\"a\" 1) (\"b\" 2))")
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h* \"a\" 1 \"b\" 2)"

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2 ;; same as the previous — no quotes
      (h-write-h*-cm! temp "(ht (\"a\" 1) (\"b\" 2))")
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h* \"a\" 1 \"b\" 2)"

;;;;; With a different equality test for the hash table

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write-h*-cm! temp '(h-t* 'eq "a" 1))
      (with-temp-buffer (insert-file-contents temp)
                        (buffer-string))
    (delete-file temp)))
=> "(h-st* 1 (quote eq) \"a\" 1)"
h-write-ht-cm! (filename ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as 'ht-cm.

For more about this output format, see h-write-ht!.
For more about FILENAME and MUSTBENEW, see h-write!.

h-write-h*-pp! (filename ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as 'h*-pp.

For more about this output format, see h-write-ht!.
For more about FILENAME and MUSTBENEW, see h-write!.

Alias: h-write-h*!.

(let ((temp (make-temp-file "h-ert-test--")))
  ;; To make sure the string is EXACTLY the same when pretty-printing,
  ;; since different Emacsen may have different configs for indentation:
  (let* (fill-prefix
         indent-region-function
         (indent-line-function #'lisp-indent-line))
    (prog2
        (h-write-h*-pp! temp "(ht (\"a\" 1) (\"b\" 2))")
        (with-temp-buffer (insert-file-contents temp)
                          (buffer-string))
      (delete-file temp))))
=> "(h* \"a\" 1\n    \"b\" 2)"

(let ((temp (make-temp-file "h-ert-test--")))
  (let* (fill-prefix
         indent-region-function
         (indent-line-function #'lisp-indent-line))
    ;; same as previous example — pp implicit in the function's name
    (prog2
        (h-write-h*! temp "(ht (\"a\" 1) (\"b\" 2))")
        (with-temp-buffer (insert-file-contents temp)
                          (buffer-string))
      (delete-file temp))))
=> "(h* \"a\" 1\n    \"b\" 2)"
h-write-ht-pp! (filename ht-like &optional mustbenew)

Write hash table HT-LIKE to file FILENAME formatted as 'ht-pp.

For more about this output format, see h-write-ht!.
For more about FILENAME and MUSTBENEW, see h-write!.

Alias: h-write-ht!.

(let ((temp (make-temp-file "h-ert-test--")))
  (let* (fill-prefix
         indent-region-function
         (indent-line-function #'lisp-indent-line))
    (prog2 ;; same as =h-write-ht!= example — but writes as (ht…)
        (h-write-ht-pp! temp "(ht (\"a\" 1) (\"b\" 2))")
        (with-temp-buffer (insert-file-contents temp)
                          (buffer-string))
      (delete-file temp))))
=> "(ht (\"a\" 1)\n    (\"b\" 2))"
Read

Functions that read objects, possible from a file or a buffer.
The generic function is h-read, with others that take one argument offered
for convenience — so, for example:

(h-read-alist obj)

is the same as

(h-read 'alist obj)

Note that h-read-htbl expects OBJ to be of regular hash table type:
#s(hash-table…). But we also have a h-read-h that also accepts, quoted,
the h-forms: '(ht…), '(h*…), '(h-s*…), '(h-t*…), or '(h-st*…). It also accepts
any of these passed as a string: "#s(hash-table…)", "(h*…)", etc.

This makes it flexible to read from files, and to have hash tables stored in
files as h-forms.

h-read (type obj)

Deal with object OBJ, expected to be of type TYPE.

  • If OBJ, a string, is a valid path of an existing file,
    read as string the contents of file.
  • If OBJ is a buffer, read as string the contents of buffer.
  • Otherwise, take OBJ as it is.

Then read it and test it according to xht--to-type, which see.

TYPE is a symbol, and can be one of:

  • 's OR 'str OR 'string
  • 'vec OR 'vector
  • 'alist
  • 'plist
  • 'list
  • 'cons-pair
  • 'lol
  • 'htbl
  • 'ht-like

When dealing with TSV, CSV, SSV, Org Table, or JSON:
use 'str (or 's, or 'string)
or the respective associated functions — same thing.

When dealing with hash tables, you have two options:

  • 'htbl expects something looking strictly like #s(hash-table…),
    and signals error otherwise.
  • 'ht-like is flexible and accepts any ht-like object — which is a
    Lisp object or string that looks like or contains one among:
    #s(hash-table…), (ht…), (h*…), (h-s*…), (h-t*…), or (h-st*…).

TYPE represents the expectation of what type the string input
should be, so it signals error when mismatched. So if, for example,
you expect your input to be a hash table #s(hash-table…), this:

(->> "/path/to/hash-table.el"  (h-read 'htbl)  h->lol)

would read this file looking for a hash table, and return it as
such (as Lisp object, no longer a string), to be converted to a
list of lists as specified. If it found, say, an alist in the file
it would signal error.

You could also use the equivalent:

(-> "/path/to/hash-table.el"  h-read-htbl  h->lol)

and this also works:

(-> (ht (:a 1) (:b 2))  h-read-htbl  h->lol)

In this case, h-read-htbl returns the very hash table read, like
identity. No problem, but unnecessary — this would suffice:

(-> (ht (:a 1) (:b 2))  h->lol)

For convenience, partial-application functions are available which
receive only OBJ as argument:

h-read-s

plus

h-read-kvl, h-read-tsv, h-read-csv, h-read-ssv,
h-read-json, h-read-orgtbl, h-lines (all are h-read-s),

plus

h-read-lol, h-read-alist, h-read-plist, h-read-list,
h-read-cons-pair, h-read-vector

and for hash tables:

  • h-read-htbl for (h-read 'htbl…) = #s(hash-table…) only
  • h-read-h for (h-read 'ht-like…) = any ht-like form.

See also: h-write! and h-write-ht-like!.

;;;; Not hash tables
(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '(("a" . 1) ("b" . 2)))
      (h-read 'alist temp)
    (delete-file temp)))
=> '(("a" . 1) ("b" . 2))

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '("a" 1 "b" 2))
      (h-read 'plist temp)
    (delete-file temp)))
=> '("a" 1 "b" 2)

;;;; Hash tables stored as such
(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp (h* "a" 1 "b" 2))
      (h-read 'htbl temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)
h-read-ht-like (obj)

Deal with object OBJ, expected to be ht-like.

For more, see h-read.
Alias: h-read-h.

;; h-read-h is an alias to h-read-ht-like.

;;;; This fails with h-read-htbl, but here they are
;;;; interpreted as hash tables
(h-read-h '(h* "a" 1 "b" 2))   H=> (h* "a" 1 "b" 2)
(h-read-h "(h* :a  1 :b  2)")  H=> (h* :a  1 :b  2)

;;;; Direct reading
;;;;; Regular — should work just like h-read-htbl
(h-read-h (h* "a" 1 "b" 2))
H=> (h* "a" 1 "b" 2)

;;;;; As h-forms
;; Notice: these are quoted lists
(h-read-h '(ht ("a" 1) ("b" 2))) H=> (h* "a" 1 "b" 2)
(h-read-h '(h* "a" 1 "b" 2))     H=> (h* "a" 1 "b" 2)
(h-read-h ''''(h* "a" 1 "b" 2))  H=> (h* "a" 1 "b" 2)
(h-read-h '(h-t* 'eq :a 1))    Hp==> (h-st* 1 'eq :a 1)

;;;;; As strings
(h-read-h "#s(hash-table size 2 test equal
                  rehash-size 1.5 rehash-threshold 0.8 data
                  (\"a\" 1 \"b\" 2))")
H=> (h* "a" 1 "b" 2)

(h-read-h "(h* \"a\" 1 \"b\" 2)")     H=> (h* "a" 1 "b" 2)
(h-read-h "'''(h* \"a\" 1 \"b\" 2)")  H=> (h* "a" 1 "b" 2)
(h-read-h "(ht (\"a\" 1) (\"b\" 2))") H=> (h* "a" 1 "b" 2)
(h-read-h "")                         H=> (h-new 1)

;; ... and if it works with the strings above, then ...

;;;; Hash tables stored in any form: regular, or as h-form
;;;;; Regular — should work just like h-read-htbl
(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp (h* "a" 1 "b" 2))
      (h-read-h temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp "#s(hash-table size 2 test equal
                             rehash-size 1.5 rehash-threshold 0.8
                             data (\"a\" 1 \"b\" 2))")
      (h-read-h temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp (h-new))
      (h-read-h temp)
    (delete-file temp)))
H=> (h-new)

   ;;;;; As h-forms
(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      ;; Reading a quoted (ht…) form and writing a compact (h*…) form —
      ;; which is not obvious from this example, because h-read-h
      ;; interprets any h form and outputs regular hash table. For that,
      ;; see the h-write-ht-like! examples, where it's clearer.
      (h-write-h*-cm! temp '(ht ("a" 1) ("b" 2)))
      (h-read-h temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write-h*! temp (h* "a" 1 "b" 2)) ;; evaluated
      (h-read-h temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write-h*! temp ''''(h* "a" 1 "b" 2)) ;; multiple quotes
      (h-read-h temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write-h*! temp "(h* \"a\" 1 \"b\" 2)") ;; string
      (h-read-h temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write-h*! temp '(h-t* 'eq :a 1)) ;; changed test
      (h-read-h temp)
    (delete-file temp)))
Hp==> (h-st* 1 'eq :a 1)
h-read-htbl (obj)

Deal with object OBJ, expected to be a hash table.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp (h* "a" 1 "b" 2))
      (h-read-htbl temp)
    (delete-file temp)))
H=> (h* "a" 1 "b" 2)

(h-read-htbl (h* "a" 1 "b" 2))  H=> (h* "a" 1 "b" 2)

;;;; These are not interpreted as hash tables — use
;;;; h-read-h (alias to h-read-htbl-like) instead
(h-read-htbl '(h* "a" 1 "b" 2))   => '(h* "a" 1 "b" 2)
(h-read-htbl "(h* :a  1 :b  2)") !!> error
h-read-s (obj)

Deal with object OBJ, expected to be a string.

For more, see h-read.

;; [[#h-read-s][h-read-s]] can be used for many string formats
;; here's an org table example
(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "| Name  | Editor |\n|-------+-------|\n| Alice | Emacs |")
      (h-read-s temp) ;; or the same : (h-read-orgtbl temp)
    (delete-file temp)))
=> "| Name  | Editor |\n|-------+-------|\n| Alice | Emacs |"
h-read-kvl (obj)

Deal with object OBJ, expected to be a KVL.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "alice = emacs\nbob   = vim")
      (h-read-s temp)
    (delete-file temp)))
=> "alice = emacs\nbob   = vim"
h-read-tsv (obj)

Deal with object OBJ, expected to be a TSV.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "Name\tEditor\nAlice\tEmacs\nBob\tVim")
      (h-read-tsv temp)
    (delete-file temp)))
=> "Name\tEditor\nAlice\tEmacs\nBob\tVim"
h-read-csv (obj)

Deal with object OBJ, expected to be a CSV.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "Name,Editor\nAlice,Emacs\nBob,Vim")
      (h-read-csv temp)
    (delete-file temp)))
=> "Name,Editor\nAlice,Emacs\nBob,Vim"
h-read-ssv (obj)

Deal with object OBJ, expected to be a SSV.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "Name   Editor\nAlice  Emacs\nBob    Vim")
      (h-read-ssv temp)
    (delete-file temp)))
=> "Name   Editor\nAlice  Emacs\nBob    Vim"
h-read-json (obj)

Deal with object OBJ, expected to be JSON.

For more, see h-read.

  (let ((temp (make-temp-file "h-ert-test--")))
    (prog2
        (h-write!
         temp
         "\
{\"Alice\":{\"Name\":\"Alice\",\"Editor\":\"Emacs\"},
\"Bob\":{\"Name\":\"Bob\",\"Editor\":\"Vim\"}}")
        (h-read-json temp)
      (delete-file temp)))
  => "\
{\"Alice\":{\"Name\":\"Alice\",\"Editor\":\"Emacs\"},
\"Bob\":{\"Name\":\"Bob\",\"Editor\":\"Vim\"}}"
h-read-lines (obj)

Deal with object OBJ, expected to be lines of text.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "Hello\nthere,\nWorld!")
      (h-read-lines temp)
    (delete-file temp)))
=> "Hello\nthere,\nWorld!"
h-read-orgtbl (obj)

Deal with object OBJ, expected to be an org table.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write!
       temp
       "| Name  | Editor |\n|-------+-------|\n| Alice | Emacs |")
      (h-read-orgtbl temp)
    (delete-file temp)))
=> "| Name  | Editor |\n|-------+-------|\n| Alice | Emacs |"
h-read-lol (obj)

Deal with object OBJ, expected to be a list of lists.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '((letter number) ("a" 1) ("b" 2)))
      (h-read-lol temp)
    (delete-file temp)))
L=> '((letter number) ("a" 1) ("b" 2))
h-read-alist (obj)

Deal with object OBJ, expected to be an association list.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '(("a" . 1) ("b" . 2)))
      (h-read-alist temp)
    (delete-file temp)))
A=> '(("a" . 1) ("b" . 2))
h-read-plist (obj)

Deal with object OBJ, expected to be a property list.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '("a" 1 "b" 2))
      (h-read-plist temp)
    (delete-file temp)))
P=> '("a" 1 "b" 2)
h-read-list (obj)

Deal with object OBJ, expected to be a list.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '(1 2 3))
      (h-read-list temp)
    (delete-file temp)))
=> '(1 2 3)
h-read-cons-pair (obj)

Deal with object OBJ, expected to be a cons pair.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp '("a" . 1))
      (h-read-cons-pair temp)
    (delete-file temp)))
=> '("a" . 1)
h-read-vector (obj)

Deal with object OBJ, expected to be a vector.

For more, see h-read.

(let ((temp (make-temp-file "h-ert-test--")))
  (prog2
      (h-write! temp [1 2 3])
      (h-read-vector temp)
    (delete-file temp)))
=> [1 2 3]
Miscellaneous
Format object as symbol, string, keyword, or number

Functions to turn object into a desirable format.

These functions were initially internal only (and therefore named
as 'xht--as-string', etc), since they weren't directly about hash
tables.

But they are useful for formatting keys and values, which is a
common enough operation — so I renamed them and made them public.
The alternative would have been to have you either write the code
yourself every time you wanted to do these operations, or else use
the longer-named internal functions, both of which seemed worse.

h-as-string (obj)

Return OBJ as a string.

(h--hmap (h-as-string key)
         (h-as-string value)
         (h* :a 1
             :b 2
             :c 3))
H=> (h* "a" "1"
        "b" "2"
        "c" "3")

(h-as-string "Emacs") => "Emacs"
(h-as-string :Emacs)  => "Emacs"
(h-as-string 'Emacs)  => "Emacs"
(h-as-string 42)      => "42"
h-as-keyword (obj)

Return OBJ as a keyword.

(h--hmap (h-as-keyword key)
         (h-as-keyword value)
         (h* 'a 1
             'b 2
             'c 3))
H=> (h* :a :1
        :b :2
        :c :3)

(h-as-keyword "Emacs") => :Emacs
(h-as-keyword :Emacs)  => :Emacs
(h-as-keyword 'Emacs)  => :Emacs
(h-as-keyword 42)      => :42
h-as-symbol (obj)

Return OBJ as an interned symbol.

(h--hmap (h-as-symbol key)
         (h-as-symbol value)
         (h* :a 1
             :b 2
             :c 3))
H=> (h* 'a '\1
        'b '\2
        'c '\3)

(h-as-symbol "Emacs") => 'Emacs
(h-as-symbol :Emacs)  => 'Emacs
(h-as-symbol 'Emacs)  => 'Emacs
(h-as-symbol 42)      => '\42
h-as-number (obj)

Return OBJ as a number, if convertible.

If not convertible, signal error.

(h--hmap (h-as-string key)
         (h-as-number value)
         (h* :a "1"
             :b "2"
             :c "3"))
H=> (h* "a" 1
        "b" 2
        "c" 3)

(h-as-number "Emacs")    !!> error
(h-as-number :Emacs)     !!> error
(h-as-number 'Emacs)     !!> error
(h-as-number   042)       =>  42
(h-as-number  "042")      =>  42
(h-as-number '\042)       =>  42
(h-as-number  :042)       =>  42
(h-as-number    42)       =>  42
(h-as-number   "42")      =>  42
(h-as-number  '\42)       =>  42
(h-as-number   :42)       =>  42
(h-as-number     4.2)     =>   4.2
(h-as-number    "4.2")    =>   4.2
(h-as-number   '\4.2)     =>   4.2
(h-as-number    :4.2)     =>   4.2
(h-as-number     0)       =>   0
(h-as-number    "0")      =>   0
(h-as-number   '\0)       =>   0
(h-as-number    :0)       =>   0
(h-as-number    -0.0)     =>  -0.0
(h-as-number   "-0.0")    =>  -0.0
(h-as-number  '\-0.0)     =>  -0.0
(h-as-number   :-0.0)     =>  -0.0
(h-as-number    -4.2)     =>  -4.2
(h-as-number   "-4.2")    =>  -4.2
(h-as-number  '\-4.2)     =>  -4.2
(h-as-number   :-4.2)     =>  -4.2
(h-as-number   -42)       => -42
(h-as-number  "-42")      => -42
(h-as-number '\-42)       => -42
(h-as-number  :-42)       => -42
(h-as-number   #b101010)  =>  42
(h-as-number   #o52)      =>  42
(h-as-number   #x2a)      =>  42
(h-as-number   #24r1i)    =>  42
(h-as-number  "#b101010") =>  42
(h-as-number  "#o52")     =>  42
(h-as-number  "#x2a")     =>  42
(h-as-number  "#24r1i")   =>  42
(h-as-number  '#b101010)  =>  42
(h-as-number  '#o52)      =>  42
(h-as-number  '#x2a)      =>  42
(h-as-number  '#24r1i)    =>  42
(h-as-number :\#b101010)  =>  42
(h-as-number :\#o52)      =>  42
(h-as-number :\#x2a)      =>  42
(h-as-number :\#24r1i)    =>  42

Specialized hash table operations

“Hash tabular”: 2D-specific operations

Functions to operate on two-dimensional hash tables.

A 2D hash table looks like this:

(h* "n01" (h* "n" "n01"
              "name" "Alice")
              "age" "42"
              "editor" "Emacs"
    "n02" (h* "n" "n02"
              "name" "Emily"
              "age" "21"
              "editor" "Nano"))

It looks like this when converted to an org table:

| n   | name  | age | editor |
|-----+-------+-----+--------|
| n01 | Alice |  42 | Emacs  |
| n02 | Emily |  21 | Nano   |

or to a list of lists:

'(("n"   "name"  "age" "editor")
  ("n01" "Alice" "42"  "Emacs")
  ("n02" "Emily" "21"  "Nano"))

or to SSV:

n    name   age  editor
n01  Alice  42   Emacs
n02  Emily  21   Nano

or to JSON:

{
  "n01": {
    "n":      "n01",
    "name":   "Alice"
    "age":    "42",
    "editor": "Emacs",
  },
  "n02": {
    "n":      "n02",
    "name":   "Emily"
    "age":    "21",
    "editor": "Nano",
  }
}

Name of its elements (I'm removing the string quotes here):

  • header: '(n name age editor)
  • idcol: '(n n01 n02)
  • id: n

Either of these three triads of labels could be given in reference
to "Alice" in the example above:

n01 name Alice
key key of value value of value
key field value
row column cell
  • The first is exact but awkward.
  • In code I often opted for the second.
  • The third is also nice, but row and column most often alludes to the entire
    row and column, not to the first item of either — to which we want to refer
    here. But it's understood if we say that the "intersection of row 'n01' and
    column 'name' is 'Alice'". This is "field 'name' of key 'n01'" (second)
    or the awkward "the value of the key 'name' of the value of key 'n01'"
    (first).

See also: h-1d<-2d and h-2d<-1d under
General hash table operations > Conversion > 2D > Hash table to hash table.

Creation

Functions for creating 2D hash tables.

h-2d-new (keys header &optional size test)

Return a 2D hash table from KEYS and HEADER.

Both must be lists.

If either is nil, or not a list, return empty hash table.

All field values are set to nil, where field is
an element of header.

For the meaning of SIZE and TEST, see h-new.

(h-2d-new '("Alice" "Bob" "Emily")
          '("name" "age" "editor"))
H=> (h* "Alice" (h* "name"  "Alice"
                    "age"    nil
                    "editor" nil)
        "Bob"   (h* "name"  "Bob"
                    "age"    nil
                    "editor" nil)
        "Emily" (h* "name"  "Emily"
                    "age"    nil
                    "editor" nil))

(h-2d-new '("Alice" "Bob") '("name"))
H=> (h* "Alice" (h* "name"  "Alice")
        "Bob"   (h* "name"  "Bob"))

(h-2d-new nil  nil) H=> (h-new)
(h-2d-new nil  3)   H=> (h-new)
(h-2d-new 3    nil) H=> (h-new)
(h-2d-new '(3) nil) H=> (h-new)
(h-2d-new nil '(3)) H=> (h-new)
Information

Functions for retrieving information about 2D hash tables.
Note: h-2d-keys is an alias to h-lkeys (for consistency).

h-2d-header (table2d)

Return the header of 2D hash table TABLE2d.

The header is the union of all keys-of-keys of TABLE2D.

This function does the heavy work to correctly guess what the
table's ID would be — which h-2d-id then uses.

(h-2d-header (ht))                                 => nil
(h-2d-header (ht ("a" nil)))                       => nil
(h-2d-header (ht ("a" 1) ("b" 2)))                 => nil
(h-2d-header (ht ("a" (h* :id "a" :cap "A"))))     => '(:id :cap)

(h-2d-header (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :id "b" :cap "B"))))
=> '(:id :cap)

(h-2d-header (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :id 'b  :cap "B"))))
=> nil

(h-2d-header (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :cap "B" :id "b"))
                 ("c" (h* :id "c"))))
=> '(:id :cap)

(h-2d-header (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :cap "B" :id "b"))
                 ("c" (h* :id "C"))))
=> nil

(h-2d-header (ht ("a" (h* :id "a" :cap "A"))
                 ("b" (h* :cap "B" :id "b"))
                 ("c" (h* :low "c"))))
=> nil

(h-2d-header (ht ("a" (h* :cap    "A"  :id "a" ))
                 ("b" (h* :cap    "B"  :id "b"))
                 ("c" (h* :double "cc" :id "c"))))
=> '(:id :cap :double)

(h-2d-header (ht ("a" (h* :id "a" :same "a"))
                 ("b" (h* :id "b" :same "b"))))
=> '(:id :same)
h-2d-idcol (table2d)

The ID column of 2D hash table TABLE2D, as list. It's ID + keys.

(h-2d-idcol (ht))                                 => nil
(h-2d-idcol (ht ("a" nil)))                       => nil
(h-2d-idcol (ht ("a" 1) ("b" 2)))                 => nil
(h-2d-idcol (ht ("a" (h* :id "a" :cap "A")))  )   => '(:id "a")

(h-2d-idcol (ht ("a" (h* :id "a" :cap "A"))
                ("b" (h* :id "b" :cap "B"))))
=> '(:id "a" "b")

(h-2d-idcol (ht ("a" (h* :id "a" :cap "A"))
                ("b" (h* :id 'b  :cap "B"))))
=> nil

(h-2d-idcol (ht ("a" (h* :id "a" :cap "A"))
                ("b" (h* :cap "B" :id "b"))
                ("c" (h* :id "c"))))
=> '(:id "a" "b" "c")

(h-2d-idcol (ht ("a" (h* :id "a" :cap "A"))
                ("b" (h* :cap "B" :id "b"))
                ("c" (h* :id "C"))))
=> nil

(h-2d-idcol (ht ("a" (h* :id "a" :cap "A"))
                ("b" (h* :cap "B" :id "b"))
                ("c" (h* :low "c"))))
=> nil

(h-2d-idcol (ht ("a" (h* :cap    "A"  :id "a" ))
                ("b" (h* :cap    "B"  :id "b"))
                ("c" (h* :double "cc" :id "c"))))
=> '(:id "a" "b" "c")

(h-2d-idcol (ht ("a" (h* :id "a" :same "a"))
                ("b" (h* :id "b" :same "b"))))
=> '(:id "a" "b")
h-2d-id (table2d)

Find the ID-like element of the header of 2D hash table TABLE2D.

It's the one associated with the table's keys.
If none is found, return nil, in which case it's not a 2D hash table.
If more than one is found (unlikely), use the first.

(h-2d-id (ht))                                 => nil
(h-2d-id (ht ("a" nil)))                       => nil
(h-2d-id (ht ("a" 1) ("b" 2)))                 => nil
(h-2d-id (ht ("a" (h* :id "a" :cap "A"))))     => :id

(h-2d-id (ht ("a" (h* :id "a" :cap "A"))
             ("b" (h* :id "b" :cap "B"))))
=> :id

(h-2d-id (ht ("a" (h* :id "a" :cap "A"))
             ("b" (h* :id 'b  :cap "B"))))
=> nil

(h-2d-id (ht ("a" (h* :id "a" :cap "A"))
             ("b" (h* :cap "B" :id "b"))
             ("c" (h* :id "c"))))
=> :id

(h-2d-id (ht ("a" (h* :id "a" :cap "A"))
             ("b" (h* :cap "B" :id "b"))
             ("c" (h* :id "C"))))
=> nil

(h-2d-id (ht ("a" (h* :id "a" :cap "A"))
             ("b" (h* :cap "B" :id "b"))
             ("c" (h* :low "c"))))
=> nil

(h-2d-id (ht ("a" (h* :cap    "A"  :id "a" ))
             ("b" (h* :cap    "B"  :id "b"))
             ("c" (h* :double "cc" :id "c"))))
=> :id

(h-2d-id (ht ("a" (h* :id "a" :same "a"))
             ("b" (h* :id "b" :same "b"))))
=> :id
h-2d-row (table2d key &optional header)

List row from 2D hash table TABLE2D given KEY and HEADER.

Example: (h-2d-row tb2 "id01" '(:id :name :age)) could
return: '("id01" "Mary" 27).

HEADER can be given in some desirable permutation.
If HEADER is nil, it will be inferred with h-2d-header.

(h-2d-row (ht) "a")                                 => nil
(h-2d-row (ht ("a" nil)) "a")                       => nil
(h-2d-row (ht ("a" nil)) "b")                       => nil
(h-2d-row (ht ("a" 1) ("b" 2)) "a")                 => nil
(h-2d-row (ht ("a" (h* :id "a" :cap "A"))) "a")     => '("a" "A")

(h-2d-row (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id "b" :cap "B")))
          "b")
=> '("b" "B")

(h-2d-row (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id 'b  :cap "B")))
          "a")
=> nil

(h-2d-row (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "c")))
          "b")
=> '("b" "B")

(h-2d-row (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "c" :double "cc")))
          "c")
=> '("c" nil "cc")

(h-2d-row (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "C")))
          "a")
=> nil

(h-2d-row (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :low "c")))
          "a")
=> nil

(h-2d-row (ht ("a" (h* :cap    "A"  :id "a" ))
              ("b" (h* :cap    "B"  :id "b"))
              ("c" (h* :double "cc" :id "c")))
          "a")
=> '("a" "A" nil)

(h-2d-row (ht ("a" (h* :cap    "A"  :id "a" ))
              ("b" (h* :cap    "B"  :id "b"))
              ("c" (h* :double "cc" :id "c")))
          "c")
=> '("c" nil "cc")

(h-2d-row (ht ("a" (h* :id "a" :same "a"))
              ("b" (h* :id "b" :same "b")))
          "b")
=> '("b" "b")
h-2d-col (table2d col &optional idcol)

List column from 2D hash table TABLE2D given COL and IDCOL.

Example: (h-2d-col tb2 :name '(:id id01 id03 id02)) could
return: '(:name "Mary" "John" "Jane").

IDCOL is the ID plus keys.
It can be given in some desirable permutation.
If IDCOL is nil, it will be inferred with h-2d-idcol.

(h-2d-col (ht) :cap)                                 => nil
(h-2d-col (ht ("a" nil)) :cap)                       => nil
(h-2d-col (ht ("a" nil)) :cap)                       => nil
(h-2d-col (ht ("a" 1) ("b" 2)) :cap)                 => nil
(h-2d-col (ht ("a" (h* :id "a" :cap "A"))) :cap)     => '(:cap "A")

(h-2d-col (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id "b" :cap "B")))
          :cap)
=> '(:cap "A" "B")

(h-2d-col (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :id 'b  :cap "B")))
          "a")
=> nil

(h-2d-col (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "c")))
          :cap)
=> '(:cap "A" "B" nil)

(h-2d-col (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "c" :double "cc")))
          :double)
=> '(:double nil nil "cc")

(h-2d-col (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :id "C")))
          :cap)
=> nil

(h-2d-col (ht ("a" (h* :id "a" :cap "A"))
              ("b" (h* :cap "B" :id "b"))
              ("c" (h* :low "c")))
          :cap)
=> nil

(h-2d-col (ht ("a" (h* :cap    "A"  :id "a" ))
              ("b" (h* :cap    "B"  :id "b"))
              ("c" (h* :double "cc" :id "c")))
          :cap)
=> '(:cap "A" "B" nil)

(h-2d-col (ht ("a" (h* :cap    "A"  :id "a" ))
              ("b" (h* :cap    "B"  :id "b"))
              ("c" (h* :double "cc" :id "c")))
          :id)
=> '(:id "a" "b" "c")

(h-2d-col (ht ("a" (h* :id "a" :same "a"))
              ("b" (h* :id "b" :same "b")))
          :same)
=> '(:same "a" "b")
Mapping

Functions for mapping 2D hash tables.

to hash table 2D

Functions that apply three functions or forms to every key–field–value
triad of a 2D hash table and write the results to a hash table.

You can think of the key as the label of the row of the equivalent
org table it would produce; of the field as the label of the
column; and of the value as the contents of the cell.

For example, if you want to produce a fresh table2d whose keys are
upcased, fields are downcased, and the values (currently integers)
are increased by 1, you could run:

(h-2d-hmap (lambda (key _field _value) (upcase key))
           (lambda (_key field _value) (downcase field))
           (lambda (_key _field value) (1+ value))
           table2d)

The underlines above are so that there's no warnings about unused
variables.

The fact that in each function the only variable used was that
which it changes needed not be so. You could have well
had (upcase (concat key "-" field)) in the first one, for
example. But it's unlikely you'd want that. Most often you'd want
to only use key for the first one, only field for the second one,
and the third one could have any combination.

Finally, note that when the field is the table's ID (the table's
"first column"), the value will be the newly calculated key
instead of applying the value function to it. This is almost
certainly what you wanted to do: apply the value functions to all
columns but the first one, which is what you'd do in a spreadsheet
with labeled rows and columns.

Note that the item is only added if both KEY-FUN and FIE-FUN return
non-nil. So no spurious (nil, something) triad results from some
reasonable conditional KEY-FUN such as:

(lambda (key, field, value) (when (> value 2) key))

In the rare case where for some reason you chose to have nil as a
key, you'll have to treat it specially.

IMPORTANT: unlike mapping to a list, mapping to a hash table
demands that the results of the keys be unique. So you must pay
attention to possible collisions. If, for example, in the case
above the original table had both "a" and "A" as original keys,
one of them would end up overwritten, because both return "A"
when upcased. Which one will depend on the key order. So:

actual size of resulting table ≤ size of original table.

In mathematical terms, excepting the rare case of a nil key in the
original, the number of keys in the resulting hash table will match
the number of keys in the original one if, and only if:

  1. there's a bijection between the set of keys and the set of
    key-fun(key,field,value).
  2. there's no (key,field,value) for which either key-fun(key,field,value) or
    fie-fun(key,field,value) returns nil.

IMPORTANT: Just because you can pass three arbitrary functions
using all the three variables in each of them doesn't mean you
should. If you, for example, selectively change a field for some
keys but not others, you'll likely split the column, so one of them
will have empty values where the other has something, and
vice-versa. And other such odd operations. It seems unlikely that
you'd want that.

Guideline for the '2d-hmap family' of functions:

  • the added 'h' means 'result is a hash table'
  • an added '-' means 'anaphoric: enter 3 forms, not 3 lambdas'
  • an added '!' means 'destructive: modify TABLE instead of
    creating a fresh one'

In the anaphoric versions, if you don't use any of the let-bound
variables key, field, or value in any of the forms, it's ok — no
warnings.

Summary:

  3 lambdas 3 forms (anaphoric)
non-destructive h-2d-hmap h--2d-hmap
destructive h-2d-hmap! h--2d-hmap!
h-2d-hmap (key-fun fie-fun val-fun table2d)

Return a 2D table from applying functions to each triad of TABLE2D.

KEY-FUN, FIE-FUN, and VAL-FUN are each called with three arguments:
key, field, and value.

  • The result of KEY-FUN is the new key.
  • The result of FIE-FUN is the new field.
  • The result of VAL-FUN is the new value.

Its anaphoric counterpart is h--2d-hmap.

(h-2d-hmap (lambda (key _field _value) key)
           (lambda (_key field _value) (intern field))
           (lambda (_key field  value)
             (pcase field
               ;; Don't add an "id" pcase like here:
               ;; it'll be ignored. The id column (here
               ;; labeled "id") will always return key.
               ("id"   :will-be-ignored)
               ("name" (capitalize value))
               ("age"  (read value))))
           (h* "id01" (h* "id"   "id01"
                          "name" "alice"
                          "age"  "41")
               "id02" (h* "id"   "id02"
                          "name" "bob"
                          "age"  "30")
               "id03" (h* "id"   "id03"
                          "name" "emily"
                          "age"  "21")))
H=> (h* "id01" (h* 'id   "id01"
                   'name "Alice"
                   'age   41)
        "id02" (h* 'id   "id02"
                   'name "Bob"
                   'age   30)
        "id03" (h* 'id   "id03"
                   'name "Emily"
                   'age   21))

(->> (h-2d-new '("Alice" "Bob")
               '("name" "Nakayama" "Smith"))
     (h-2d-hmap (lambda (key   _   _) key)
                (lambda (_ field   _) field)
                (lambda (key field _)
                  (downcase
                   (format "%s-%s"
                           key field))))
     h->orgtbl)
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | alice-nakayama | alice-smith |
     | Bob   | bob-nakayama   | bob-smith   |"

(->> (h-2d-new '(10 21 42)
               '("n" 2 3 4))
     (h-2d-hmap (lambda (key   _   _) key)
                (lambda (_ field   _) field)
                (lambda (key field _)
                  (expt key field)))
     h->orgtbl)
O=> "|  n |    2 |     3 |       4 |
     |----+------+-------+---------|
     | 10 |  100 |  1000 |   10000 |
     | 21 |  441 |  9261 |  194481 |
     | 42 | 1764 | 74088 | 3111696 |"

;;;; Example of filtering
;;;;; Return only names, and only for entries whose "name" has a letter "i"
(h-2d-hmap (lambda (key field value)
             (and (equal field "name")
                  (s-contains? "i" value)
                  key))
           (lambda (_   field _    ) field)
           (lambda (_   _     value) value)
           (h* "id01" (h* "id"   "id01"
                          "name" "alice"
                          "age"  "41")
               "id02" (h* "id"   "id02"
                          "name" "bob"
                          "age"  "30")
               "id03" (h* "id"   "id03"
                          "name" "emily"
                          "age"  "21")))
H=> (h* "id01" (h* "name" "alice")
        "id03" (h* "name" "emily"))

;;;; Doesn't always need to be a lambda: variadic functions work too
(->> (h-2d-new '("Alice" "Bob")
               '("name" "Nakayama" "Smith"))
     (h-2d-hmap (lambda (key   _ _) key)
                (lambda (_ field _) field)
                #'concat)
     h->orgtbl)
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | AliceNakayama  | AliceSmith  |
     | Bob   | BobNakayama    | BobSmith    |"

;;;; Careful: depending on what you pass, the result may split fields
;;;;; Here's an example of that:

(h-2d-hmap (lambda (key   _     _)
             (upcase key))
           (lambda (_ field value)
             (pcase value
               ("bob"   nil)
               ("emily" (upcase field))
               (_       (intern field))))
           (lambda (_ field value)
             (pcase field
               ("id"   value)
               ("name" (capitalize value))
               ("age"  (read value))))
           (h* "id01" (h* "id"   "id01"
                          "name" "alice"
                          "age"  "41")
               "id02" (h* "id"   "id02"
                          "name" "bob"
                          "age"  "30")
               "id03" (h* "id"   "id03"
                          "name" "emily"
                          "age"  "21")))
;;;;;; Some odd thing...
H=> (h* "ID01" (h* 'id "ID01"
                   'name "Alice"
                   'age 41)
        "ID02" (h* 'id "ID02"
                   'age 30)
        "ID03" (h* 'id "ID03"
                   "NAME" "Emily"
                   'age 21))

;;;;;; ...that has split fields
(h->orgtbl (h* "ID01" (h* 'id "ID01"
                          'name "Alice"
                          'age 41)
               "ID02" (h* 'id "ID02"
                          'age 30)
               "ID03" (h* 'id "ID03"
                          "NAME" "Emily"
                          'age 21)))
O=>  "| id   | name  | age | NAME  |
      |------+-------+-----+-------|
      | ID01 | Alice |  41 | nil   |
      | ID02 | nil   |  30 | nil   |
      | ID03 | nil   |  21 | Emily |"
;;;;;; ^ This applies to the other functions of this family as well.
h--2d-hmap (key-form fie-form val-form table2d)

Anaphoric version of h-2d-hmap.

For every key–value pair in TABLE2D, evaluate KEY-FORM, FIE-FORM,
VAL-FORM with the variables key, field, and value bound.

(h--2d-hmap key (intern field) (pcase field
                                 ;; Don't add an "id" pcase like here:
                                 ;; it'll be ignored. The id column (here
                                 ;; labeled "id") will always return key.
                                 ("id"   :will-be-ignored)
                                 ("name" (capitalize value))
                                 ("age"  (read value)))
            (h* "id01" (h* "id"   "id01"
                           "name" "alice"
                           "age"  "41")
                "id02" (h* "id"   "id02"
                           "name" "bob"
                           "age"  "30")
                "id03" (h* "id"   "id03"
                           "name" "emily"
                           "age"  "21")))
H=> (h* "id01" (h* 'id   "id01"
                   'name "Alice"
                   'age   41)
        "id02" (h* 'id   "id02"
                   'name "Bob"
                   'age   30)
        "id03" (h* 'id   "id03"
                   'name "Emily"
                   'age   21))

(let ((tbl2d (h* "id01" (h* "id"   "id01"
                            "name" "alice"
                            "age"  "41")
                 "id02" (h* "id"   "id02"
                            "name" "bob"
                            "age"  "30")
                 "id03" (h* "id"   "id03"
                            "name" "emily"
                            "age"  "21"))))
  (h--2d-hmap (h-as-keyword  key)
              (h-as-symbol field)
              (if (s-numeric?  value)
                  (h-as-number value)
                value)
              tbl2d))
H=> (h* :id01 (h* 'id :id01
                  'name "alice"
                  'age 41)
        :id02 (h* 'id :id02
                  'name "bob"
                  'age 30)
        :id03 (h* 'id :id03
                  'name "emily"
                  'age 21))

(->> (h-2d-new '("Alice" "Bob")
               '("name" "Nakayama" "Smith"))
     (h--2d-hmap key field (downcase
                            (format "%s-%s"
                                    key field)))
     h->orgtbl)
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | alice-nakayama | alice-smith |
     | Bob   | bob-nakayama   | bob-smith   |"

(->> (h-2d-new '("Alice" "Bob")
               '("name" "Nakayama" "Smith"))
     (h--2d-hmap key field (concat key field))
     h->orgtbl)
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | AliceNakayama  | AliceSmith  |
     | Bob   | BobNakayama    | BobSmith    |"

(->> (h-2d-new '(10 21 42)
               '("n" 2 3 4))
     (h--2d-hmap key field (expt key field))
     h->orgtbl)
O=> "|  n |    2 |     3 |       4 |
     |----+------+-------+---------|
     | 10 |  100 |  1000 |   10000 |
     | 21 |  441 |  9261 |  194481 |
     | 42 | 1764 | 74088 | 3111696 |"
h-2d-hmap! (key-fun fie-fun val-fun table2d)

Update TABLE2D by applying functions to each of its triads.

Functions are KEY-FUN, FIE-FUN, VAL-FUN.

Just like h-2d-hmap (which see), but modifies TABLE2D instead of
producing a fresh one.

Its anaphoric counterpart is h--2d-hmap!.

(let ((tbl2d (h* "id01" (h* "id"   "id01"
                            "name" "alice"
                            "age"  "41")
                 "id02" (h* "id"   "id02"
                            "name" "bob"
                            "age"  "30")
                 "id03" (h* "id"   "id03"
                            "name" "emily"
                            "age"  "21"))))
  (h-2d-hmap! (lambda (key _field _value) key)
              (lambda (_key field _value) (intern field))
              (lambda (_key field  value)
                (pcase field
                  ;; Don't add an "id" pcase like here:
                  ;; it'll be ignored. The id column (here
                  ;; labeled "id") will always return key.
                  ("id"   :will-be-ignored)
                  ("name" (capitalize value))
                  ("age"  (read value))))
              tbl2d)
  tbl2d)
H=> (h* "id01" (h* 'id   "id01"
                   'name "Alice"
                   'age   41)
        "id02" (h* 'id   "id02"
                   'name "Bob"
                   'age   30)
        "id03" (h* 'id   "id03"
                   'name "Emily"
                   'age   21))

(let ((tbl2d (h-2d-new '("Alice" "Bob")
                       '("name" "Nakayama" "Smith"))))
  (h-2d-hmap! (lambda (key   _   _) key)
              (lambda (_ field   _) field)
              (lambda (key field _)
                (downcase
                 (format "%s-%s"
                         key field)))
              tbl2d)
  (h->orgtbl tbl2d))
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | alice-nakayama | alice-smith |
     | Bob   | bob-nakayama   | bob-smith   |"

(let ((tbl2d (h-2d-new '(10 21 42)
                       '("n" 2 3 4))))
  (h-2d-hmap! (lambda (key   _   _) key)
              (lambda (_ field   _) field)
              (lambda (key field _)
                (expt key field))
              tbl2d)
  (h->orgtbl tbl2d))
O=> "|  n |    2 |     3 |       4 |
     |----+------+-------+---------|
     | 10 |  100 |  1000 |   10000 |
     | 21 |  441 |  9261 |  194481 |
     | 42 | 1764 | 74088 | 3111696 |"

;;;; Doesn't always need to be a lambda: variadic functions work too
(let ((tbl2d (h-2d-new '("Alice" "Bob")
                       '("name" "Nakayama" "Smith"))))
  (h-2d-hmap! (lambda (key _   _)   key)       ;
              (lambda (_ field _) field)       ;|
              #'concat ;; initial value=nil^, so (concat k f nil)
              tbl2d)
  (h->orgtbl tbl2d))
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | AliceNakayama  | AliceSmith  |
     | Bob   | BobNakayama    | BobSmith    |"
h--2d-hmap! (key-form fie-form val-form table2d)

Anaphoric version of h-2d-hmap!.

For every key–value pair in TABLE2D, evaluate KEY-FORM, FIE-FORM,
VAL-FORM with the variables key, field, and value bound.

(let ((messy-tbl2d (h* "id01" (h* 'id    "id01"
                                  "name" :alice
                                  "age"  '\41)
                       'id02  (h* 'id    'id02
                                  "name" 'BOB
                                  "age"  :30)
                       "id03" (h* 'id    "id03"
                                  "name" "EmIlY"
                                  "age"  "21"))))
  (h--2d-hmap! (h-as-keyword  key)
               (h-as-symbol field)
               (if (equal field "age")
                   (h-as-number value)
                 (-> value h-as-string capitalize intern))
               messy-tbl2d)
  (h->lol messy-tbl2d))
L=> '((id     name   age)
      (:id01  Alice   41)
      (:id02  Bob     30)
      (:id03  Emily   21))

(let ((tbl2d (h* "id01" (h* "id"   "id01"
                            "name" "alice"
                            "age"  "41")
                 "id02" (h* "id"   "id02"
                            "name" "bob"
                            "age"  "30")
                 "id03" (h* "id"   "id03"
                            "name" "emily"
                            "age"  "21"))))
  (h--2d-hmap! key (intern field) (pcase field
                                    ;; Don't add an "id" pcase like here:
                                    ;; it'll be ignored. The id column (here
                                    ;; labeled "id") will always return key.
                                    ("id"   :will-be-ignored)
                                    ("name" (capitalize value))
                                    ("age"  (read value)))
               tbl2d)
  tbl2d)
H=> (h* "id01" (h* 'id   "id01"
                   'name "Alice"
                   'age   41)
        "id02" (h* 'id   "id02"
                   'name "Bob"
                   'age   30)
        "id03" (h* 'id   "id03"
                   'name "Emily"
                   'age   21))

(let ((tbl2d (h-2d-new '("Alice" "Bob")
                       '("name" "Nakayama" "Smith"))))
  (h--2d-hmap! key field (downcase
                          (format "%s-%s"
                                  key field))
               tbl2d)
  (h->orgtbl tbl2d))
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | alice-nakayama | alice-smith |
     | Bob   | bob-nakayama   | bob-smith   |"

(let ((tbl2d (h-2d-new '("Alice" "Bob")
                       '("name" "Nakayama" "Smith"))))
  (h--2d-hmap! key field (concat key field) tbl2d)
  (h->orgtbl tbl2d))
O=> "| name  | Nakayama       | Smith       |
     |-------+----------------+-------------|
     | Alice | AliceNakayama  | AliceSmith  |
     | Bob   | BobNakayama    | BobSmith    |"

(let ((tbl2d (h-2d-new '(10 21 42)
                       '("n" 2 3 4))))
  (h--2d-hmap! key field (expt key field) tbl2d)
  (h->orgtbl tbl2d))
O=> "|  n |    2 |     3 |       4 |
     |----+------+-------+---------|
     | 10 |  100 |  1000 |   10000 |
     | 21 |  441 |  9261 |  194481 |
     | 42 | 1764 | 74088 | 3111696 |"
Selection or rejection of columns

Functions for selecting or rejecting columns from 2D hash tables.

h-2d-sel-cols (table2d cols)

Return a table with only selected named columns COLS of TABLE2D.

Any column of COLS that isn't in TABLE2D is ignored.

The first column (IDCOL), containing the keys, is always implicitly
selected: otherwise non-repeatability of keys wouldn't be
guaranteed.

This function doesn't modify TABLE2D. Its destructive counterpart
is h-2d-sel-cols!.

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols htbl '("name" "age")))
H=>   (h* "id01" (h* "id"   "id01"
                     "name" "Alice"
                     "age"  "42")
          "id02" (h* "id"   "id02"
                     "name" "Bob"
                     "age"  "30")
          "id03" (h* "id"   "id03"
                     "name" "Emily"
                     "age"  "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols htbl '("editor")))
H=> (h* "id01" (h* "id" "id01"  "editor" "Emacs")
        "id02" (h* "id" "id02"  "editor" "Vim")
        "id03" (h* "id" "id03"  "editor" "Nano"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols htbl "editor"))
H=> (h* "id01" (h* "id" "id01"  "editor" "Emacs")
        "id02" (h* "id" "id02"  "editor" "Vim")
        "id03" (h* "id" "id03"  "editor" "Nano"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols htbl nil))
H=> (h* "id01" (h* "id" "id01")
        "id02" (h* "id" "id02")
        "id03" (h* "id" "id03"))

(let* ((htbl (h-new)))
  (h-2d-sel-cols htbl '("name")))
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-sel-cols htbl nil))
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-sel-cols htbl "name"))
H=> (h-new)
h-2d-sel-cols! (table2d cols)

Remove all but named columns COLS from 2D hash table TABLE2D.

The first column (IDCOL), containing the keys, is always implicitly
selected: otherwise non-repeatability of keys wouldn't be
guaranteed.

This function modifies TABLE2D. Its non-destructive counterpart
is h-2d-sel-cols.

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols! htbl '("name" "age"))
  htbl)
H=>   (h* "id01" (h* "id"   "id01"
                     "name" "Alice"
                     "age"  "42")
          "id02" (h* "id"   "id02"
                     "name" "Bob"
                     "age"  "30")
          "id03" (h* "id"   "id03"
                     "name" "Emily"
                     "age"  "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols! htbl '("editor"))
  htbl)
H=> (h* "id01" (h* "id" "id01"  "editor" "Emacs")
        "id02" (h* "id" "id02"  "editor" "Vim")
        "id03" (h* "id" "id03"  "editor" "Nano"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols! htbl "editor")
  htbl)
H=> (h* "id01" (h* "id" "id01"  "editor" "Emacs")
        "id02" (h* "id" "id02"  "editor" "Vim")
        "id03" (h* "id" "id03"  "editor" "Nano"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-sel-cols! htbl nil)
  htbl)
H=> (h* "id01" (h* "id" "id01")
        "id02" (h* "id" "id02")
        "id03" (h* "id" "id03"))

(let* ((htbl (h-new)))
  (h-2d-sel-cols! htbl '("name"))
  htbl)
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-sel-cols! htbl nil)
  htbl)
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-sel-cols! htbl "name")
  htbl)
H=> (h-new)
h-2d-rej-cols (table2d cols)

Return a table without the named columns COLS of TABLE2D.

Any column of COLS that isn't in TABLE2D is ignored.

The first column (IDCOL), containing the keys, is never rejected:
otherwise non-repeatability of keys wouldn't be guaranteed.

This function doesn't modify TABLE2D. Its destructive counterpart
is h-2d-rej-cols!.

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols htbl '("name" "age")))
H=> (h* "id01" (h* "id" "id01"  "editor" "Emacs")
        "id02" (h* "id" "id02"  "editor" "Vim")
        "id03" (h* "id" "id03"  "editor" "Nano"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols htbl '("editor")))
H=> (h* "id01" (h* "id"     "id01"
                   "name"   "Alice"
                   "age"    "42")
        "id02" (h* "id"     "id02"
                   "name"   "Bob"
                   "age"    "30")
        "id03" (h* "id"     "id03"
                   "name"   "Emily"
                   "age"    "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols htbl "editor"))
H=> (h* "id01" (h* "id"     "id01"
                   "name"   "Alice"
                   "age"    "42")
        "id02" (h* "id"     "id02"
                   "name"   "Bob"
                   "age"    "30")
        "id03" (h* "id"     "id03"
                   "name"   "Emily"
                   "age"    "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols htbl nil))
H=> (h* "id01" (h* "id"     "id01"
                   "editor" "Emacs"
                   "name"   "Alice"
                   "age"    "42")
        "id02" (h* "id"     "id02"
                   "editor" "Vim"
                   "name"   "Bob"
                   "age"    "30")
        "id03" (h* "id"     "id03"
                   "editor" "Nano"
                   "name"   "Emily"
                   "age"    "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols htbl '("id" "editor" "name" "age")))
H=> (h* "id01" (h* "id" "id01")
        "id02" (h* "id" "id02")
        "id03" (h* "id" "id03"))

(let* ((htbl (h-new)))
  (h-2d-rej-cols htbl '("name")))
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-rej-cols htbl nil))
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-rej-cols htbl "name"))
H=> (h-new)
h-2d-rej-cols! (table2d cols)

Remove named columns COLS from 2D hash table TABLE2D.

Any column of COLS that isn't in TABLE2D is ignored.

The first column (IDCOL), containing the keys, is never rejected:
otherwise non-repeatability of keys wouldn't be guaranteed.

This function modifies TABLE2D. Its non-destructive counterpart
is h-2d-rej-cols.

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols! htbl '("name" "age"))
  htbl)
H=> (h* "id01" (h* "id" "id01"  "editor" "Emacs")
        "id02" (h* "id" "id02"  "editor" "Vim")
        "id03" (h* "id" "id03"  "editor" "Nano"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols! htbl '("editor"))
  htbl)
H=> (h* "id01" (h* "id"     "id01"
                   "name"   "Alice"
                   "age"    "42")
        "id02" (h* "id"     "id02"
                   "name"   "Bob"
                   "age"    "30")
        "id03" (h* "id"     "id03"
                   "name"   "Emily"
                   "age"    "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols! htbl "editor")
  htbl)
H=> (h* "id01" (h* "id"     "id01"
                   "name"   "Alice"
                   "age"    "42")
        "id02" (h* "id"     "id02"
                   "name"   "Bob"
                   "age"    "30")
        "id03" (h* "id"     "id03"
                   "name"   "Emily"
                   "age"    "21"))

(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols! htbl nil)
  htbl)
H=> (h* "id01" (h* "id"     "id01"
                   "editor" "Emacs"
                   "name"   "Alice"
                   "age"    "42")
        "id02" (h* "id"     "id02"
                   "editor" "Vim"
                   "name"   "Bob"
                   "age"    "30")
        "id03" (h* "id"     "id03"
                   "editor" "Nano"
                   "name"   "Emily"
                   "age"    "21"))


(let* ((htbl (h* "id01" (h* "id"     "id01"
                            "editor" "Emacs"
                            "name"   "Alice"
                            "age"    "42")
                 "id02" (h* "id"     "id02"
                            "editor" "Vim"
                            "name"   "Bob"
                            "age"    "30")
                 "id03" (h* "id"     "id03"
                            "editor" "Nano"
                            "name"   "Emily"
                            "age"    "21"))))
  (h-2d-rej-cols! htbl '("id" "editor" "name" "age"))
  htbl)
H=> (h* "id01" (h* "id" "id01")
        "id02" (h* "id" "id02")
        "id03" (h* "id" "id03"))

(let* ((htbl (h-new)))
  (h-2d-rej-cols! htbl '("name"))
  htbl)
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-rej-cols! htbl nil)
  htbl)
H=> (h-new)

(let* ((htbl (h-new)))
  (h-2d-rej-cols! htbl "name")
  htbl)
H=> (h-new)
Transposition

Functions for transposing 2D hash tables.

h-2d-transpose (table2d)

Return a hash table that is TABLE2D transposed.

(h-2d-transpose
 (h* "Alice" (h* "editor" "Emacs"
                 "name"   "Alice"
                 "age"    42)
     "Bob"   (h* "editor" "Vim"
                 "name"   "Bob"
                 "age"    30)
     "Emily" (h* "editor" "Nano"
                 "name"   "Emily"
                 "age"    21)))
H=> (h* "editor" (h* "name"  "editor"
                     "Alice" "Emacs"
                     "Bob"   "Vim"
                     "Emily" "Nano")
        "age"    (h* "name"  "age"
                     "Alice" 42
                     "Bob"   30
                     "Emily" 21))

(h-2d-transpose
 (h* "editor" (h* "name"  "editor"
                  "Alice" "Emacs"
                  "Bob"   "Vim"
                  "Emily" "Nano")
     "age"    (h* "name"  "age"
                  "Alice" 42
                  "Bob"   30
                  "Emily" 21)))
H=> (h* "Alice" (h* "editor" "Emacs"
                    "name"   "Alice"
                    "age"    42)
        "Bob"   (h* "editor" "Vim"
                    "name"   "Bob"
                    "age"    30)
        "Emily" (h* "editor" "Nano"
                    "name"   "Emily"
                    "age"    21))

(-> "| name  | editor | age |
     |-------+--------+-----|
     | Alice | Emacs  |  42 |
     | Bob   | Vim    |  30 |
     | Emily | Nano   |  21 |"
    h<-orgtbl  h-2d-transpose  h->orgtbl)
O=> "| name   | Alice | Bob | Emily |
     |--------+-------+-----+-------|
     | editor | Emacs | Vim | Nano  |
     | age    | 42    | 30  | 21    |"

(h-2d-transpose (h-new)) H=> (h-new)
h-2d-transpose! (table2d)

Transpose TABLE2D.

(let ((htbl (h* "Alice" (h* "editor" "Emacs"
                            "name"   "Alice"
                            "age"    42)
                "Bob"   (h* "editor" "Vim"
                            "name"   "Bob"
                            "age"    30)
                "Emily" (h* "editor" "Nano"
                            "name"   "Emily"
                            "age"    21))))
  (h-2d-transpose! htbl)
  htbl)
H=> (h* "editor" (h* "name"  "editor"
                     "Alice" "Emacs"
                     "Bob"   "Vim"
                     "Emily" "Nano")
        "age"    (h* "name"  "age"
                     "Alice" 42
                     "Bob"   30
                     "Emily" 21))

(let ((htbl (h* "editor" (h* "name"  "editor"
                             "Alice" "Emacs"
                             "Bob"   "Vim"
                             "Emily" "Nano")
                "age"    (h* "name"  "age"
                             "Alice" 42
                             "Bob"   30
                             "Emily" 21))))
  (h-2d-transpose! htbl)
  htbl)
H=> (h* "Alice" (h* "editor" "Emacs"
                    "name"   "Alice"
                    "age"    42)
        "Bob"   (h* "editor" "Vim"
                    "name"   "Bob"
                    "age"    30)
        "Emily" (h* "editor" "Nano"
                    "name"   "Emily"
                    "age"    21))

(let ((htbl (h-new)))
  (h-2d-transpose! htbl)
  htbl)
H=> (h-new)
Hash tables as sets

Functions for creating and manipulating hash tables as sets.
Values are all set to t, and all that matters is the presence of keys.

Note: once created, hash-tables-as-sets can be manipulated with the
same functions for regular ones, including union (h-mix),
difference (h-dif), and intersection (h-cmn) and their
destructive counterparts.

h-as-set<-vector (vector &optional size test)

Create a hash table as set from VECTOR.

The KEYS will be the elements of the vector, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed.

(h-as-set<-vector [:a :e :a :a :r :v :a :v :w :e :a])
H=> (h* :a t :e t :r t :v t :w t)
(h-as-set<-vector [])        H=> (h-new)
(h-as-set<-vector '(:a :e))  !!> error
h-as-set<-list (list &optional size test)

Create a hash table as set from LIST.

The KEYS will be the elements of the list, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed.

(h-as-set<-list '(:a :e :a :a :r :v :a :v :w :e :a))
H=> (h* :a t :e t :r t :v t :w t)

;;;; Adding to the set: use h-mix, either binding and destructively...
(let ((htbl (h-new)))
  (h-mix! htbl
          (h-as-set<-list '(:a :e :a :a :r :v :a :v :w :e :a)))
  (h-mix! htbl
          (h-as-set<-vector [:x :y :z]))
  htbl)
H=> (h* :a t :e t :r t :v t :w t :x t :y t :z t)

;;;; ...or thread it non-destructively
(-> '(:a :e :a :a :r :v :a :v :w :e :a)
    h-as-set<-list
    (h-mix (h-as-set<-vector [:x :y :z]))
    (h-mix (h-as-set<-list '(5 42))))
H=> (h* :a t :e t :r t :v t :w t :x t :y t :z t 5 t 42 t)

;;;; empty, error
(h-as-set<-list '())      H=> (h-new)
(h-as-set<-list [:a :e])  !!> error
h-as-set<-lines (str &optional size test)

Create a hash table as set from the lines of string STR.

The KEYS will be the lines, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed.

(h-as-set<-lines "These here\nare 5\nlines\nand not 2\nlines")
H=> (h* "These here" t
        "are 5"      t
        "lines"      t
        "and not 2"  t)
(h-as-set<-lines "")  H=> (h* "" t)
(h-as-set<-lines nil) !!> error
h-as-set<-words (str &optional size test)

Create a hash table as set from the words of string STR.

The KEYS will be the words, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed.

(h-as-set<-words "These are words, and words,\nand more words.")
H=> (h* "These" t
        "are"   t
        "words" t
        "and"   t
        "more"  t)
(h-as-set<-words "")  H=> (h-new)
(h-as-set<-words nil) !!> error
Hash tables for counting

Functions for using hash tables for counting.

h-count<-vector (vector &optional size test)

Create a hash table with the elements of VECTOR and their count.

Optional arguments SIZE and TEST may be passed.

(h-count<-vector [:a :e :a :a :r :v :a :v :w :e :a])
H=> (h* :a 5 :e 2 :r 1 :v 2 :w 1)

(h-count<-vector ["Emacs" "Nano" "Emacs" "Emacs" "Nano"])
H=> (h* "Emacs" 3 "Nano" 2)

(h-count<-vector [])        H=> (h-new)
(h-count<-vector '(:a :e))  !!> error
h-count<-list (list &optional size test)

Create a hash table with the elements of LIST and their count.

Optional arguments SIZE and TEST may be passed.

(h-count<-list '(:a :e :a :a :r :v :a :v :w :e :a))
H=> (h* :a 5 :e 2 :r 1 :v 2 :w 1)

(h-count<-list '("Emacs" "Nano" "Emacs" "Emacs" "Nano"))
H=> (h* "Emacs" 3 "Nano" 2)

(h-count<-list '())      H=> (h-new)
(h-count<-list [:a :e])  !!> error
h-count<-lines (str &optional size test)

Create a hash table with the lines of string STR and their count.

Optional arguments SIZE and TEST may be passed.

(h-count<-lines "These here\nare 5\nlines\nand not 2\nlines")
H=> (h* "These here" 1
        "are 5"      1
        "lines"      2
        "and not 2"  1)
(h-count<-lines "")  H=> (h* "" 1)
(h-count<-lines nil) !!> error
h-count<-words (str &optional size test)

Create a hash table with the words of string STR and their count.

Optional arguments SIZE and TEST may be passed.

(h-count<-words "These are words, and words,\nand more words.")
H=> (h* "These" 1
        "are"   1
        "words" 3
        "and"   2
        "more"  1)
(h-count<-words "")  H=> (h-new)
(h-count<-words nil) !!> error
h-count-put (table &rest objs)

Create a hash table with objects OBJS with the counts as values.

Each OBJ might be:

  • a hash table with counts for values: will be added to the count
    of TABLE.
  • a vector: each element will add 1 to the count of TABLE.
  • a list: each element will add 1 to the count of TABLE.
  • anything else: added as element: if new, with count 1, or
    otherwise added to the count of TABLE.

This function is free of side-effects. Its destructive counterpart
is h-count-put!.

;;;; It autodetects the input type: hash table, list, or vector
(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put htbl (h* "Emacs" 4 "Vim" 1 "Nano" 1)))
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put htbl '("Emacs" "Vim" "Nano"
                      "Emacs" "Emacs" "Emacs")))
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put htbl ["Emacs" "Vim" "Nano"
                     "Emacs" "Emacs" "Emacs"]))
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

;;;; It can read different number-like formats in the target and source.
;;;;; Matches the target format
(let ((htbl (h* "Emacs" "3" "Nano" "1")))
  (h-count-put htbl '("Emacs" "Nano"
                      "Emacs" "Emacs" "Emacs")))
H=> (h* "Emacs" "7"
        "Nano"  "2")


(let ((htbl (h* "Emacs" 3 "Nano" "1")))
  (h-count-put htbl ["Emacs" "Nano"
                     "Emacs" "Emacs" "Emacs"]))
H=> (h* "Emacs" 7
        "Nano"  "2")

;;;;; Note that new counts default to regular number
(let ((htbl (h* "Emacs" "3" "Nano" "1")))
  (h-count-put htbl (h* "Emacs" :4 "Vim" '1 "Pico" "2" "Nano" 1)))
H=> (h* "Emacs" "7"
        "Nano"  "2"
        "Vim"    1  ;<-- as number because the count was nil
        "Pico"   2) ;<-- as number because the count was nil

;;;; For convertibles-to-ht, convert it yourself:
(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (->> '(("Emacs" . 4) ("Vim" . 1) ("Nano" . 1))
       h<-alist*
       (h-count-put htbl)))
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

;;;; Other types are added as they are
(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put htbl "Emacs"))
H=> (h* "Emacs" 4
        "Nano"  1)

(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put htbl 42))
H=> (h* "Emacs" 3
        "Nano"  1
        42      1)
h-count-put! (table &rest objs)

Add counts of objects OBJS to the counts of hash table TABLE.

This function modifies TABLE. Its side-effects–free counterpart is
h-count-put.

;;;; It autodetects the input type: hash table, list, or vector
(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put! htbl (h* "Emacs" 4 "Vim" 1 "Nano" 1))
  htbl)
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put! htbl '("Emacs" "Vim" "Nano"
                       "Emacs" "Emacs" "Emacs"))
  htbl)
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put! htbl ["Emacs" "Vim" "Nano"
                      "Emacs" "Emacs" "Emacs"])
  htbl)
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

;;;; It can read different number-like formats in the target and source.
;;;;; Matches the target format
(let ((htbl (h* "Emacs" "3" "Nano" "1")))
  (h-count-put! htbl '("Emacs" "Nano"
                       "Emacs" "Emacs" "Emacs"))
  htbl)
H=> (h* "Emacs" "7"
        "Nano"  "2")

(let ((htbl (h* "Emacs" 3 "Nano" "1")))
  (h-count-put! htbl ["Emacs" "Nano"
                      "Emacs" "Emacs" "Emacs"])
  htbl)
H=> (h* "Emacs" 7
        "Nano"  "2")

;;;;; Note that new counts default to regular number
(let ((htbl (h* "Emacs" "3" "Nano" "1")))
  (h-count-put! htbl (h* "Emacs" :4 "Vim" '1 "Pico" "2" "Nano" 1))
  htbl)
H=> (h* "Emacs" "7"
        "Nano"  "2"
        "Vim"    1  ;<-- as number because the count was nil
        "Pico"   2) ;<-- as number because the count was nil

;;;; For convertibles-to-ht, convert it yourself:
(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (->> '(("Emacs" . 4) ("Vim" . 1) ("Nano" . 1))
       h<-alist*
       (h-count-put! htbl))
  htbl)
H=> (h* "Emacs" 7
        "Nano"  2
        "Vim"   1)

;;;; Other types are added as they are
(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put! htbl "Emacs")
  htbl)
H=> (h* "Emacs" 4
        "Nano"  1)

(let ((htbl (h* "Emacs" 3 "Nano" 1)))
  (h-count-put! htbl 42)
  htbl)
H=> (h* "Emacs" 3
        "Nano"  1
        42      1)

Miscellaneous

Additional examples

Some additional examples that may be useful and don't fit any
particular function.

xht--misc-examples ()

This function does nothing.

It's just a hack to add to examples.el (and also to the README) a
few miscellaneous tests and examples that aren't attached to any
particular function.

;;;; Importing DSVs: how to deal with other delimiters?
;;;;; Easiest way is to replace to TAB and import as TSV. Example:

(let ((colon-sv "id:name:age\n1:alice:42\n2:bob:30"))
  (->> colon-sv
       (s-replace ":" "\t")
       h<-tsv))
H=>  (h* "1" (h* "id"   "1"
                 "name" "alice"
                 "age"  "42")
         "2" (h* "id"   "2"
                 "name" "bob"
                 "age"  "30"))

;;;; When doing many operations in a table, Dash's -doto can be handy.
;;;;; It avoids repetitions…
(let ((my-small-table (h* :a 1 :b 2)))
  (-doto my-small-table
    (h-put! :c 3)
    (h-put! :d 4)
    (h-rem! :b)
    (h-put! :e (h* :f 6 :g 7))))
H=> (let ((my-small-table (h* :a 1 :b 2)))
      (h-put! my-small-table :c 3)
      (h-put! my-small-table :d 4)
      (h-rem! my-small-table :b)
      (h-put! my-small-table :e (h* :f 6 :g 7))
      my-small-table)

;;;;; …or the creation of new hash table copies at every step
(let ((my-small-table (h* :a 1 :b 2)))
  (-doto my-small-table
    (h-put! :c 3)
    (h-put! :d 4)
    (h-rem! :b)
    (h-put! :e (h* :f 6 :g 7))))
H=> (-> (h* :a 1 :b 2)
        (h-put :c 3)
        (h-put :d 4)
        (h-rem :b)
        (h-put :e (h* :f 6 :g 7)))

Commands

XHT dispatcher

With the dispatcher you can interactively convert, display, insert, or
write to disk whatever looks like a hash table or could be converted
from/into one. There're also some specialized functions that offer you
"speed dial" access to the dispatcher options.

Main dispatcher
xht-do (&optional ht-opts)

The main command for you to do all sorts of hash table things.

This is XHT's main dispatcher.

  • Launch it to convert, display, insert, write to disk.
  • It reads and write from (almost) everything that can be
    transformed from or into a hash table.
  • Input can be the last sexp, some thing at point, region
    selection, a file, your clipboard, or the whole buffer you're in.

Try it.

You can also make your own function to call it non-interactively.
You must then provide HT-OPTS, a tiny hash table with options to
bypass the input of any specific questions. This hash table may
have any number of the up to six variables holding answers to the
questions asked, namely:

'(do-what  where-from  where-to  what-to  format-to   file-to)

For example, if you frequently convert the last sexp to hash table,
or want to visualize the hash table before point as (h*…), and then
have this inserted at point. In fact, this one should be
particularly frequent, so we already have a function for it:
xht-do-h*-ify-last-sexp-to-point, which is essentially this:

(xht-do (h* :do-what    ?c
            :where-from ?x
            :where-to   ?i
            :what-to    ?h
            :format-to  ?1
            :file-to    nil))

That is: [c]onvert, from last se[x]p, [i]nsert at point, to [h]ash
table, format #[1] (which is pretty-printed h* form).

Try that command when point is after a #s(hash-table…) or alist.

With that in mind you can create a similar one adapted to your
needs. You can then key-bind it for quick access.

See also these related specialized commands:

Specialized quicker access

These commands pass pre-defined choices to the main dispatcher so you can
more quickly do the things you do more often without having to answer all of
the main dispatcher's questions.

xht-do-convert-dwim ()

Convert region, thing at point or last-sexp — try to guess it.

  • If you have a region selected, use that.
  • Otherwise, if you're inside a string, a vector, some sort of
    list, or #s(hash-table…), use that.
  • Otherwise, use last sexp

Then interactively select where to, what to, and format.

See also xht-do for other specialized commands listed there.

xht-do-h*-ify-dwim-to-clipboard ()

First h*-ify region, thing at point or last-sexp; send it to clipboard.

Like xht-do-h*-ify-dwim-to-point, but to clipboard instead of at point.

xht-do-h*-ify-dwim-to-file ()

First h*-ify region, thing at point or last-sexp; write to file.

Like xht-do-h*-ify-dwim-to-point, but to new file instead of at point.

xht-do-h*-ify-dwim-to-replace ()

First h*-ify region, thing at point or last-sexp; replace source.

Like xht-do-h*-ify-dwim-to-point, but replace source instead of
inserting at point.

xht-do-h*-ify-dwim-to-new-buffer ()

First h*-ify region, thing at point or last-sexp; insert at new buffer.

Like xht-do-h*-ify-dwim-to-point, but to new buffer instead of at point.

xht-do-h*-ify-dwim-to-point ()

First h*-ify region, thing at point or last-sexp; insert at point.

Equivalent to calling xht-do and then answering
with ?c N ?i ?h 1 — where N represents the following do-what-I-mean
autodetection:

  • If you have a region selected, ?r
  • Otherwise, if you're inside a string, a vector, some sort of
    list, or #s(hash-table…), ?t
  • Otherwise, ?x

See also xht-do for other specialized commands listed there.

xht-do-h*-ify-selected-region-to-point ()

First h*-ify selected region; then insert results at point.

Equivalent to calling xht-do and then answering
with ?c ?r ?i ?h 1.

See also xht-do for other specialized commands listed there.

xht-do-h*-ify-thing-at-point-to-point ()

First h*-ify thing at point; then insert results at point.

Equivalent to calling xht-do and then answering
with ?c ?t ?i ?h 1.

See also xht-do for other specialized commands listed there.

xht-do-h*-ify-last-sexp-to-point ()

First h*-ify last-sexp; then insert results at point.

Equivalent to calling xht-do and then answering
with ?c ?x ?i ?h 1.

See also xht-do for other specialized commands listed there.

Minor modes

xht-do minor mode

A minor mode for turning on keys for xht-do actions.

xht-do-mode (&optional arg)

Enable keys for xht-do actions.

See also xht-do-mode-lighter and global-xht-do-mode.

global-xht-do-mode (&optional arg)

Toggle Xht-Do mode in all buffers.

With prefix ARG, enable Global Xht-Do mode if ARG is positive;
otherwise, disable it. If called from Lisp, enable the mode if
ARG is omitted or nil.

Xht-Do mode is enabled in all buffers where
xht--turn-on-do-mode would do it.
See xht-do-mode for more information on Xht-Do mode.

Font lock minor mode

A minor mode for fontifying XHT's functions and equality operators.

xht-fontify-mode (&optional arg)

Toggle fontification of XHT/HT special variables.

This is a buffer-local minor mode intended for Emacs Lisp buffers.
Enabling it causes syntax highlighting of:

  • xht's callables (functions, macros, aliases)
  • ht's callables (functions, macros, aliases)
  • anaphoric variables such as key and value.
  • exemplify-ert macros and equality operators
    (for testing and examples — see ./dev folder)

They are all turned on when global-xht-fontify-mode is on.

If you wish, you can selectively disable any of them, through the
customize interface: M-x customize-group RET xht RET. See there
the variables starting with 'xht-fontlock-add-'.

See also xht-fontify-mode-lighter and global-xht-fontify-mode.

global-xht-fontify-mode (&optional arg)

Toggle Xht-Fontify mode in all buffers.

With prefix ARG, enable Global Xht-Fontify mode if ARG is positive;
otherwise, disable it. If called from Lisp, enable the mode if
ARG is omitted or nil.

Xht-Fontify mode is enabled in all buffers where
xht--turn-on-fontify-mode would do it.
See xht-fontify-mode for more information on Xht-Fontify mode.

See README

Commands to open xht's README.org. Optionally, find things in it.

xht-see-readme (&optional heading narrow)

Open xht's README.org file.

Search for the file in xht.el's directory.

If found, open it read-only.

If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string. If prefixed
by "cid:", search it by CUSTOMID.

If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found.

xht-see-function-in-readme (&optional function)

See FUNCTION in xht's README.org.

When called interactively, select function from minibuffer.

xht-see-news ()

See the News in xht's README.org file.

Appendix

Have you already had a look at the additional examples above? Great!

Here's yet some extra material.

Still more examples of usage

Obtaining hash table values by selecting the key through completing read (from minibuffer)

Try it!

When keys are strings:

(let ((htbl (h* "car" 3 "dice" 4 "elephant" 8)))
  (h-get htbl
         (completing-read "Which key? " htbl)))          ;<-- C-x C-e

When keys are symbols:

(let ((htbl (h* 'car 3 'dice 4 'elephant 8)))
  (h-get htbl
         (read (completing-read "Which key? " htbl))))   ;<-- C-x C-e
Converting a headerless list-of-lists to hash table

Whenever you see lol in th name of an xht function, it's meant to be a list-of-lists with a header. These are of dimension 2 and are easy to convert:

(h<-lol '((label count day)
          (:a    10    "Mon")
          (:b    20    "Tue")
          (:c    30    "Wed")))

H=> (h* :a (h* 'label :a
               'count 10
               'day "Mon")
        :b (h* 'label :b
               'count 20
               'day "Tue")
        :c (h* 'label :c
               'count 30
               'day "Wed"))

If these come without a header, you can put one yourself before converting:

(->> '((:a    10    "Mon")
       (:b    20    "Tue")
       (:c    30    "Wed"))
     (cons '(label count day))
     h<-lol)

H=> (h* :a (h* 'label :a
               'count 10
               'day "Mon")
        :b (h* 'label :b
               'count 20
               'day "Tue")
        :c (h* 'label :c
               'count 30
               'day "Wed"))

These are two-dimensional and can't be further simplified. They're equivalent to an org table with header:

| label | count | day |
|-------+-------+-----|
| a     |    10 | Mon |
| b     |    20 | Tue |
| c     |    30 | Wed |

But sometimes you meet those that have only two "columns":

(h<-lol '((label day)
          (:a   "Mon")
          (:b   "Tue")
          (:c   "Wed")))

H=> (h* :a (h* 'label :a
               'day "Mon")
        :b (h* 'label :b
               'day "Tue")
        :c (h* 'label :c
               'day "Wed"))

These can be reduced to 1D:

(h-1d<-2d (h<-lol '((label day)
                    (:a   "Mon")
                    (:b   "Tue")
                    (:c   "Wed"))))
H=> (h* :a "Mon"
        :b "Tue"
        :c "Wed")

;; same as
(h-1d<-2d (h* :a (h* 'label :a
                     'day "Mon")
              :b (h* 'label :b
                     'day "Tue")
              :c (h* 'label :c
                     'day "Wed")))
H=> (h* :a "Mon"
        :b "Tue"
        :c "Wed")

Ok. But sometimes we meet these without a header, in which case they're not a lol as xht defines it. We can't convert it directly or the first value will be presumed to be a header, which is not what we want:

(h-1d<-2d (h<-lol '((:a "Mon")
                    (:b "Tue")
                    (:c "Wed"))))
H=> (h* :b "Tue"
        :c "Wed")

What to do? Here are two options:

Option 1: insert a dummy header yourself, to be removed when converting to 1D

(->> '((:a "Mon")
       (:b "Tue")
       (:c "Wed"))
     (cons '(x y))
     h<-lol
     h-1d<-2d)

H=> (h* :a "Mon"
        :b "Tue"
        :c "Wed")

Option 2: "use ht"

(->> '((:a "Mon")
       (:b "Tue")
       (:c "Wed"))
     (cons 'ht)
     h-lambdify
     funcall)

H=> (h* :a "Mon"
        :b "Tue"
        :c "Wed")

What?!

See, the ht macro accepts as input two-item lists, so these are equivalent:

(ht (:a 1) (:b 2) (:c 3)) H=> (h* :a 1 :b 2 :c 3)

This is very convenient here. We can cons the symbol 'ht to that headerless list-of-lists, then h-lambdify it (which is to say: cons a lambda() to it), then funcall it, and the output is the 1D hash table we wanted.

Now, if we're doing this stuff interactively and just want a hash table expression inserted at point, you don't even need to h-lambdify it. If we stop at the cons, the result is a quoted list:

(->> '((:a "Mon")
       (:b "Tue")
       (:c "Wed"))
     (cons 'ht))
=> '(ht
     (:a "Mon")
     (:b "Tue")
     (:c "Wed"))

Thankfully, this quoted list is recognized by xht-do as what it seems to be, so you can just:

(->> '((:a "Mon")
       (:b "Tue")
       (:c "Wed"))
     (cons 'ht)) ;<-- C-x x x  here

to have the h* expression inserted as indicated below:

(->> '((:a "Mon")
       (:b "Tue")
       (:c "Wed"))
     (cons 'ht))

(h* :a "Mon"
    :b "Tue"
    :c "Wed")

(which, by the way, is exactly how I have inserted it above).

Above I assume that you have the handy xht-do-h*-ify-dwim-to-point bound to C-x x x as I do.

So you want to destructure some hash tables, huh?

There are a few ways.

All the examples below return the exact same string: "Alice uses Emacs!"

;;; 1D
;;;; Longer

(let* ((htbl  (h* "Alice" "Emacs"
                  "Bob"   "Vim"
                  "Emily" "Nano"))
       (Alice (h-get htbl "Alice")))
  (format "%s uses %s!" "Alice" Alice))


;;;; Simpler: using XHT's h-let

(let ((htbl  (h* "Alice" "Emacs"
                 "Bob"   "Vim"
                 "Emily" "Nano")))
  (h-let htbl
    (format "%s uses %s!" "Alice" .Alice)))

;;;; Or: using s-format
(let ((htbl  (h* "Alice" "Emacs"
                 "Bob"   "Vim"
                 "Emily" "Nano")))
  (s-format "Alice uses ${Alice}!"
            'gethash htbl))

;;;; Or: using Dash's -let
;;;;; Autobinding

(-let* ((htbl (h* "Alice" "Emacs"
                  "Bob"   "Vim"
                  "Emily" "Nano"))
        ((&hash "Alice") htbl))
  (format "%s uses %s!" "Alice" Alice))

;;;;; Binding 'editor

;;;;;; using format
(-let* ((htbl (h* "Alice" "Emacs"
                  "Bob"   "Vim"
                  "Emily" "Nano"))
        ((&hash "Alice" editor) htbl))
  (format "%s uses %s!" "Alice" editor))

;;;;;; using s-lex-format
(-let* ((htbl (h* "Alice" "Emacs"
                  "Bob"   "Vim"
                  "Emily" "Nano"))
        ((&hash "Alice" editor) htbl))
  (s-lex-format "Alice uses ${editor}!"))

;; or
(-let [(&hash "Alice" editor)
       (h* "Alice" "Emacs"
           "Bob"   "Vim"
           "Emily" "Nano")]
  (s-lex-format "Alice uses ${editor}!"))

;; or
(--> (h* "Alice" "Emacs"
         "Bob"   "Vim"
         "Emily" "Nano")
     (-let [(&hash "Alice" editor) it]
       (s-lex-format "Alice uses ${editor}!")))

;; or
(--> (h* "Alice" "Emacs"
         "Bob"   "Vim"
         "Emily" "Nano")
     (-let [editor (h-get it "Alice")]
       (s-lex-format "Alice uses ${editor}!")))

;; or
(--> (h* "Alice" "Emacs"
         "Bob"   "Vim"
         "Emily" "Nano")
     (let ((editor (h-get it "Alice")))
       (s-lex-format "Alice uses ${editor}!")))


;;; 2D
;;;; Longer

(let* ((orgtbl "| id | name  | age | editor |
                |----+-------+-----+--------|
                | 01 | Alice |  42 | Emacs  |
                | 02 | Bob   |  30 | Vim    |
                | 03 | Emily |  21 | Nano   |")
       (htbl   (h<-orgtbl orgtbl))
       (name   (h-get* htbl "01" "name"))
       (editor (h-get* htbl "01" "editor")))
  (format "%s uses %s!" name editor))


;;;; Simpler: using XHT's h-let

(let* ((orgtbl "| id | name  | age | editor |
                |----+-------+-----+--------|
                | 01 | Alice |  42 | Emacs  |
                | 02 | Bob   |  30 | Vim    |
                | 03 | Emily |  21 | Nano   |")
       (htbl   (h<-orgtbl orgtbl)))
  (h-let htbl
    (format "%s uses %s!" .01.name .01.editor)))

;;;; Or: using s-format and h-get

(let* ((orgtbl "| id | name  | age | editor |
                |----+-------+-----+--------|
                | 01 | Alice |  42 | Emacs  |
                | 02 | Bob   |  30 | Vim    |
                | 03 | Emily |  21 | Nano   |")
       (htbl   (h<-orgtbl orgtbl)))
  (s-format "${name} uses ${editor}!"
            'gethash
            (h-get htbl "01")))

;;;; Or: using Dash's -let
;;;;; Binding 'name and 'editor
;;;;;; using format

(-let* ((orgtbl "| id | name  | age | editor |
                 |----+-------+-----+--------|
                 | 01 | Alice |  42 | Emacs  |
                 | 02 | Bob   |  30 | Vim    |
                 | 03 | Emily |  21 | Nano   |")
        (htbl   (h<-orgtbl orgtbl))
        ((&hash "01" (&hash "name" "editor")) htbl))
  (format "%s uses %s!" name editor))

;;;;;; using s-lex-format

(--> "| id | name  | age | editor |
      |----+-------+-----+--------|
      | 01 | Alice |  42 | Emacs  |
      | 02 | Bob   |  30 | Vim    |
      | 03 | Emily |  21 | Nano   |"
     h<-orgtbl
     (-let [(&hash "01" (&hash "name" "editor")) it]
       (s-lex-format "${name} uses ${editor}!")))

(h*…) can be multiple times faster than (ht…) for creating smaller tables

Below I'm using bench and bench-multi from Adam Porter's emacs-package-dev-handbook.
These functions garbage-collect before running.

Compare:

(bench (ht (:a 1) (:b 2)))
Total runtime # of GCs Total GC runtime
7.827e-06 0 0.0
(bench (h* :a 1 :b 2))
Total runtime # of GCs Total GC runtime
1.753e-06 0 0.0

Or together:

(bench-multi :forms
  (("ht" (ht (:a 1) (:b 2)))
   ("h*" (h*  :a 1   :b 2))))
Form x fastest Total runtime # of GCs Total GC runtime
h* fastest 0.000002 0 0
ht 5.19 0.000009 0 0

Why?

Because h* sets the hash table nominal size to its actual size — the number of items it receives (in the case above: 2), whereas ht defaults to Elisp's default of 65.

(h-prop (ht (:a 1) (:b 2)) 'size)  => 65
(h-prop (h*  :a 1   :b 2)  'size)  =>  2

Likewise if you're using nested hash tables. Let's try something: extending an example (alphabet) from ht's README to compare times.

(bench-multi :forms
  (("ht" (ht ("English" (ht (1  (ht ('letter "a") ('name "A")))
                            (2  (ht ('letter "b") ('name "B")))
                            (3  (ht ('letter "c") ('name "C")))
                            (4  (ht ('letter "d") ('name "D")))
                            (5  (ht ('letter "e") ('name "E")))
                            (6  (ht ('letter "f") ('name "F")))
                            (7  (ht ('letter "g") ('name "G")))
                            (8  (ht ('letter "h") ('name "H")))
                            (9  (ht ('letter "i") ('name "I")))
                            (10 (ht ('letter "j") ('name "J")))
                            (11 (ht ('letter "k") ('name "K")))
                            (12 (ht ('letter "l") ('name "L")))
                            (13 (ht ('letter "m") ('name "M")))
                            (14 (ht ('letter "n") ('name "N")))
                            (15 (ht ('letter "o") ('name "O")))
                            (16 (ht ('letter "p") ('name "P")))
                            (17 (ht ('letter "q") ('name "Q")))
                            (18 (ht ('letter "r") ('name "R")))
                            (19 (ht ('letter "s") ('name "S")))
                            (20 (ht ('letter "t") ('name "T")))
                            (21 (ht ('letter "u") ('name "U")))
                            (22 (ht ('letter "v") ('name "V")))
                            (23 (ht ('letter "w") ('name "W")))
                            (24 (ht ('letter "x") ('name "X")))
                            (25 (ht ('letter "y") ('name "Y")))
                            (26 (ht ('letter "z") ('name "Z")))))))

   ("h*" (h*  "English" (h*  1  (h*  'letter "a"   'name "A")
                             2  (h*  'letter "b"   'name "B")
                             3  (h*  'letter "c"   'name "C")
                             4  (h*  'letter "d"   'name "D")
                             5  (h*  'letter "e"   'name "E")
                             6  (h*  'letter "f"   'name "F")
                             7  (h*  'letter "g"   'name "G")
                             8  (h*  'letter "h"   'name "H")
                             9  (h*  'letter "i"   'name "I")
                             10 (h*  'letter "j"   'name "J")
                             11 (h*  'letter "k"   'name "K")
                             12 (h*  'letter "l"   'name "L")
                             13 (h*  'letter "m"   'name "M")
                             14 (h*  'letter "n"   'name "N")
                             15 (h*  'letter "o"   'name "O")
                             16 (h*  'letter "p"   'name "P")
                             17 (h*  'letter "q"   'name "Q")
                             18 (h*  'letter "r"   'name "R")
                             19 (h*  'letter "s"   'name "S")
                             20 (h*  'letter "t"   'name "T")
                             21 (h*  'letter "u"   'name "U")
                             22 (h*  'letter "v"   'name "V")
                             23 (h*  'letter "w"   'name "W")
                             24 (h*  'letter "x"   'name "X")
                             25 (h*  'letter "y"   'name "Y")
                             26 (h*  'letter "z"   'name "Z"))))

   ("plist" (list "English" (list 1  '(letter "a"  name "A")
                                  2  '(letter "b"  name "B")
                                  3  '(letter "c"  name "C")
                                  4  '(letter "d"  name "D")
                                  5  '(letter "e"  name "E")
                                  6  '(letter "f"  name "F")
                                  7  '(letter "g"  name "G")
                                  8  '(letter "h"  name "H")
                                  9  '(letter "i"  name "I")
                                  10 '(letter "j"  name "J")
                                  11 '(letter "k"  name "K")
                                  12 '(letter "l"  name "L")
                                  13 '(letter "m"  name "M")
                                  14 '(letter "n"  name "N")
                                  15 '(letter "o"  name "O")
                                  16 '(letter "p"  name "P")
                                  17 '(letter "q"  name "Q")
                                  18 '(letter "r"  name "R")
                                  19 '(letter "s"  name "S")
                                  20 '(letter "t"  name "T")
                                  21 '(letter "u"  name "U")
                                  22 '(letter "v"  name "V")
                                  23 '(letter "w"  name "W")
                                  24 '(letter "x"  name "X")
                                  25 '(letter "y"  name "Y")
                                  26 '(letter "z"  name "Z"))))

   ("alist" '(("English"
               (1  (letter . "a") (name . "A"))
               (2  (letter . "b") (name . "B"))
               (3  (letter . "c") (name . "C"))
               (4  (letter . "d") (name . "D"))
               (5  (letter . "e") (name . "E"))
               (6  (letter . "f") (name . "F"))
               (7  (letter . "g") (name . "G"))
               (8  (letter . "h") (name . "H"))
               (9  (letter . "i") (name . "I"))
               (10 (letter . "j") (name . "J"))
               (11 (letter . "k") (name . "K"))
               (12 (letter . "l") (name . "L"))
               (13 (letter . "m") (name . "M"))
               (14 (letter . "n") (name . "N"))
               (15 (letter . "o") (name . "O"))
               (16 (letter . "p") (name . "P"))
               (17 (letter . "q") (name . "Q"))
               (18 (letter . "r") (name . "R"))
               (19 (letter . "s") (name . "S"))
               (20 (letter . "t") (name . "T"))
               (21 (letter . "u") (name . "U"))
               (22 (letter . "v") (name . "V"))
               (23 (letter . "w") (name . "W"))
               (24 (letter . "x") (name . "X"))
               (25 (letter . "y") (name . "Y"))
               (26 (letter . "z") (name . "Z")))))))
Form x fastest Total runtime # of GCs Total GC runtime
h* fastest 0.000002 0 0
alist 1.31 0.000002 0 0
plist 4.34 0.000007 0 0
ht 50.37 0.000080 0 0

On the one hand, let's have in mind that we're talking about microseconds here. So this difference is only likely to matter if you're doing this operation repeatedly (say, inside a loop). For one-off creations, you're unlikely to feel much of a difference.

On the other hand, microseconds can add up. Are we going to get into the habit of throwing hash tables at tiny sets of key–value pairs as if there's no tomorrow? As you can't possibly have failed to notice, I'm totally for it. In this case, then, we might as well give 'em alists and plists a good run for their money.

xht for those who already use ht

XHT has by now more than 200 public functions. That's a lot to digest.

If you already use ht and want to start using xht, you may wonder which functions of the latter do what you're used to doing with the former. So below is a summary mapping htxht based on what the ht functions do. This should help you get started.

If you currently use… have a look at…
ht h*, h-s*, h-t*, h-st*
ht-create h-new, h-empty-clone
ht-copy h-clone*, h<-ht
ht-get, ht-get* h-get, h-get* (both are just aliases)
ht-find h-pop, h-pop!, h-first, h--first, h-first!, h--first!
ht-merge h-mix*, h-mix
ht-update! == ht-update h-mix*!, h-mix!
ht-set! == ht-set h-put*!, h-put! (or side-effect-free: h-put*, h-put)
ht-remove! == ht-remove h-rem*!, h-rem! (or side-effect-free: h-rem*, h-rem)
ht-clear! == ht-clear h-clr! (or side-effect-free: h-clr)
ht->alist h->alist*, h->alist
ht<-alist h<-alist*, h<-alist
ht->plist h->plist*, h->plist
ht<-plist h<-plist*, h<-plist
ht-select h-sel!, h--sel!, h-sel*!, h--sel*!
  h-sel, h--sel, h-sel*, h--sel*
ht-reject! h-rej!, h--rej!, h-rej*!, h--rej*!
ht-reject h-rej, h--rej, h-rej*, h--rej*
ht-select-keys h-sel-keys, h-sel-keys!, h-rej-keys, h-rej-keys!
ht-contains? h-has-key?, h-has-key*?, h-has-keys?
ht-equal? h= (and also: h-pr==, h-pr=, h==, h_=, h~=)
ht-map h-lmap, h-lkeep, h-vmap (see also: h-hmap family)
ht-amap h--lmap, h--lkeep, h--vmap (see also: h--hmap family)
ht-each h-each
ht-aeach h--each
ht-keys h-lkeys (alias: h-keys), h-vkeys
ht-values h-lvalues (alias: h-values), h-vvalues
ht-items h-litems (alias: h-items), h-vitems

Mind you that there isn't (always) an exact correspondence — so check the functions' docstrings for the differences and details.

Thank you

I'd particularly like to thank Adam Porter and Wilfred Hughes, whose writings brought Emacs Lisp hash tables to my attention. It's probable that I wouldn't have given it much thought otherwise.

To that I add that ht's existence and my use of it led me to eventually wonder how much farther the idea could still be developed. This in turn led me to actually coding much of what would become the first release of XHT. And my repeated use of his helpful during development was very... helpful!

Thank you Artur Malabarba for let-alist. Studying it gave me the idea for what became h-let, which adapts let-alist's code and works like a charm. And how can anyone not use your aggressive-indent-mode?

Dash's code inspired xht-fontify-mode, which I extended with defcustom options, and thanks to which you can now make all your hash table operations stand out in bright colors if you want to. Dash also influenced my choice of names for functions (such as h-lkeep, h-first, the use of a double dash for anaphoric macros, etc), ideas for functions themselves (ditto), and choice of argument order (such as with h--each).

Contributing

See my page Software for information about how to contribute to any of my Emacs packages.

Version 1.0 of this package was released with more than 1800 tests/examples; I wrote them myself, and tried to be fairly comprehensive, but coverage might still be missing here and there. So you can also help by suggesting more examples for examples.el. These can be those that illustrate well some usage, or those that cover some combination of arguments (or odd inputs) not yet covered there.

News

2.0.0 (unreleased — not yet tagged)

XHT Breaking News

This release brings new functions, new variables, and more examples.

It also brings some potentially breaking changes in some KVL-related functions.

KVLs are now better handled in both detection and conversion, as detailed below. Moving the optional non-default delimiter from a positional argument to a let-bindable variable made its conversion signature the same of all other convertible types. This made possible the elimination of some exceptional handling that KVLs demanded in a few places, which in turn made the creation of functions such as h-let-it straightforward.

Changes
Potentially breaking
h-type is now more specific

It now returns either :kvl or :lines (depending on the case) instead of :kvl-or-lines.

This may cause breakage if you were using its :kvl-or-lines result somewhere (e.g. in a cond or pcase statement).

h->kvl now defaults to the much more common "=" as KVL delimiter instead of TAB

If you were using it somewhere without passing a SEP argument, the output will be different.
To keep outputting it using TAB as delimiter (if that's what you want), pass it explicitly:

(h->kvl (h* 'Alice 42 'Bob 30) "\t")
=> "\
Alice   42
Bob 30
"

whereas without the SEP it now outputs:

(h->kvl (h* 'Alice 42 'Bob 30))
=> "\
Alice=42
Bob=30
"
h-kvl? and h<-kvl now use the regex " *= *" for default delimiter

It used to also match TAB — but this separator is rarer for such files, and made it indistinguishable from two-column TSVs for detection: it made it hard to tell whether the first line would have been a TSV header or a key–value line.

The delimiter is now stored in the variable h-kvl-sep-re, so the default value comes from there instead of being harcoded in the functions. See "Deprecations" further below.

Other
h<-tsv and h<-ssv are now able to ignore empty lines
h->tsv and h->ssv now add a trailing — as h->kvl does
Many improvements in documentation
New features
New regular variables
h-kvl-sep-re, h-kvl-comment-re, and h-kvl-section-re

whose default values now regulate KVL-related operations and can be temporarily let-bound as needed.

New non-interactive functions
h-let-it

extends h-let to any key–value thing convertible to hash table. See its many examples.

h<-cons-pair and h->cons-pair

because although a cons-pair is rather trivial, it was a missing key–value store to convert from and to; used in h<-it's autodetection.

h-read-kvl, h-read-lines, and h-read-cons-pair

were added for completeness, as they were missing elements of the h-read-X functions set.

New commands
xht-see-news

to open README.org, narrow into the News heading, skip to the latest, recenter, and show branches.

xht-see-function-in-readme

to pick an xht function from minibuffer, find it in README.org, recenter, and show it.

New arguments to existing commands
xht-see-readme may now take two optional arguments when called non-interactively
New usage examples

(we now have more than 2000 examples, covering pretty much every public function of the package!)

for the new functions h-let-it, h<-cons-pair, and h->cons-pair
for h-let
for kvl-related functions
for all functions of the ‘h-rej’ family
and many others
Deprecations
h-kvl? and h<-kvl no longer take SEP as optional argument

To pass a different delimiter, temporarily let-bind the variable h-kvl-sep-re instead.

The old syntax of both functions is still supported (no breakage should immediately happen), but is otherwise deprecated and should be replaced.

Fixes
Fixed h--rej* and h--rej*!, which were not working
h-json? no longer throws an error in some cases where it should have returned nil
h<-lines no longer treats blank strings exceptionally, nor does it accept nil

This is more consistent and expected. So:

(h<-lines "  \n   \n")   H=> (h* 0 "  " 1 "   " 2 "")
(h<-lines "")            H=> (h* 0 "")
(h<-lines "" 5 'eq)     Hp=> (h-st* 5 'eq 0 "")
(h<-lines nil)           !!> error
Many fixes in requires and dependencies
Many small bugfixes
Many small docstring improvements

1.0.0

Major public release

See also

Other libraries

This README was generated with the enormous help of Exemplify-ERT and OrgReadme-fy, two libraries of the same author. If you liked how it was organized and you're an Emacs developer, you may want to give them a look.

OrgReadme-fy itself uses XHT to create some half dozen hash tables that it uses to piece together a README.org file. This means you can have some further idea of how XHT looks like in action by having a look at orgreadme-fy.el's code. In particular, check orgreadme-fy-libfile->orgstr, which at the moment makes good use of h--first, h--sel, h<-plist, h-mix, h--each, and h-let. For more, see the other functions there ending in -ht — say, orgreadme-fy-funs-ht.

License

This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.

Therefore, license and copyright information can be found in:

  • each file's comment header, or
  • an adjacent file of the same name with the additional extension .license, or
  • the .reuse/dep5 file

The full text of the licenses can be found in the LICENSES subdirectory.


xht.el

Structure

;;; xht.el --- The extensive hash table library  -*- lexical-binding: t -*-
;;; Commentary:
;;;; For all the details, please do see the README
;;; Acknowledgments:
;;; Code:
;;;; Libraries
;;;; Symbols from other packages
;;;; Package metadata
;;;; Customizable variables
;;;; Other variables
;;;; Description macro
;;;; Naming conventions
;;;; Functions
;;;;; General hash table operations
;;;;;; Creation
;;;;;;; Empty one
;;;;;;; Cloning and copying
;;;;;;; From keys and values
;;;;;;;; passed directly
;;;;;;;; passed as vectors
;;;;;;;; passed as lists
;;;;;;;; helper: determine initial size
;;;;;; Binding
;;;;;;; Dot-binding (h-let and h-let-it)
;;;;;; Canonical representations
;;;;;;; Changing canonical representation — from table
;;;;;;;; #s(hash-table…)
;;;;;;;; (ht…)
;;;;;;;; (h*…) and (h-st*…)
;;;;;;; Changing canonical representation — from any ht-like form
;;;;;;;; helper
;;;;;; Mapping from hash table
;;;;;;; to vector
;;;;;;; to list
;;;;;;; to hash table
;;;;;;;; non-recursive
;;;;;;;; recursive
;;;;;;; for side-effects only
;;;;;; Keys operations
;;;;;;; Retrieval (getting)
;;;;;;;; Return value given key
;;;;;;;; Return the very first pair (pop)
;;;;;;;; Return a random pair
;;;;;;;; Return the first pair matching a predicate
;;;;;;; Addition (setting)
;;;;;;;; put
;;;;;;;; put-add
;;;;;;; Removal
;;;;;;;; rem
;;;;;;; Selection
;;;;;;;; sel-keys
;;;;;;;; sel
;;;;;;; Rejection
;;;;;;;; rej-keys
;;;;;;;; rej
;;;;;;; Reverse order
;;;;;;; Change numerical value, including #'1+ and #'1-
;;;;;;;; helper
;;;;;;; Miscellaneous key, value, pair lookups
;;;;;;;; Has this key, this value, or this pair at some level?
;;;;;;;; Has all these keys, these values, or these pairs?
;;;;;; Multi-table operations
;;;;;;; Union
;;;;;;;; mix
;;;;;;;;; helper
;;;;;;; Difference
;;;;;;;; dif
;;;;;;;;; helper
;;;;;;; Intersection
;;;;;;;; cmn
;;;;;;;;; helper
;;;;;;; Comparison / Equality / Equivalence
;;;;;;;; 1D: implicit    (Vector, List, Lines) (indices as keys)
;;;;;;;; 1D: explicit    (KVL, Cons Cell)
;;;;;;;; ≥1D unspecified (Hash table)          (possibly nested)
;;;;;;;;; helper
;;;;;;;; ≥1D unspecified (Alist, Plist, JSON)  (possibly nested)
;;;;;;;; 2D              (LOL, Org table, TSV, CSV, SSV)
;;;;;;;; Unknown thing
;;;;;;;;; helper
;;;;;;;; Helper
;;;;;; Conversion
;;;;;;; Hash table to hash table
;;;;;;; 1D: implicit    (indices as keys)
;;;;;;;; Vectors
;;;;;;;; Lists
;;;;;;;; Lines
;;;;;;; 1D: explicit
;;;;;;;; Key–Value Lines
;;;;;;;; Cons Pairs
;;;;;;; ≥1D unspecific  (possibly nested)
;;;;;;;; Association lists
;;;;;;;;; from alist
;;;;;;;;; to alist
;;;;;;;; Property lists
;;;;;;;;; from plist
;;;;;;;;; to plist
;;;;;;;; JSON
;;;;;;; 2D              (tabular)
;;;;;;;; Lisp tables (lists of lists)
;;;;;;;; Org tables
;;;;;;;; TSV, CSV, SSV
;;;;;;;;; Without using org (preferred)
;;;;;;;;; Using org
;;;;;;; Unknown thing
;;;;;; Predicates
;;;;;;; Type
;;;;;;; Dimension
;;;;;; Properties and dimension
;;;;;;; Hash tables
;;;;;;; Generalization to other types
;;;;;; Writing and reading
;;;;;;; Write to file
;;;;;;; Read
;;;;;; Miscellaneous
;;;;;;; Format object as symbol, string, keyword, or number
;;;;; Specialized hash table operations
;;;;;; “Hash tabular”: 2D-specific operations
;;;;;;; Creation
;;;;;;; Information
;;;;;;; Mapping
;;;;;;;; to hash table 2D
;;;;;;; Selection or rejection of columns
;;;;;;; Transposition
;;;;;;; helper
;;;;;; Hash tables as sets
;;;;;; Hash tables for counting
;;;;;;; helper
;;;;; Helper
;;;;;; keyword <-> symbol <-> string <-> number
;;;;;; Convert from string to types + types predicates
;;;;;;; string to type (selector)
;;;;;;; string to hash table
;;;;;;; string to alist
;;;;;;; string to plist
;;;;;;; string to lol (list of lists)
;;;;;;; string to list
;;;;;;; string to cons pair
;;;;;;; string to vector
;;;;;; Miscellaneous
;;;;; Miscellaneous
;;;;;; Additional examples
;;;; Commands
;;;;; XHT dispatcher
;;;;;; Main dispatcher
;;;;;;; Helper functions
;;;;;;;; Non-interactive user input: cleanup
;;;;;;;; Interactive user input
;;;;;;;; String-maker
;;;;;;;; Reading
;;;;;;;;; Thing at point
;;;;;;;;;; Hash table
;;;;;;;;; Boundaries
;;;;;;;; String formatting (cm or pp)
;;;;;;;;; JSON
;;;;;;;;; Other strings
;;;;;;;; Insertion
;;;;;;;; Display
;;;;;;;; Last sexp
;;;;;; Specialized quicker access
;;;;;;; Helper functions
;;;;; Minor modes
;;;;;; xht-do minor mode
;;;;;; Font lock minor mode
;;;;; See README
;;;;;; Helper functions
;;;;;;; old helpers
;;;; Aliases
;;;;; For those who prefer to use a single namespace
;;;;;; To ht functions
;;;;;; To native functions
;;;;; Preds: users of 'p' for y/n questions will be pleased — amiritep
;;;;;; To h functions
;;;;;; To ht functions
;;;;;; To native functions
;;;;; DWIM
;;;;;; because usually lists is what you want
;;;;;; because you probably want it in pretty-printed format
;;;;; Just shorter
;;;;; Consistency
;;;;; Directional: make any order work
;;;;; Provisional: until other functions have been implemented
;;;; Wrapping up
;;; xht.el ends here

Contents

;;; xht.el --- The extensive hash table library  -*- lexical-binding: t -*-

;; SPDX-FileCopyrightText:  © flandrew <https://keyoxide.org/191F5B3E212EF1E515C19918AF32B9A5C1CCCB2D>
;; SPDX-License-Identifier: GPL-3.0-or-later

;;---------------------------------------------------------------------------
;; Author:            flandrew
;; Created:           2022-04-10
;; Version:           2.0.0-git2023.07.31
;; Homepage:          <https://flandrew.srht.site/listful/software.html>
;; Keywords:          extensions, lisp
;; Package-Requires:  ((emacs "25.1") (dash "2.15") (s "1.12") (ht "2.3"))
;;---------------------------------------------------------------------------

;; This file is part of XHT, which is NOT part of GNU Emacs.

;; 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. For more details, see the full license at
;; either LICENSES/GPL-3.0-or-later.txt or <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; XHT is a hash table library. It's extensive and systematically-structured.
;;
;; My intention with it is that:
;;
;;   1. dealing with hash tables in Emacs Lisp be pleasant,
;;
;;   2. hash tables become your go-to choice for most key–value operations in
;;      the language, and
;;
;;   3. for almost everything hash-table–related that you might want to do in
;;      Emacs Lisp, this library will have functions for it — or at least
;;      close enough that you can do it by composing a few of them.
;;
;; Let me know if I succeeded.
;;
;;;; For all the details, please do see the README
;;
;; Open it easily with any of the below:
;;   (find-file-read-only              "README.org")   <--- C-x C-e here¹, or
;;   (find-file-read-only-other-frame  "README.org")   <--- C-x C-e here¹, or
;;   (find-file-read-only-other-window "README.org")   <--- C-x C-e here¹
;;
;; or from any buffer:
;;   M-x xht-see-readme
;;
;; or read it online:
;;   <https://flandrew.srht.site/listful/sw-emacs-xht.html>
;;
;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; Acknowledgments:
;;
;; A few of this library's functions adapted code from ht.el.
;; This is usually indicated in the functions themselves with a link to the
;; corresponding ht function. (See, e.g., ‘h-put!’ and ‘h-rem!’.)
;;
;; Thank you, Wilfred Hughes (and other ht contributors)!
;;
;; About ht:
;;   SPDX-FileCopyrightText:  © 2013 Wilfred Hughes
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Wilfred Hughes
;;   Homepage:                <https://github.com/Wilfred/ht.el>
;;
;; ------------------------------------------------------------------------
;;
;; A few parts of this library were inspired by dash.el.
;; The following functions borrowed and adapted code from dash:
;; - much of xht's fontlock/fontification (from dash's fontification)
;; - part of the h-zip and h-unzip family of functions
;;
;; My thanks to Magnar Sveen and the other dash contributors.
;;
;; About dash:
;;   SPDX-FileCopyrightText:  © 2012 Free Software Foundation, Inc.
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Magnar Sveen
;;   Homepage:                <https://github.com/magnars/dash.el>
;;
;; ------------------------------------------------------------------------
;;
;; The main and the helper functions of ‘h-let’ were inspired by, and
;; adapted from, ‘let-alist’.
;;
;; I thank Artur Malabarba for this handy macro.
;;
;; About let-alist:
;;   SPDX-FileCopyrightText:  © 2014 Free Software Foundation, Inc.
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Artur Malabarba
;;
;; ------------------------------------------------------------------------
;;
;; The functions ‘xht--do-string-cm-ify’, ‘xht--do-string-escape’, and
;; xht--do-string-escape-ws’ were adapted from string-edit.el.
;;
;; About string-edit:
;;   SPDX-FileCopyrightText:  © 2013 Magnar Sveen
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Magnar Sveen
;;   Homepage:                <https://github.com/magnars/string-edit.el>
;;
;; ------------------------------------------------------------------------
;;
;; The function ‘h-alist?’ was adapted from json.el's ‘json-alist-p’.
;;
;; About json:
;;   SPDX-FileCopyrightText:  © 2006 Free Software Foundation, Inc.
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Theresa O'Connor
;;
;; ------------------------------------------------------------------------
;;
;; Helper ‘xht--str-nw?’ was adapted from org-macs.el's ‘org-string-nw-p’.
;;
;; About org:
;;   SPDX-FileCopyrightText:  © 2004 Free Software Foundation, Inc.
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Carsten Dominik
;;   Homepage:                <https://orgmode.org>
;;
;; ------------------------------------------------------------------------
;;
;; Macro ‘xht--describe’ was borrowed from OrgReadme-fy.
;;
;; Functions ‘xht-see-readme’, ‘xht--see-library-readme’, and
;; xht--libname->libfile’ were adapted from ‘orgreadme-fy-see-readme’,
;; orgreadme-fy-see-library-readme’, and ‘orgreadme-fy-libname->libfile’,
;; respectively.
;;
;; About orgreadme-fy:
;;   SPDX-FileCopyrightText:  © flandrew
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  flandrew
;;   Homepage:                <https://sr.ht/~flandrew/orgreadme-fy>
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Code:
;;;; Libraries

(require 's)
(require 'ht)
(require 'pp)
(require 'rx)
(require 'dash)
(require 'json)
(require 'pcase)
(require 'subr-x)    ; if-let’, ‘when-let
(require 'lisp-mnt)  ; lm-summary’, ‘lm-version’, ‘lm-homepage
(eval-when-compile
  (require 'inline))

;;;; Symbols from other packages

;; Silence "not known to be defined" compiler warnings
(declare-function org-mode          "ext:org"       ())
(declare-function org-at-table-p    "ext:org"       (&optional table-type))
(declare-function org-ctrl-c-ctrl-c "ext:org"       (&optional arg))
(declare-function orgtbl-mode       "ext:org-table" (&optional arg))
(declare-function org-table-begin   "ext:org-table" (&optional table-type))
(declare-function org-table-end     "ext:org-table" (&optional table-type))
(declare-function org-table-to-lisp "ext:org-table" (&optional txt))
(declare-function org-table-convert-region "ext:org-table" (beg0 end0 &optional
                                                                 separator))
(declare-function aggressive-indent-mode   "ext:aggressive-indent" (&optional
                                                                    arg))

;;;; Package metadata

(defconst xht--name "XHT")

(defconst xht--dot-el
  (format "%s.el" (file-name-sans-extension (eval-and-compile
                                              (or load-file-name
                                                  buffer-file-name)))))
(defconst xht--readme-org
  (expand-file-name "README.org" (file-name-directory xht--dot-el)))

(defconst xht--summary  (lm-summary  xht--dot-el))
(defconst xht--version  (lm-version  xht--dot-el))
(defconst xht--homepage (lm-homepage xht--dot-el))

;;;; Customizable variables

(defgroup xht nil
  (format "%s." xht--summary)
  :group 'extensions
  :group 'lisp
  :link  '(emacs-library-link :tag "Lisp file" "xht.el")
  :link  `(file-link :tag "README.org" ,xht--readme-org)
  :link  `(url-link  :tag "Homepage"   ,xht--homepage))

;; See also ‘xht-do-mode-lighter’ under xht-do minor mode.
;; See also ‘xht-fontlock-add-anaphoric-variables’ and other defcustom options
;; under Font lock minor mode.

;;;; Other variables

(defvar h-kvl-sep-re  " *= *"
  "Regular expression to match KVL separator (field delimiter).

So if a candidate KVL string has in every non-blank, non-commented
line a single equal sign surrounded or not by spaces, this will be
taken as a valid KVL, and the regex will be taken as the field
delimiter to parse the key–value lines.

This variable should not be directly changed. Rather, it should be
let-bound around KVL-related expressions when the default delimiter
is not applicable. Here are two examples for when colon is used as
delimiter (the first strictly with no spaces, the second accepting
it whether surrounded by spaces or not):

  (let ((h-kvl-sep-re \":\"))
    (h-kvl? \"Alice:42\\nBob:30\"))

  (let ((h-kvl-sep-re \" *: *\"))
    (h<-kvl \"\
Alice : 42
Bob   : 30\"))")

(defvar h-kvl-section-re
  "^[ \t]*\\[.*\\][ \t]*$"
  "Regular expression to match sections in KVLs.

This is used to non-destructively strip sections in key–value lines
in KVL–related functions performing conversion and type-testing.

It by default matches lines with [some text] surrounded or not by
spaces or TABs. This is useful to deal with config files that have
sections only for information purposes but which don't otherwise
modify or restrict the keys under it.

This variable should not be directly changed. Rather, it should be
let-bound around KVL-related expressions when the default section
regex is not applicable. Here is an example for a hypothetical KVL
that uses braces to mark sections:

  (let ((h-kvl-section-re \"^[ \\t]*{.*}[ \\t]*$\"))
    (h-kvl? \"\\
{People}
Alice = 42
Bob   = 30
Emily = 21\"))")

(defvar h-kvl-comment-re
  "^#.*\\|[ \t]#.*"
  "Regular expression to match comments in KVLs.

This is used to non-destructively strip comments in key–value lines
in KVL–related functions performing conversion and type-testing.

It by default matches:
- BOL followed by # followed by anything until EOL
- One space or TAB followed by # followed by anything until EOL

where BOL and EOL mean beginning and end of line, respectively.

This variable should not be directly changed. Rather, it should be
let-bound around KVL-related expressions when the default comment
regex is not applicable. Here is an example for a hypothetical KVL
that uses semicolons for comments:

  (let ((h-kvl-comment-re \"^;.*\\\\| +;.*\"))
    (h-kvl? \"\\
;; These are some people
Alice=42  ; Alice's real age!
Bob=30
Emily=21\"))")

;; See also ‘xht-do-mode-map’ under xht-do minor mode.

;;;; Description macro

(defmacro xht--describe (str)
  "Describe with string STR the contents under this heading.
Think of it as a docstring for the headings of your elisp files.

For the full docstring, look for ‘orgreadme-fy-describe’ in the
package ‘orgreadme-fy’."
  (declare (indent 0))
  (unless (stringp str)
    (user-error "‘xht--describe’ must receive a string")))

;;;; Naming conventions

(xht--describe
  "This library adopts the following naming conventions:

| These functions | look like |
|-----------------+-----------|
| Regular         | h-foo     |
| Anaphoric       | h--foo    |
| Interactive     | xht-foo   |
| Private         | xht--foo  |

and

| These variables | look like |
|-----------------+-----------|
| Regular         | h-foo     |
| Customizable    | xht-foo   |
| Private         | xht--foo  |

The two tables above represent almost all of the more than 500
symbols defined by ‘xht’. These also happen to be the same familiar
conventions that have long been adopted by ‘dash’.

There are some *functions that convert from and to hash tables*:

| These functions      | look like            |
|----------------------+----------------------|
| Regular (conversion) | h->foo, h<-foo       |
| Private (conversion) | xht-->foo, xht<--foo |

This intuitive use of '->' and '<-' to represent conversion will
certainly look familiar to those who have used ‘ht’.

That's pretty much it.

The only symbols that don't perfectly match the tables above are:

- The commands ‘global-xht-do-mode’ and ‘global-xht-fontify-mode’.

- Half a dozen regular functions:
  - The type-checking predicate ‘h?’.
  - The ubiquitous hash-table creator ‘h*’.
  - The hash-table equality predicates ‘h=’, ‘h==’, ‘h_=’, ‘h~=’.

Now, complementing the previous conventions, you'll notice that
some suffixes are consistently employed:

- ? :: Functions that have an ? as suffix are *predicates*
       (they \"ask\" a yes-or-no question and return t or nil).
       They have a corresponding -p alias.

- = :: Functions that have an = as suffix are *equality predicates*
       and therefore check whether two or more things can be
       considered equivalent according to some standard.

- * :: Functions that act on *nested hash tables* have an * as
       suffix, and vice-versa. (With the notable exception of ‘h*’,
       whose * has a slightly different, but hopefully obvious
       enough, connotation.)

- ! :: Functions that *destructively modify* the table that is
       passed as argument have an ! as suffix, and vice-versa.

       *There are no !-less aliases for destructive functions*:
       wherever the ! is dropped, it means it's side-effect free:
       the value of the result of the operations is returned, and
       TABLE remains unmodified. If you find it happening
       otherwise, it's most likely unintentional and a bug — please
       report.

       Note that this differs from ht's naming convention, in which
ht-clear’, ‘ht-remove’, ‘ht-set’, and ‘ht-update’ remain as
       aliases to, respectively, ‘ht-clear!’, ‘ht-remove!’,
ht-set!’, and ‘ht-update!’ — all of which destructive
       functions. (But ‘ht-reject’ is not an alias to ‘ht-reject!’:
       the former is non-destructive, the latter is destructive.)

       On the other hand, xht follows this convention consistently.
       For example:

       | Non-destructive | Destructive |
       |-----------------+-------------|
       | ‘h-rej’         | ‘h-rej!’    |
       | ‘h-sel’         | ‘h-sel!’    |
       | ‘h-put’         | ‘h-put!’    |
       | ‘h-mix’         | ‘h-mix!’    |
       | ‘h-mix*’        | ‘h-mix*!’   |

       etc.

The library itself may be called either XHT or xht. Both are fine.

Mixed casing is better avoided. Yes, you'll see it titleized in the
docstrings of ‘global-xht-do-mode’ and ‘global-xht-fontify-mode’ —
but only because these docstrings happen to be automatically
generated by ‘define-globalized-minor-mode’, which calls
easy-mmode-pretty-mode-name’, which titleizes names.")

;;;; Functions
;;;;; General hash table operations
;;;;;; Creation

(xht--describe
  "Functions that create a hash table.")

;;;;;;; Empty one

(xht--describe
  "Functions that create an empty hash table.")

(define-inline h-new (&optional sz ts we rs rt)
  "Create empty hash table.

Arguments passed correspond to, respectively, size, test, weakness,
rehash-size, rehash-threshold. When nil, they match that of TABLE.

SZ is the initial assigned size. When nil, default to 65.

TS is the function used to compare the hash keys.
It can be ‘eq’, ‘eql’, ‘equal’ or a user-supplied test created
via ‘define-hash-table-test’. When nil, default to ‘equal’.

The other three default to Emacs' defaults:
- WE defaults to nil.
- RS defaults to 1.5.
- RT defaults to 0.8.

This is similar to ‘ht-create’. The differences are:

- size as a first optional argument, test as second;

- possibility of passing other parameters accepted
  by ‘make-hash-table’, which see.

See also: ‘h-clr’ and ‘h-clr!’."
  (inline-quote
   (make-hash-table
    :size             (or ,sz 65)
    :test             (or ,ts 'equal)
    :weakness         ,we
    :rehash-size      (or ,rs 1.5)
    :rehash-threshold (or ,rt 0.8))))

(define-inline h-empty-clone (table &optional sz ts we rs rt)
  "Create empty hash table with some or all properties from TABLE.
Arguments passed correspond to, respectively, size, test, weakness,
rehash-size, rehash-threshold. When nil, they match that of TABLE.

This is a non-destructive function.
See also: ‘h-clr’ and ‘h-clr!’."
  (inline-quote
   (make-hash-table
    :size             (or ,sz (hash-table-size             ,table))
    :test             (or ,ts (hash-table-test             ,table))
    :weakness         (or ,we (hash-table-weakness         ,table))
    :rehash-size      (or ,rs (hash-table-rehash-size      ,table))
    :rehash-threshold (or ,rt (hash-table-rehash-threshold ,table)))))

(define-inline h-clr (table)
  "Create empty hash table whose properties match those of TABLE.

It preserves all of the properties of the original table — except
data, which is empty. It's therefore particularly useful as a basis
for other hash table functions (such as ‘h-hmap*’) that perform
non-destructive operations — for which a fresh empty table must be
created, and for which the use of ‘h-new’ or ‘ht-create’ would
result in a loss of these properties.

This is a non-destructive function.
Its destructive counterpart is ‘h-clr!’."
  (declare (pure t) (side-effect-free t))
  (inline-quote
   (make-hash-table
    :size             (hash-table-size             ,table)
    :test             (hash-table-test             ,table)
    :weakness         (hash-table-weakness         ,table)
    :rehash-size      (hash-table-rehash-size      ,table)
    :rehash-threshold (hash-table-rehash-threshold ,table))))
;; ^ We could just (defsubst h-clr (table) (h-empty-clone table))
;;   but this function is a bit too fundamental, so I'm optimizing it.

(define-inline h-clr! (table)
  "Remove all keys from TABLE.
This is equivalent to current ‘ht-clear!’.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-clr’."
  (inline-quote
   (ignore
    (clrhash ,table))))

;;;;;;; Cloning and copying

(xht--describe
  "See ‘h-clone*’ and ‘h<-ht’.")

;;;;;;; From keys and values

(xht--describe
  "Functions to create a hash table from keys and values.")

;;;;;;;; passed directly

(defun h* (&rest pairs)
  "Create a hash table with the key–value PAIRS.
Keys are compared with ‘equal’.

This function is similar to ‘ht’, with the difference that no
parentheses are used around each key–value pair: so its syntax is
identical to that used to create a plist, but replacing ‘list’ with
h*’.

Also, caution: with plists and alists, repeated items that come
first have priority, whereas when passing pairs to ‘ht’ or ‘h*’.
it's the opposite — the repeated item's last value wins.

Compare:
#+begin_src emacs-lisp
  (plist-get '(:a 1 :b 2 :a 5) :a)       => 1
  (ht-get (h*  :a 1 :b 2 :a 5) :a)       => 5
  (ht-get (ht (:a 1) (:b 2) (:a 5)) :a)  => 5
#+end_src

Another difference is that, instead of 65, its initial nominal size
defaults to the result of ‘xht--init-size’, which determines
initial nominal size as a function of the number of pairs.
Currently, ‘xht--init-size’ uses ‘xht--init-size-exact’, which
returns the exact number of pairs.

\(fn KEY-1 VALUE-1 KEY-2 VALUE-2 ...)"
  (declare (pure t) (side-effect-free t))
  (let ((len (length pairs)))
    (if (= 0 (% len 2))
        (let ((tbl (h-new (xht--init-size (/ len 2)))))
          (while pairs
            (h-put! tbl (pop pairs) (pop pairs)))
          tbl)
      (signal 'wrong-number-of-arguments '((3 . 3) 2)))))

(defun h-s* (size &rest pairs)
  "Create a hash table of size SIZE with the key–value PAIRS.
This is exactly like ‘h*’ (which see), with the difference that its
nominal size is explicitly passed as first argument instead of being
inferred.

When SIZE is nil, it defaults to h*'s default.

\(fn SIZE KEY-1 VALUE-1 KEY-2 VALUE-2 ...)"
  (declare (pure t) (side-effect-free t))
  (let ((len (length pairs)))
    (if (= 0 (% len 2))
        (let ((tbl (h-new (or size (xht--init-size (/ len 2)))
                          nil)))
          (while pairs
            (h-put! tbl (pop pairs) (pop pairs)))
          tbl)
      (signal 'wrong-number-of-arguments '((3 . 3) 2)))))

(defun h-t* (test &rest pairs)
  "Create a hash table of function TEST with the key–value PAIRS.
This is exactly like ‘h*’ (which see), with the difference that its
test function is explicitly passed as first argument instead of
being inferred.

When TEST is nil, it defaults to h*'s default, which is h-new's
default: 'equal.

\(fn TEST KEY-1 VALUE-1 KEY-2 VALUE-2 ...)"
  (declare (pure t) (side-effect-free t))
  (let ((len (length pairs)))
    (if (= 0 (% len 2))
        (let ((tbl (h-new (xht--init-size (/ len 2))
                          test)))
          (while pairs
            (h-put! tbl (pop pairs) (pop pairs)))
          tbl)
      (signal 'wrong-number-of-arguments '((3 . 3) 2)))))

(defun h-st* (size test &rest pairs)
  "Create a hash table of SIZE and TEST with the key–value PAIRS.
This is exactly like ‘h*’ (which see), with the difference that its
nominal size and its test function are explicitly passed as
first arguments instead of being inferred.

When SIZE is nil, it defaults to h*'s default.

When TEST is nil, it defaults to h*'s default, which is h-new's
default: 'equal.

\(fn SIZE TEST KEY-1 VALUE-1 KEY-2 VALUE-2 ...)"
  (declare (pure t) (side-effect-free t))
  (let ((len (length pairs)))
    (if (= 0 (% len 2))
        (let ((tbl (h-new (or size (xht--init-size (/ len 2)))
                          test)))
          (while pairs
            (h-put! tbl (pop pairs) (pop pairs)))
          tbl)
      (signal 'wrong-number-of-arguments '((3 . 3) 2)))))

(defun h-reduce-r (&rest keys-value)
  "Return a one-key nested table with the KEYS-VALUE sequence.
So, for example, (h-reduce-r :a :b :c 2) is equivalent
to (h* :a (h* :b (h* :c 2))), which is the same
as (ht (:a (ht (:b (ht (:c 2))))))."
  (declare (pure t) (side-effect-free t))
  (pcase (length keys-value)
    ((pred (<= 2)) (--reduce-r (h* it acc) keys-value))
    (1             (h* keys-value)) ;; wrong-number-of-arguments error
    (0             (h-new 1))))

;;;;;;;; passed as vectors

(defmacro h--zip-vectors-with (key-form val-form keys values
                                        &optional size test)
  "Anaphoric version of ‘h-zip-vectors-with’.
The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Each element of KEYS   is bound to the symbol key.
Each element of VALUES is bound to the symbol value.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

The (roughly) inverse of this function is ‘h--vunzip-with’."
  (let ((lk (make-symbol "length-of-keys"))
        (lv (make-symbol "length-of-values"))
        (i  (make-symbol "i"))
        (r  (make-symbol "results")))
    `(if ,keys
         (and (vectorp ,keys)
              (vectorp ,values)
              (let* ((,lk (length ,keys))
                     (,lv (length ,values))
                     (,i  0)
                     (,r  (h-new (or ,size
                                     (xht--init-size ,lk))
                                 ,test))
                     key value)
                (ignore key value)
                (while (< ,i ,lk)
                  (setq key   (aref ,keys ,i)
                        value (when (< ,i ,lv)
                                (aref ,values ,i))
                        ,i    (1+ ,i))
                  (h-put! ,r ,key-form ,val-form))
                ,r))
       (h-new 1))))

(defun h-zip-vectors-with (key-fun val-fun keys values &optional size test)
  "Create a hash table with KEYS and VALUES modified by functions.
The function KEY-FUN modifies each key.
The function VAL-FUN modifies each value.

Both functions take two variables: key and value, respectively.

Each KEY and VALUE is taken one by one from their corresponding
positions at the vectors KEYS and VALUES.

If KEYS is nil, return empty hash table.
If KEYS is shorter than VALUES, ignore additional elements of VALUES.
If VALUES is shorter than KEYS, complete VALUES with nils.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

The anaphoric version of this function is ‘h--zip-vectors-with’.

The (roughly) inverse of this function is ‘h-vunzip-with’."
  (declare (pure t) (side-effect-free t))
  (h--zip-vectors-with (funcall key-fun key value)
                       (funcall val-fun key value)
                       keys values size test))

(defun h-zip-vectors (keys values &optional size test)
  "Create a hash table with KEYS and VALUES. Both must be vectors.
This is just like ‘h-zip-vectors-with’, but without modifying any of the keys
and values before putting them on the table.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

The (roughly) inverse of this function is ‘h-vunzip’."
  (declare (pure t) (side-effect-free t))
  (h--zip-vectors-with key value keys values size test))

;;;;;;;; passed as lists

(defmacro h--zip-lists-with (key-form val-form keys values
                                      &optional size test)
  "Anaphoric version of ‘h-zip-lists-with’.
The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Each element of KEYS   is bound to the symbol key.
Each element of VALUES is bound to the symbol value.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

The (roughly) inverse of this function is ‘h--lunzip-with’."
  (let ((ks (make-symbol "keys"))
        (vs (make-symbol "values"))
        (lk (make-symbol "length-of-keys"))
        (r  (make-symbol "results")))
    `(if ,keys
         (and (listp ,keys)
              (listp ,values)
              (let* ((,ks (-copy ,keys))
                     (,vs (-copy ,values))
                     (,lk (length ,ks))
                     (,r  (h-new (or ,size
                                     (xht--init-size ,lk))
                                 ,test))
                     key value)
                (ignore key value)
                (while ,ks
                  (setq key   (car ,ks)
                        value (car ,vs))
                  (h-put! ,r ,key-form ,val-form)
                  (!cdr ,ks)
                  (!cdr ,vs))
                ,r))
       (h-new 1))))

(defun h-zip-lists-with (key-fun val-fun keys values &optional size test)
  "Create a hash table with KEYS and VALUES modified by functions.
The function KEY-FUN modifies each key.
The function VAL-FUN modifies each value.

Both functions take two variables: key and value, respectively.

Each KEY and VALUE is taken one by one from their corresponding
positions at the lists KEYS and VALUES.

If KEYS is nil, return empty hash table.
If KEYS is shorter than VALUES, ignore additional elements of VALUES.
If VALUES is shorter than KEYS, complete VALUES with nils.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

The anaphoric version of this function is ‘h--zip-lists-with’.

The (roughly) inverse of this function is ‘h-lunzip-with’."
  (declare (pure t) (side-effect-free t))
  (h--zip-lists-with (funcall key-fun key value)
                     (funcall val-fun key value)
                     keys values size test))

(defun h-zip-lists (keys values &optional size test)
  "Create a hash table with KEYS and VALUES. Both must be lists.
This is just like ‘h-zip-lists-with’, but without modifying any of the keys
and values before putting them on the table.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

The (roughly) inverse of this function is ‘h-lunzip’."
  (declare (pure t) (side-effect-free t))
  (h--zip-lists-with key value keys values size test))


;;;;;;;; helper: determine initial size

(defalias 'xht--init-size 'xht--init-size-exact
  "Function used to determine the initial size of a hash table.
If at some point we decide that a function exists which is more
appropriate for the initial nominal size of a hash table to be
created from a given number of pairs, refactoring is made easier by
simply changing this alias to point to that new, supposedly better,
xht--init-size-<something> function.")

(defsubst xht--init-size-exact (n-pairs)
  "Return N-PAIRS, the exact number of pairs expected.
The rationale is that the initial number of pairs will be, on
average, a much better estimate of how much space the table needs.
For tables (sometimes much) smaller than 65, this leads to better
performance, especially when the table won't be updated with more
items (or when only a few)."
  n-pairs)

(defsubst xht--init-size-fixed (&optional n-pairs)
  "Return 65, Emacs' default nominal creation size for hash tables.
Ignore N-PAIRS."
  (ignore n-pairs)
  65)

;;;;;; Binding

(xht--describe
  "Functions to bind the values of a hash table to variables.
The main and the helper functions of ‘h-let’ were inspired by, and
adapted from, ‘let-alist’.")

;;;;;;; Dot-binding (h-let and h-let-it)

;; xht-do’ uses ‘h-let’, and this compiler error happened at the former:
;;   Error: Symbol’s function definition is void: ‘xht--let-deep-dot-search
;; Adding an autoload to this private function solved it, but so did wrapping
;; all four in ‘eval-and-compile’, which seems preferable.

(eval-and-compile
  (defun xht--let-deep-dot-search (data)
    "Return alist of symbols inside DATA that start with a '.'.
Perform a deep search and return an alist where each car is the
symbol, and each cdr is the same symbol without the '.'."
    (declare (pure t) (side-effect-free t))
    (cond
     ((symbolp data)
      (let ((name (symbol-name data)))
        (when (string-match "\\`\\." name)
          ;; Return the cons cell inside a list, so it can be appended
          ;; with other results in the clause below.
          (list (cons data (intern (replace-match "" nil nil name)))))))
     ((not (consp data)) nil)
     (t (append (xht--let-deep-dot-search (car data))
                (xht--let-deep-dot-search (cdr data))))))

  (defun xht--let-access-sexp (symbol variable)
    "Return a sexp used to access SYMBOL inside VARIABLE."
    (declare (pure t) (side-effect-free t))
    (let* ((clean (xht--let-remove-dot symbol))
           (name  (symbol-name clean)))
      (if (string-match "\\`\\." name)
          clean
        (xht--let-list-to-sexp
         (mapcar #'intern (nreverse (split-string name "\\.")))
         variable))))

  (defun xht--let-list-to-sexp (list var)
    "Turn symbols LIST into recursive calls to ‘gethash’ on VAR.
The symbol will be successively searched also formatted as string,
keyword, number."
    (declare (pure t) (side-effect-free t))
    (let* ((tbl (if (cdr list)
                    (xht--let-list-to-sexp (cdr list) var)
                  var))
           (cl  (car list))                 ;symbol
           (scl (format "%s" cl))           ;string
           (kcl (intern (concat ":" scl)))  ;keyword
           (rcl (read scl))
           (ncl (when (numberp rcl) rcl)))  ;maybe number
      `(or (gethash ',cl   ,tbl)
           (gethash  ,scl  ,tbl)
           (gethash  ,kcl  ,tbl)
           (gethash  ,ncl  ,tbl))))

  (defun xht--let-remove-dot (symbol)
    "Return SYMBOL without the initial dot."
    (declare (pure t) (side-effect-free t))
    (let ((name (symbol-name symbol)))
      (if (string-match "\\`\\." name)
          (intern (replace-match "" nil nil name))
        symbol))))

;;;###autoload
(defmacro h-let (table &rest body)
  "Let-bind dotted symbols to their value in TABLE and execute BODY.
Dotted symbol is any symbol starting with a dot. Only those present
in BODY are let-bound, and this search is done at compile time.

For instance, the following code:

#+begin_src emacs-lisp
  (h-let table
    (if (and .title .body)
        .body
      .site
      .site.contents))
#+end_src

essentially expands to:

#+begin_src emacs-lisp
  (let ((.title (h-get 'title table))
        (.body  (h-get 'body  table))
        (.site  (h-get 'site  table))
        (.site.contents (h-get 'contents (h-get 'site table))))
    (if (and .title .body)
        .body
      .site
      .site.contents))
#+end_src

That is still somewhat simplified, however. In fact, this line:

#+begin_src emacs-lisp
  (h-get 'title table)
#+end_src

looks more like:

#+begin_src emacs-lisp
  (or (h-get \"title\" table)
      (h-get 'title  table)
      (h-get :title  table))
#+end_src

so that your query should work regardless of whether the key is a
string, a symbol, or a keyword. (Or even number.)

Note that if you mix these types in the keys and two keys happen to
share the same word (such as having both 'title and :title as keys
of the same table), results will be unpredictable. But this case
should be as rare as it's frequent the need for type autodetection
in a given one-typed-key hash table.

If you nest ‘h-let’ invocations, the inner one can't access the
variables of the outer one. You can, however, access nested hash
tables by using dots inside the symbol, as showed in the example
above.

This function was inspired by, and adapted from, ‘let-alist’.

See also: ‘h-let-it’."
  (declare (indent 1) (debug t))
  (let ((var (make-symbol "table")))
    `(let ((,var ,table))
       (let ,(mapcar (lambda (x)
                       `(,(car x)
                         ,(xht--let-access-sexp (car x) var)))
                     (delete-dups (xht--let-deep-dot-search body)))
         ,@body))))

;;;###autoload
(defmacro h-let-it (thing &rest body)
  "Let-bind dotted symbols to their value in THING and execute BODY.
Behind the scenes, it converts key–value THING to hash table using
h<-it’, and then dot-binds the result using ‘h-let’."
  (declare (indent 1) (debug t))
  `(funcall #'h-let (h<-it ,thing) ,@body))

;;;;;; Canonical representations
;;;;;;; Changing canonical representation — from table

(xht--describe
  "Functions returning a canonical form that would've generated a hash table.

Note that hash tables will fail equality tests such as:
#+begin_src emacs-lisp
  (eq     #s(hash-table test equal data ('a 1))
          #s(hash-table test equal data ('a 1))) ;=> nil
#+end_src

or:
#+begin_src emacs-lisp
  (eql    #s(hash-table test equal data ('a 1))
          #s(hash-table test equal data ('a 1))) ;=> nil
#+end_src

or:
#+begin_src emacs-lisp
  (equal  #s(hash-table test equal data ('a 1))
          #s(hash-table test equal data ('a 1))) ;=> nil
#+end_src

or:
#+begin_src emacs-lisp
  (equal  (ht ('a 1))  (ht ('a 1))) => nil
#+end_src

But, of course:
#+begin_src emacs-lisp
  (equal '(ht ('a 1)) '(ht ('a 1))) => t
  (equal '(h*  'a 1)  '(h*  'a 1))  => t
#+end_src

So the comparisons below work.

Note: h-htbl-form, not shown here, is just the table itself in
regular form: #s(hash-table…)")

;;;;;;;; #s(hash-table…)

(defsubst h-htbl-form (table)
  "Hash TABLE object as itself.
Function defined only so we have a parallel with ht and h* forms."
  (declare (pure t) (side-effect-free t))
  table)

(defun h-htbl-cm-str (table)
  "Hash TABLE object as a string in compact representation."
  (declare (pure t) (side-effect-free t))
  (format "%S" table))

(defun h-htbl-pp-str (table)
  "Hash TABLE object as a string in pretty-printed representation."
  (declare (pure t) (side-effect-free t))
  (pp-to-string table))

;;;;;;;; (ht…)

(defun h-ht-form (table)
  "Restore canonical (ht…) form that creates hash TABLE.
Note that this doesn't preserve TABLE's equality test: (ht…)
defaults to 'equal as test, so if TABLE's test is anything else, it
will become 'equal if the form is evaluated."
  (declare (pure t) (side-effect-free t))
  (let* (result
         (print-quoted t)
         (keyvals (dolist (key (h-keys table 'rev) result)
                    (let ((value (h-get table key)))
                      (push (list (xht--quote key)
                                  (if (ht? value)
                                      (h-ht-form value) ;<-- recurse
                                    (if (equal 't value)
                                        value
                                      (xht--quote value))))
                            result)))))
    `(ht ,@keyvals)))

(defun h-ht-cm-str (table)
  "Restore as string canonical (ht…) form that creates hash TABLE.
Compact representation."
  (declare (pure t) (side-effect-free t))
  (let ((print-quoted t))
    (->> table  h-ht-form  (format "%S"))))

(defun h-ht-pp-str (table)
  "The pretty-print string of (ht…) form that creates hash TABLE."
  (with-temp-buffer
    (emacs-lisp-mode)
    (let ((print-quoted t))
      (->> table  h-ht-form  pp  s-trim
           (replace-regexp-in-string "(ht\n *"      "(ht ")
           (replace-regexp-in-string "\\([^)]\\)\n *" "\\1 ")
           insert)
      (indent-region (point-min) (point-max))
      (xht--buff-str-no-prop))))

;;;;;;;; (h*…) and (h-st*…)

(defun h-h*-form (table)
  "Restore canonical (h*…) or (h-st*…) form that creates hash TABLE.
If TABLE's test is:

- 'equal, we use (h*…), which preserves it. Note that if this form
  is evaluated the nominal size of that table will be determined by
h*’. (This is unlikely to be an issue.)

- not 'equal, we recover its nominal size and equality test and
  use (h-st*…)."
  (declare (pure t) (side-effect-free t))
  (let* (result
         (print-quoted t)
         (keyvals (dolist (key (h-keys table 'rev) result)
                    (let ((value (h-get table key)))
                      (push (if (ht? value)
                                (h-h*-form value) ;<-- recurse
                              (if (equal 't value)
                                  value
                                (xht--quote value)))
                            result)
                      (push (xht--quote key)
                            result)))))
    (if (equal 'equal (h-prop table 'test))
        `(h* ,@keyvals)
      `(h-st* ,(h-prop table 'size)
              ',(h-prop table 'test)
              ,@keyvals))))

(defun h-h*-cm-str (table)
  "Restore as string (h*…) or (h-st*…) form that creates hash TABLE.
Compact representation."
  (declare (pure t) (side-effect-free t))
  (->> table  h-h*-form  (format "%S")))

(defun h-h*-pp-str (table)
  "The pretty-print string of (h*…) form that creates hash TABLE.
If table's test isn't 'equal, use (h-st*…) form."
  (let ((print-quoted t))
    (with-temp-buffer
      (with-syntax-table emacs-lisp-mode-syntax-table
        (->> table  h-h*-cm-str  s-trim  insert)
        (goto-char (point-min))
        (while (not (eobp))
          (cond ((or (looking-at " *(h\\* ")
                     (looking-at " *(h-st\\* "))
                 (goto-char (scan-lists (point) 1 -1))
                 (goto-char (scan-sexps (point) 2)))
                (t
                 (goto-char (scan-sexps (point) 1))
                 (when (looking-at " *)")
                   (re-search-forward " *)+" nil t))
                 (re-search-forward " +$" nil t)
                 (unless (eobp)
                   (insert "\n"
                           (let ((sz (save-excursion
                                       (backward-sexp 2)
                                       (1- (current-column)))))
                             (if (>= sz 0)
                                 (make-string sz ?\s)
                               "")))
                   (goto-char (scan-sexps (point) 1))))))
        (xht--buff-str-no-prop)))))

;;;;;;; Changing canonical representation — from any ht-like form

(xht--describe
  "Choose different representations of hash tables.")

(defun h-lambdify (ht-like)
  "Convert an HT-LIKE object into a lambda that creates the table.
HT-LIKE may come in any of the following autodetected formats:

As Lisp objects:
1. #s(hash-table…) = regular representation
2. (ht…) form
3. (h*…), (h-s*…), (h-t*…), or (h-st*…) form

or any of the above formatted as string (presumably with %S)."
  (pcase (xht--ht-like-format-type ht-like)
    ((or :ht-str :h*-str :h-s*-str :h-t*-str :h-st*-str)
     `(lambda () ,(read ht-like)))
    ((or :ht-form :h*-form :h-s*-form :h-t*-form :h-st*-form
         :htbl)
     `(lambda () ,ht-like))
    (:no-hash-table
     (user-error "‘h-lambdify’: Not hash-table–like — won't lambdify"))))

(defun h-format (format ht-like)
  "Format as FORMAT hash table–like object HT-LIKE.
HT-LIKE may come in any of the following autodetected formats:

As Lisp objects:
1. #s(hash-table…) = regular representation
2. (ht…) form
3. (h*…), (h-s*…), (h-t*…), or (h-st*…) form

or as strings:
Any of the above formatted as string (presumably with %S)

The output FORMAT, in turn, must be specified as one of:
1. htbl         :: #s(hash-table…) (Lisp object)
2. h*-form      :: (h*…) form      (Lisp object)
3. ht-form      :: (ht…) form      (Lisp object)

4. htbl-cm-str  :: #s(hash-table…) string in compact format
5. htbl-pp-str  :: #s(hash-table…) string in pretty-printed format
6. h*-cm-str    :: (h*…) string in compact format
7. h*-pp-str    :: (h*…) string in pretty-printed format
8. ht-cm-str    :: (ht…) string in compact format
9. ht-pp-str    :: (ht…) string in pretty-printed format

Any of these FORMAT options may be passed as either a string, a
keyword, or a symbol — as you prefer. So, for example, any among
\"htbl\", :htbl, or 'htbl works."
  (declare (pure t) (side-effect-free t))
  (let ((as-htbl
         (pcase (xht--ht-like-format-type ht-like)
           ((or :ht-str
                :h*-str
                :h-s*-str
                :h-t*-str
                :h-st*-str)  (-> ht-like  read  xht--dequote-list  eval))
           ;; it's likely not 'quoted in the str, ^ but it could be.
           ((or :ht-form
                :h*-form
                :h-s*-form
                :h-t*-form
                :h-st*-form) (-> ht-like        xht--dequote-list  eval))
           (:htbl-str        (-> ht-like  read))
           (:htbl                ht-like)
           (:no-hash-table
            (user-error "‘h-format’: Not a hash table: %s" ht-like)))))
    (pcase (h-as-keyword format)
      (:htbl                         as-htbl)
      (:h*-form      (h-h*-form      as-htbl))
      (:ht-form      (h-ht-form      as-htbl))
      (:htbl-cm-str  (h-htbl-cm-str  as-htbl))
      (:htbl-pp-str  (h-htbl-pp-str  as-htbl))
      (:h*-cm-str    (h-h*-cm-str    as-htbl))
      (:h*-pp-str    (h-h*-pp-str    as-htbl))
      (:ht-cm-str    (h-ht-cm-str    as-htbl))
      (:ht-pp-str    (h-ht-pp-str    as-htbl))
      (_ (user-error "‘h-format’: Not a reformat output option")))))

;;;;;;;; helper

(defun xht--ht-like-format-type (ht-like)
  "Return type of hash table representation of HT-LIKE."
  (declare (pure t) (side-effect-free t))
  (let* ((str?    (stringp ht-like))
         (lispobj (if str? (read ht-like) ht-like)))
    ;; Note: when string we won't test whether it's pretty-printed or not.
    ;; This is a bit tricky, so we'll take a simpler route.
    (cond ((h?    lispobj) (if str? :htbl-str :htbl))
          ((listp lispobj)
           (setq lispobj (xht--dequote-list lispobj))
           ;; it could be '''multiply-quoted^, so we remove excesses.
           (pcase (car lispobj)
             ('ht    (if str? :ht-str    :ht-form))
             ('h*    (if str? :h*-str    :h*-form))
             ('h-s*  (if str? :h-s*-str  :h-s*-form))
             ('h-t*  (if str? :h-t*-str  :h-t*-form))
             ('h-st* (if str? :h-st*-str :h-st*-form))
             ;; ^note that 'st' is about 'size' and 'test': nothing
             ;; to do with 'string', as is the case of '-str'.
             (_ :no-hash-table)))
          (t :no-hash-table))))

;;;;;; Mapping from hash table
;;;;;;; to vector

(xht--describe
  "Functions that apply a function to every key–value pair of a
hash table and write the results to a vector.

In particular, here's a guideline for the 'vmap family' of
functions:

- the added 'v' means 'result is a vector'
-  an added '-' means 'anaphoric: enter a form, not a lambda'

In the anaphoric version, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a vector
|----------------------+--------------+---------------------|
|                      | a lambda     | a form (anaphoric)  |
|----------------------+--------------+---------------------|
| vector may have nils | ‘h-vmap’     | ‘h--vmap’           |
|----------------------+--------------+---------------------|")

(defun h-vmap (fun table &optional reverse)
  "Apply FUN to each key–value pair of TABLE and return vector.
Function FUN is called with two arguments, key and value.

If REVERSE is non-nil, show it in reverse order.

If you don't use both of these variables, then to avoid warnings
use an underscore before the to-be-ignored variable. For example,
to return a vector of all (expected to be integer) values plus 1:

#+begin_src emacs-lisp
  (h-vmap (lambda (_key value) (1+ value)) table)
#+end_src

Its anaphoric counterpart is ‘h--vmap’."
  (declare (pure t) (side-effect-free t))
  (let* ((size    (h-size table))
         (results (make-vector size nil))
         (pos     (if reverse
                      (lambda (idx) (- size idx 1))
                    (lambda (idx) idx)))

         (idx     0))
    (maphash (lambda (key value)
               (aset results (funcall pos idx) (funcall fun key value))
               (setq idx (1+ idx)))
             table)
    results))

(defmacro h--vmap (form table &optional reverse)
  "Anaphoric version of ‘h-vmap’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound. Results given as vector.

If REVERSE is non-nil, show it in reverse order."
  `(h-vmap (lambda (key value) (ignore key value) ,form)
           ,table ,reverse))

(defmacro h--vunzip-with (key-form val-form table &optional reverse)
  "Anaphoric version of ‘h-vunzip-with’.
The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Both forms take two variables: key and value, respectively.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

If REVERSE is non-nil, reverse the order of both vectors.

The (roughly) inverse of this function is ‘h-zip-vectors’.

See also: ‘h--lunzip-with’."
  (let ((sz (make-symbol "size"))
        (ks (make-symbol "keys"))
        (vs (make-symbol "values"))
        (p  (make-symbol "pos"))
        (i  (make-symbol "i")))
    `(let* ((,sz (h-size ,table))
            (,ks (make-vector ,sz nil))
            (,vs (make-vector ,sz nil))
            (,p  (if ,reverse
                     (lambda (i) (- ,sz i 1))
                   (lambda (i) i)))
            (,i  0))
       (maphash (lambda (key value)
                  (ignore key value)
                  (aset ,ks (funcall ,p ,i) ,key-form)
                  (aset ,vs (funcall ,p ,i) ,val-form)
                  (setq ,i (1+ ,i)))
                ,table)
       (list ,ks ,vs))))

(defun h-vunzip-with (key-fun val-fun table &optional reverse)
  "Return a list of modified vectors of KEYS and VALUES from TABLE.
If REVERSE is non-nil, reverse the order of both vectors.

The function KEY-FUN modifies each key in the results.
The function VAL-FUN modifies each value in the results.

Both functions take two variables: key and value, respectively.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

The (roughly) inverse of this function is ‘h-zip-vectors’.

Its anaphoric counterpart is ‘h--vunzip-with’.

See also: ‘h-lunzip-with’."
  (declare (pure t) (side-effect-free t))
  (h--vunzip-with (funcall key-fun key value)
                  (funcall val-fun key value)
                  table reverse))

(defun h-vunzip (table &optional reverse)
  "Return a list of the vectors of KEYS and VALUES from TABLE.
If REVERSE is non-nil, reverse the order of both vectors.

The (roughly) inverse of this function is ‘h-zip-vectors’.

See also: ‘h-lunzip’."
  (declare (pure t) (side-effect-free t))
  (h--vunzip-with key value table reverse))

(defun h-vitems (table &optional reverse)
  "Return a vector of two-element lists '(key value) from TABLE.
If REVERSE is non-nil, show it in reverse order (the vector;
the pair remains in the (key, value) order)."
  (declare (pure t) (side-effect-free t))
  (h--vmap (list key value) table reverse))

(defmacro h-vkeys (table &optional reverse)
  "Return a vector of all the keys in TABLE.
If REVERSE is non-nil, show it in reverse order."
  (declare (pure t) (side-effect-free t))
  `(h--vmap key ,table ,reverse))

(defmacro h-vvalues (table &optional reverse)
  "Return a vector of all the values in TABLE.
If REVERSE is non-nil, show it in reverse order."
  (declare (pure t) (side-effect-free t))
  `(h--vmap value ,table ,reverse))

(defun h-vrandom (table &optional n)
  "Return a vector of N pseudo-randomly chosen items from hash TABLE.
N must be an integer; if not, signal error.

- If N is nil, make it N=1.

- If N≥ size(TABLE), return a vector of all TABLE's items (same
  result as running ‘h-vitems’).

- If N=0, return nil.

- If N=1, item is returned as a two-item list (key, value).

- If N>1, items are returned as a vector of two-item lists (key, value).

- If N is negative, make N = N + size(TABLE). For example, if TABLE
  has 10 items and N=-2, return 8 pseudo-random items.

See also: ‘h-lrandom’."
  (declare (side-effect-free t))
  (let ((size (h-size table)))
    (cond
     ((null n) (setq n 1))
     ((not (integerp n))
      (user-error "‘h-vrandom’: n must be an integer"))
     ((< n 0)  (setq n (+ size n))))
    (cond
     ((= n 0)     nil)
     ((>= n size) (h-vitems table))
     (t           (let ((sel-keys (make-vector n nil))
                        (keys     (h-keys table))
                        idx key)
                    (while (> n 0)
                      (setq idx  (random (length keys))
                            key  (nth idx keys)
                            keys (-remove-at idx keys)
                            n    (1- n))
                      (aset sel-keys n (list key (h-get table key))))
                    sel-keys)))))

;;;;;;; to list

(xht--describe
  "Functions that apply a function to every key–value pair of a
hash table and write the results to a list.

In particular, here's a guideline for the 'lmap/lkeep family' of
functions:

- the added 'l' means 'result is a list'
-  an added '-' means 'anaphoric: enter a form, not a lambda'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a list
|----------------------+--------------+---------------------|
|                      | a lambda     | a form (anaphoric)  |
|----------------------+--------------+---------------------|
| list may have nils   | ‘h-lmap’     | ‘h--lmap’           |
| any nils are removed | ‘h-lkeep’    | ‘h--lkeep’          |
|----------------------+--------------+---------------------|")

(defun h-lmap (fun table &optional reverse)
  "Apply FUN to each key–value pair of TABLE; list the results.
Function FUN is called with two arguments, key and value.

Identical to current ‘ht-map’, with the difference that the
returned list matches the current order of the TABLE's keys.

If REVERSE is non-nil, show it in reverse order.

If you don't use both of these variables, then to avoid warnings
use an underscore before the to-be-ignored variable. For example,
to return a list of all (expected to be integer) values plus 1:

#+begin_src emacs-lisp
  (h-lmap (lambda (_key value) (1+ value)) table).
#+end_src

Its anaphoric counterpart is ‘h--lmap’.

See also: ‘h-vmap’."
  ;; Note: Using ‘nreverse’ after push seemed to me as more natural for
  ;; mapping, especially when one had control of the order of items added to
  ;; the hash table.
  ;;
  ;; The cost seems worth it, since ‘nreverse’ takes less than a millisecond
  ;; to reverse a list with 100k elements — and I suspect that one dealing
  ;; with a hash table that large will be unlikely to want to map the whole of
  ;; it to a list. And when that is the case and 1 ms is too much, pass a
  ;; non-nil REVERSE flag.
  ;;
  ;; For simpler, smaller hash tables, a delay in the order of microseconds
  ;; won't matter, and the user will likely be more interested in reproducing
  ;; the items in the order that he or she added.
  (declare (pure t) (side-effect-free t))
  (let (results)
    (maphash
     (lambda (key value)
       (push (funcall fun key value) results))
     table)
    ;; That's right, because ‘push’ reversed it already:
    (if reverse
        results
      (nreverse results))))

(defmacro h--lmap (form table &optional reverse)
  "Anaphoric version of ‘h-lmap’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound. List the results.

If REVERSE is non-nil, show it in reverse order.

See also: ‘h--vmap’."
  `(h-lmap (lambda (key value) (ignore key value) ,form)
           ,table ,reverse))

(defun h-lkeep (fun table &optional reverse)
  "Apply FUN to each key–value pair of TABLE; list non-nil results.

We follow Dash's naming scheme here, for which 'keep' is just like
'map' — but with the nils removed.

Function FUN is called with two arguments, key and value.

If REVERSE is non-nil, show it in reverse order.

As with ‘h-lmap’, if you don't use both of these variables, then to
avoid warnings use an underscore before the to-be-ignored variable.

Its anaphoric counterpart is ‘h--lkeep’."
  (declare (pure t) (side-effect-free t))
  (let (results)
    (maphash
     (lambda (key value)
       (when-let ((fkv (funcall fun key value)))
         (push fkv results)))
     table)
    ;; That's right, because ‘push’ reversed it already:
    (if reverse
        results
      (nreverse results))))

(defmacro h--lkeep (form table &optional reverse)
  "Anaphoric version of ‘h-lkeep’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound. List the non-nil results.

If REVERSE is non-nil, show it in reverse order."
  `(h-lkeep (lambda (key value) (ignore key value) ,form)
            ,table ,reverse))

(defmacro h--lunzip-with (key-form val-form table &optional reverse)
  "Anaphoric version of ‘h-lunzip-with’.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

Each element of KEYS   is bound to the symbol key.
Each element of VALUES is bound to the symbol value.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

If REVERSE is non-nil, reverse the order of both lists.

The (roughly) inverse of this function is ‘h-zip-lists’.

Alias: ‘h--unzip-with’.

See also: ‘h--vunzip-with’."
  (let ((ks (make-symbol "keys"))
        (vs (make-symbol "values")))
    `(let (,ks ,vs)
       (maphash (lambda (key value)
                  (ignore key value)
                  (push ,key-form ,ks)
                  (push ,val-form ,vs))
                ,table)
       (list (if ,reverse ,ks (nreverse ,ks))
             (if ,reverse ,vs (nreverse ,vs))))))

(defun h-lunzip-with (key-fun val-fun table &optional reverse)
  "Return a list of modified lists of KEYS and VALUES from TABLE.
If REVERSE is non-nil, reverse the order of both lists.

The function KEY-FUN modifies each key in the results.
The function VAL-FUN modifies each value in the results.

Both functions take two variables: key and value, respectively.

Each key and value is taken one by one from their corresponding
pairs at the hash table TABLE.

The (roughly) inverse of this function is ‘h-zip-lists’.

Its anaphoric counterpart is ‘h--lunzip-with’.

Alias: ‘h-unzip-with’.

See also: ‘h-vunzip-with’."
  (declare (pure t) (side-effect-free t))
  (h--lunzip-with (funcall key-fun key value)
                  (funcall val-fun key value)
                  table reverse))

(defun h-lunzip (table &optional reverse)
  "Return a list of the lists of KEYS and VALUES from TABLE.
If REVERSE is non-nil, reverse the order of both lists.

The (roughly) inverse of this function is ‘h-zip-lists’.

Alias: ‘h-unzip’.

See also: ‘h-vunzip’."
  (declare (pure t) (side-effect-free t))
  (h--lunzip-with key value table reverse))

(defun h-litems (table &optional reverse)
  "Return a list of two-element lists '(key value) from TABLE.
Like ‘ht-items’, but in TABLE's stored order by default.

If REVERSE is non-nil, show it in reverse order (the larger list;
the pair remains in the (key, value) order).

Alias: ‘h-items’."
  (declare (pure t) (side-effect-free t))
  (h--lmap (list key value) table reverse))

(defun h-lkeys (table &optional reverse)
  "Return a list of all the keys in TABLE.
Like ‘ht-keys’, but in TABLE's stored order by default.

If REVERSE is non-nil, show it in reverse order.

Aliases: ‘h-keys’, ‘h-2d-keys’."
  (declare (pure t) (side-effect-free t))
  (h-lmap (lambda (key _value) key) table reverse))

(defun h-lvalues (table &optional reverse)
  "Return a list of all the values in TABLE.
Like ‘ht-values’, but in TABLE's stored order.

If REVERSE is non-nil, show it in reverse order.

Alias: ‘h-values’."
  (declare (pure t) (side-effect-free t))
  (h-lmap (lambda (_key value) value) table reverse))

(defun h-lrandom (table &optional n)
  "Return a list of N pseudo-randomly chosen items from hash TABLE.
N must be an integer; if not, signal error.

- If N is nil, make it N=1.

- If N≥ size(TABLE), return a list of all TABLE's items in pseudo-random order

  (like ‘h-litems’, but shuffled).

- If N=0, return nil.

- If N=1, item is returned as a two-item list (key, value).

- If N>1, items are returned as a list of two-item lists (key, value).

- If N is negative, make N = N + size(TABLE). For example, if TABLE has 10
  items and N=-2, return 8 pseudo-random items.

See also: ‘h-vrandom’, ‘h-pop-random’, and ‘h-pop-random!’."
  (declare (side-effect-free t))
  (let ((size (h-size table)))
    ;; Adjust n:
    (cond ((null n)           (setq n 1))
          ((not (integerp n)) (user-error
                               "‘h-lrandom’: n must be an integer"))
          ((< n 0)            (setq n (+ size n)))
          ((>= n size)        (setq n size)))
    ;; Decide on n:
    (if (= n 0)
        nil
      (let ((keys (h-keys table))
            idx key sel-keys)
        (while (> n 0)
          (setq idx  (random (length keys))
                key  (nth idx keys)
                keys (-remove-at idx keys)
                n    (1- n))
          (push (list key (h-get table key))
                sel-keys))
        sel-keys))))

;;;;;;; to hash table

(xht--describe
  "Functions that apply two functions to every key–value pair of a hash table
and write the results to a hash table.

For example, if you want to produce a fresh table whose keys
\(currently strings) are upcased and the values \(currently
integers) are increased by 1, you could run:

#+begin_src emacs-lisp
  (h-hmap (lambda (key _value) (upcase key))
          (lambda (_key value) (1+ value))
          table)
#+end_src

The underlines above are so that there's no warnings about unused
variables. We do it because we didn't use VALUE in KEY-FUN, nor KEY
in VAL-FUN — but this doesn't need to be so: we could want that the
new key be also (or instead) a function of the VALUE, and
vice-versa.

Note that the pair is only added if KEY-FUN returns non-nil. So no
spurious (nil, something) pair results from some reasonable
conditional KEY-FUN such as:

#+begin_src emacs-lisp
  (lambda (key, value) (when (> value 2) key))
#+end_src

In the rare case where for some reason you chose to have nil as a
key, you'll have to treat it specially.

IMPORTANT: unlike mapping to a list, mapping to a hash table
demands that the results of the keys be unique. So you must pay
attention to possible collisions. If, for example, in the case
above the original table had both \"a\" and \"A\" as original keys,
one of them would end up overwritten, because both return \"A\"
when upcased. Which one will depend on the key order. So:

  actual size of resulting table  ≤  size of original table.

In mathematical terms, excepting the rare case of a nil key in the
original, the number of keys in the resulting hash table will match
the number of keys in the original one if, and only if:

1. there's a bijection between the set of keys and the set of
   key-fun(key,value).

2. there's no (key,value) for which key-fun(key,value) returns nil.


Guideline for the 'hmap family' of functions:

- the added 'h' means 'result is a hash table'

-  an added '-' means 'anaphoric: enter 2 forms, not 2 lambdas'

-  an added '*' means 'recurse: apply the values-function or -form
                       to the values whenever these are hash tables'

-  an added '!' means 'destructive: modify TABLE instead of
                       creating a fresh one'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return fresh table
|-------------------------+---------------+---------------------|
|                         | 2 lambdas     | 2 forms (anaphoric) |
|-------------------------+---------------+---------------------|
| simple, doesn't recurse | ‘h-hmap’      | ‘h--hmap’           |
| nesting-aware, recurses | ‘h-hmap*’     | ‘h--hmap*’          |
|-------------------------+---------------+---------------------|

Destructive: modify original TABLE, return nil
|-------------------------+---------------+---------------------|
|                         | 2 lambdas     | 2 forms (anaphoric) |
|-------------------------+---------------+---------------------|
| simple, doesn't recurse | ‘h-hmap!’     | ‘h--hmap!’          |
| nesting-aware, recurses | ‘h-hmap*!’    | ‘h--hmap*!’         |
|-------------------------+---------------+---------------------|

See also: ‘h-2d-hmap’, ‘h--2d-hmap’, ‘h-2d-hmap!’, ‘h--2d-hmap!’")

;;;;;;;; non-recursive

(defun h-hmap (key-fun val-fun table)
  "Return a table from applying KEY-FUN, VAL-FUN to each pair of TABLE.
While ‘h-lmap’ produces a list, this one produces a hash table.

Both KEY-FUN and VAL-FUN are each called with two arguments, KEY
and VALUE.
- The result of KEY-FUN is the new key.
- The result of VAL-FUN is the new value.

Its anaphoric counterpart is ‘h--hmap’."

  (declare (pure t) (side-effect-free t))
  (let ((result (h-clr table)))
    (maphash
     (lambda (key value)
       (let ((newk (funcall key-fun key value)))
         (when newk
           (puthash newk
                    (funcall val-fun key value)
                    result))))
     table)
    result))

(defmacro h--hmap (key-form val-form table)
  "Anaphoric version of ‘h-hmap’.

The form KEY-FORM modifies each key.
The form VAL-FORM modifies each value.

For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

While ‘h--lkeep’ produces a list, this one produces a hash table."
  `(h-hmap (lambda (key value) (ignore key value) ,key-form)
           (lambda (key value) (ignore key value) ,val-form)
           ,table))

(defun h-hmap! (key-fun val-fun table)
  "Update TABLE by applying KEY-FUN, VAL-FUN to its pairs.
Just like ‘h-hmap’ (which see), but modifies TABLE instead of
producing a fresh one.

Its anaphoric counterpart is ‘h--hmap!’."
  (maphash
   (lambda (key value)
     (let ((newk (funcall key-fun key value)))
       (remhash key table)
       (when newk
         (puthash newk
                  (funcall val-fun key value)
                  table))))
   table))

(defmacro h--hmap! (key-form val-form table)
  "Anaphoric version of ‘h-hmap!’.
For every key–value pair in TABLE, evaluate KEY-FORM and VAL-FORM
with the variables key and value bound."
  `(h-hmap! (lambda (key value) (ignore key value) ,key-form)
            (lambda (key value) (ignore key value) ,val-form)
            ,table))

;;;;;;;; recursive

(defun h-hmap* (key-fun val-fun table)
  "Return a table from applying KEY-FUN, VAL-FUN to each pair of TABLE.
Just like ‘h-hmap’, but recurses wherever TABLE is nested.

This means that for any VALUE that is itself a hash table, instead
of replacing it with the result of VAL-FUN, as ‘h-hmap’ would
do, VALUE will be this very ‘h-hmap*’ recursively applied to
that hash table with KEY-FUN and VAL-FUN.

Its anaphoric counterpart is ‘h--hmap*’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-clr table)))
    (maphash
     (lambda (key value)
       (let ((newk (funcall key-fun key value)))
         (when newk
           (puthash newk
                    (if (ht? value)
                        (h-hmap* key-fun val-fun value)
                      ;; ^ recurses
                      (funcall val-fun key value))
                    result))))
     table)
    result))

(defmacro h--hmap* (key-form val-form table)
  "Anaphoric version of ‘h-hmap*’.
For every key–value pair in TABLE, evaluate KEY-FORM and VAL-FORM
with the variables key and value bound."
  `(h-hmap* (lambda (key value) (ignore key value) ,key-form)
            (lambda (key value) (ignore key value) ,val-form)
            ,table))

(defun h-hmap*! (key-fun val-fun table)
  "Update TABLE by applying KEY-FUN, VAL-FUN to each of its pairs.
Just like ‘h-hmap*’ (which see), but modifies TABLE instead of
producing a fresh one.

Its anaphoric counterpart is ‘h--hmap*!’."
  (maphash
   (lambda (key value)
     (let ((newk (funcall key-fun key value)))
       (remhash key table)
       (when newk
         (puthash newk
                  (if (ht? value)
                      (h-hmap* key-fun val-fun value)
                    (funcall val-fun key value))
                  table))))
   table))

(defmacro h--hmap*! (key-form val-form table)
  "Anaphoric version of ‘h-hmap*!’.
For every key–value pair in TABLE, evaluate KEY-FORM and VAL-FORM
with the variables key and value bound."
  `(h-hmap*! (lambda (key value) (ignore key value) ,key-form)
             (lambda (key value) (ignore key value) ,val-form)
             ,table))

;;;;;;; for side-effects only

(xht--describe
  "Functions that map over the hash table for producing side-effects
somewhere.")

(defun h-each (table fun)
  "Apply function FUN to each key–value pair of TABLE.
FUN is called with two arguments: key and value.

Intended to be used for side-effects only. Returns nil.

This function is similar to current ‘ht-each’ (in turn an alias to
maphash’), but with TABLE as the first argument.

Its anaphoric counterpart is ‘h--each’."
  (declare (indent 1))
  (maphash fun table))

(defmacro h--each (table &rest body)
  "Anaphoric version of ‘h-each’.
For every key–value pair in TABLE, evaluate BODY with the variables
key and value bound.

This function is similar to current ‘ht-aeach’. Differences:

1. TABLE is the first argument.

2. BODY instead of FORM, obviating the need of ‘progn’ whenever
   more than one form is needed.

3. issues no warnings if either KEY or VALUE isn't used."
  (declare (debug (sexp body)) (indent 1))
  `(maphash (lambda (key value)
              (ignore key value)
              ,@body)
            ,table))

;;;;;; Keys operations
;;;;;;; Retrieval (getting)

(xht--describe
  "Functions that retrieve key–value pairs from tables.")

;;;;;;;; Return value given key

(xht--describe
  "Currently, there are no functions here.
- ‘h-get’  is aliased to ‘ht-get’.
- ‘h-get*’ is aliased to ‘ht-get*’.")

;;;;;;;; Return the very first pair (pop)

(defun h-pop (table)
  "Return the very first pair from TABLE.
Pair is returned as a two-element list: '(key value).

Note that this function is not destructive: it won't remove the
pair from TABLE. For that, use ‘h-pop!’."
  (catch 'break
    (maphash
     (lambda (key value)
       (throw 'break (list key value)))
     table)))

(defun h-pop! (table)
  "Return the very first pair from TABLE and remove it.
Pair is returned as a two-element list: '(key value).

If you don't want it removed, use ‘h-pop’ instead."
  (let ((pop (h-pop table)))
    (h-rem! table (car pop))
    pop))

;;;;;;;; Return a random pair

(defun h-pop-random (table)
  "Return a pseudo-random pair from TABLE.
Pair is returned as a two-element list: '(key value).

Note that this function is not destructive: it won't remove the
pair from TABLE. For that, use ‘h-pop-random!’.

See also: ‘h-vrandom’ and ‘h-lrandom’."
  (let ((goal (random (h-size table)))
        (idx  0))
    (catch 'break
      (maphash
       (lambda (key value)
         (when (= idx goal)
           (throw 'break (list key value)))
         (setq idx (1+ idx)))
       table))))

(defun h-pop-random! (table)
  "Return a pseudo-random pair from TABLE and remove it.
Pair is returned as a two-element list: '(key value).

If you don't want it removed, use ‘h-pop-random’.

See also: ‘h-vrandom’ and ‘h-lrandom’."
  (let ((pop (h-pop-random table)))
    (h-rem! table (car pop))
    pop))

;;;;;;;; Return the first pair matching a predicate

(defun h-first (fun table)
  "Return the first pair from TABLE for which FUN returns non-nil.
In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

Function FUN is called with two arguments, key and value.

This function is exactly like current ‘ht-find’."
  (catch 'break
    (maphash
     (lambda (key value)
       (when (funcall fun key value)
         (throw 'break (list key value))))
     table)))

(defmacro h--first (form table)
  "Anaphoric version of ‘h-first’.
Return the first pair from TABLE for which FORM returns non-nil.
In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

FORM is called with two arguments: key and value."
  `(catch 'break
     (maphash
      (lambda (key value)
        (ignore key value)
        (when ,form
          (throw 'break (list key value))))
      ,table)))

(defun h-first! (fun table)
  "Remove from TABLE the first pair for which FUN returns non-nil.
In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

Function FUN is called with two arguments, key and value."
  (let ((first (h-first fun table)))
    (h-rem! table (car first))
    first))

(defmacro h--first! (form table)
  "Anaphoric version of ‘h-first!’.
Remove from TABLE the first pair for which FORM returns non-nil.
In this case, pair is returned as a two-element list: '(key value).
Return nil otherwise.

FORM is called with two arguments, key and value."
  (let ((f (make-symbol "first")))
    `(let ((,f (h--first ,form ,table)))
       (h-rem! ,table (car ,f))
       ,f)))

;;;;;;; Addition (setting)

(xht--describe
  "Functions that add a key–value pair to tables, either destructively or
side-effects-free. Included are those that take a sequence of keys
and a final value — for nested hash tables.

Same logic applies to put functions as with mix functions — only
that it's table + key–value instead of table + tables; and only one
kv pair makes sense \(otherwise it'd be either a hash table with
the pairs, to which we'd apply mixing; or some alist or plist,
which we'd convert to htbl then mix).

It may seem a bit odd to \"put a pair non-destructively\", but the
logic is the same as that of h-mix, and having an exactly analogous
abstraction is useful.")

;;;;;;;; put

(xht--describe "Summary:
|-------------------------+-----------------+-------------|
|                         | Non-destructive | Destructive |
|-------------------------+-----------------+-------------|
| simple, doesn't recurse | ‘h-put’         | ‘h-put!’    |
| nesting-aware, recurses | ‘h-put*’        | ‘h-put*!’   |
|-------------------------+-----------------+-------------|

While the destructive ‘h-put!’ is like ‘ht-set!’, there's no non-destructive
counterparts in ht library.")


(define-inline h-put! (table key value)
  "Associate KEY in TABLE with VALUE.
This is EXACTLY equivalent to current ‘ht-set!’.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-put’.

This function is not nesting-aware.
Its also-destructive but nesting-aware counterpart is ‘h-put*!’.

For nesting-awareness with no side-effects, use ‘h-put*’."
  (inline-quote
   (ignore
    (puthash ,key ,value ,table))))

(defun h-put (table key value)
  "Return a table that is TABLE with KEY–VALUE applied.
This is a side-effect-free function.
Its destructive counterpart is ‘h-put!’.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is ‘h-put*’.

For nesting-awareness with side-effects, use ‘h-put*!’."
  (declare (pure t) (side-effect-free t))
  (let ((htbl (h-clone* table)))
    (h-put! htbl key value)
    htbl))

(defun h-put*! (table &rest keys-value)
  "Set VALUE to KEYS sequence in hash table TABLE.
KEYS-VALUE are KEYS and VALUE.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

This last key will be set to VALUE.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-put*’.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is ‘h-put!’.

For no nesting-awareness and no side-effects, use ‘h-put’."
  (let ((newh (apply #'h-reduce-r keys-value)))
    (h-mix*! table newh)))

(defun h-put* (table &rest keys-value)
  "Return a table that is TABLE with KEYS–VALUE applied.
KEYS-VALUE are KEYS and VALUE.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

This last key will be set to VALUE.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This is a side-effect-free function.
Its destructive counterpart is ‘h-put*!’.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is ‘h-put’.

For no nesting-awareness but with side-effects, use ‘h-put!’."
  (declare (pure t) (side-effect-free t))
  (let ((htbl (h-clone* table))
        (newh (apply #'h-reduce-r keys-value)))
    (h-mix* htbl newh)))

;;;;;;;; put-add

(xht--describe
  "While 'put' replaces any current value (call it 'curval')
with the newly provided value, 'put-add' adds to it.

The idea is that you are adding to a set. So:

| Current value  | New value                                          |
|----------------+----------------------------------------------------|
| #s(hash table) | - if VALUE is also a hash table,                   |
|                |   then (‘h-mix*!’ VALUE CURVAL);                   |
|                | - if VALUE is an explicit key–value structure, try |
|                |   to convert it to hash table, then ‘h-mix*!’;     |
|                | - otherwise, don't add: keep CURVAL intact.        |
|                |                                                    |
|                | By 'explicit' I mean: no implicit ones, such as    |
|                | simple list, vector, or string lines.              |
|                |                                                    |
|                | And for simplicity, 'key–value lines' string       |
|                | is also out.                                       |
|----------------+----------------------------------------------------|
| [some vector]  |  [VALUE some vector]                               |
| '(some list)   | '(VALUE some list)                                 |
| nil            |   VALUE                                            |

With anything else, make a list, adding VALUE to its first place.
Example:

| Current value | New value            |
|---------------+----------------------|
| 42            | '(VALUE 42)          |
| :keyword      | '(VALUE :keyword)    |
| 'symbol       | '(VALUE symbol)      |
| \"string\"    | '(VALUE \"string\")  |
| etc.          |                      |

Summary:
|-------------------------+-----------------+---------------|
|                         | Non-destructive | Destructive   |
|-------------------------+-----------------+---------------|
| simple, doesn't recurse | ‘h-put-add’     | ‘h-put-add!’  |
| nesting-aware, recurses | ‘h-put-add*’    | ‘h-put-add*!’ |
|-------------------------+-----------------+---------------|")

(defun h-put-add! (table key value)
  "Add VALUE to the current value of KEY in hash table TABLE.
While ‘h-put!’ replaces any current value (call it 'curval')
with the newly provided value, ‘h-put-add!’ adds to it.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-put-add’.

This function is not nesting-aware.
Its also-destructive but nesting-aware counterpart is ‘h-put-add*!’.

For nesting-awareness with no side-effects, use ‘h-put-add*’."
  (let* ((curval (ht-get table key))
         (newval (xht--put-add curval value)))
    (h-put! table key newval)))

(defun h-put-add (table key value)
  "Return a table that is TABLE with VALUE added to KEY's current.
While ‘h-put’ replaces any current value with the newly provided
value, ‘h-put-add’ adds to it.

This is a side-effect-free function.
Its destructive counterpart is ‘h-put-add!’.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is ‘h-put-add*’.

For nesting-awareness with side-effects, use ‘h-put-add*!’."
  (declare (pure t) (side-effect-free t))
  (let* ((htbl   (h-clone* table))
         (curval (ht-get table key))
         (newval (xht--put-add curval value)))
    (h-put htbl key newval)))

(defun h-put-add*! (table &rest keys-value)
  "Add VALUE to the current KEYS sequence's value in hash table TABLE.
KEYS-VALUE are KEYS and VALUE.

While ‘h-put*!’ replaces any current value with the newly provided
value, ‘h-put-add*!’ adds to it.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-put-add*’.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is ‘h-put-add!’.

For no nesting-awareness and no side-effects, use ‘h-put-add’."
  (let* ((keys   (-butlast   keys-value))
         (value  (-last-item keys-value))
         (curval (apply #'ht-get* table keys))
         (newval (xht--put-add curval value)))
    (apply #'h-put*! table (-snoc keys newval))))

(defun h-put-add* (table &rest keys-value)
  "Return a table that is TABLE with VALUE added to KEYS' current.
KEYS-VALUE are KEYS and VALUE.

While ‘h-put*’ replaces any current value with the newly provided
value, ‘h-put-add*’ adds to it.

For more information, please see ‘h-put-add!’.

If any of the keys in the sequence KEYS can't be found,
they will be created on-the-fly.

This is a side-effect-free function.
Its destructive counterpart is ‘h-put-add*!’.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is ‘h-put-add’.

For no nesting-awareness but with side-effects, use ‘h-put-add!’."
  (declare (pure t) (side-effect-free t))
  (let* ((htbl   (h-clone* table))
         (keys   (-butlast   keys-value))
         (value  (-last-item keys-value))
         (curval (apply #'ht-get* table keys))
         (newval (xht--put-add curval value)))
    (apply #'h-put* htbl (-snoc keys newval))))

(defun xht--put-add (curval value)
  "Try to add VALUE to CURVAL depending on types.
Helper to ‘h-put-add’ functions, which see."
  (declare (pure t) (side-effect-free t))
  (let ((good    (list :ht  :lol  :alist  :plist
                       :cons-pair :orgtbl
                       :json  :tsv  :csv  :ssv))
        (no-good (list :vector :list
                       :kvl    :lines
                       :empty  :null)))
    (cond
     ((ht?     curval) (let ((type (h-type value)))
                         (cond ((memq type    good) (h-mix curval
                                                           (h<-it value)))
                               ((memq type no-good) curval)
                               ;; What else could it be?
                               ;; Well, we'll keep curval.
                               (t                   curval))))
     ((null    curval) value)
     ((vectorp curval) (->> (append curval nil)  (cons value)  vconcat))
     ((listp   curval) (cons value curval))
     (t                (list value curval)))))

;;;;;;; Removal

(xht--describe
  "Functions that remove a key–value pair from tables, either destructively
or side-effects-free. Included are those that take a sequence of keys — for
nested hash tables.")

;;;;;;;; rem

(xht--describe "Summary:
|-------------------------+-----------------+-------------|
|                         | Non-destructive | Destructive |
|-------------------------+-----------------+-------------|
| simple, doesn't recurse | ‘h-rem’         | ‘h-rem!’    |
| nesting-aware, recurses | ‘h-rem*’        | ‘h-rem*!’   |
|-------------------------+-----------------+-------------|")

(define-inline h-rem! (table key)
  "Remove KEY from hash table TABLE.
This is EXACTLY equivalent to current ‘ht-remove!’.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-rem’.

This function is not nesting-aware.
Its also-destructive but nesting-aware counterpart is ‘h-rem*!’.

For nesting-awareness with no side-effects, use ‘h-rem*’."
  (inline-quote
   (remhash ,key ,table)))

(defun h-rem (table key)
  "Return a table that is TABLE with KEY removed.
This is a side-effect-free function.
Its destructive counterpart is ‘h-rem!’.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is ‘h-rem*’.

For nesting-awareness with side-effects, use ‘h-rem*!’."
  (declare (pure t) (side-effect-free t))
  (let ((htbl (h-clone* table)))
    (h-rem! htbl key)
    htbl))

(defun h-rem*! (table &rest keys)
  "Remove key at end of KEYS sequence from hash table TABLE.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

The last will be removed, together with the value associated with it.

If any of the keys in the sequence KEYS can't be found,
no action is taken.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-rem*’.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is ‘h-rem!’.

For no nesting-awareness and no side-effects, use ‘h-rem’."
  (let* ((keys-b (-butlast   keys))
         (key-v  (-last-item keys))
         (key-k  (with-demoted-errors
                     (apply #'ht-get* table keys-b))))
    ;; If last key in KEYS is a key of the hash table found by ht-getting* the
    ;; penultimate key, so remove it; otherwise it doesn't exist, so it can't
    ;; be removed
    (and (ht?           key-k)
         (ht-contains?  key-k key-v) ;; (memq key-v (ht-keys key-k))
         (remhash key-v key-k))))

(defun h-rem* (table &rest keys)
  "Return a table that is TABLE with key at end of KEYS removed.

Each key in KEYS has as value the next, all of which hash tables,
except for the final key, which could return any value.

The last will be removed, together with the value associated with it.

If any of the keys in the sequence KEYS can't be found,
no action is taken.

This is a side-effect-free function.
Its destructive counterpart is ‘h-rem*!’.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is ‘h-rem’.

For no nesting-awareness but with side-effects, use ‘h-rem!’."
  (declare (pure t) (side-effect-free t))
  (let* ((htbl   (h-clone* table))
         (keys-b (-butlast   keys))
         (key-v  (-last-item keys))
         (key-k  (with-demoted-errors
                     (apply #'ht-get* htbl keys-b))))
    ;; If last key in KEYS is a key of the hash table found by ht-getting* the
    ;; penultimate key, so remove it; otherwise it doesn't exist, so it can't
    ;; be removed, so return the table as is:
    (and (ht? key-k)
         (memq key-v (ht-keys key-k))
         (remhash key-v key-k))
    htbl))

;;;;;;; Selection

(xht--describe
  "Functions that create a hash table that has only the specified keys.")

;;;;;;;; sel-keys

(xht--describe "Summary:
|-------------------------+-----------------+---------------|
|                         | Non-destructive | Destructive   |
|-------------------------+-----------------+---------------|
| simple, doesn't recurse | ‘h-sel-keys’    | ‘h-sel-keys!’ |
|-------------------------+-----------------+---------------|")

(defun h-sel-keys (table keys)
  "Return a copy of TABLE with only the pairs specified by KEYS.
KEYS is a list.

This function is similar to current ‘ht-select-keys’. The main
difference is that the copy's size is equal to TABLE's instead of
the default 65. All properties are preserved.

This is a side-effect-free function.
Its destructive counterpart is ‘h-sel-keys!’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-clr table)))
    (dolist (key keys result)
      (unless (equal 'key-not-found
                     (gethash key table 'key-not-found))
        (puthash key (gethash key table) result)))))

(defun h-sel-keys! (table keys)
  "Update TABLE to contain only the pairs specified by KEYS.
KEYS is a list.

This is a destructive function.
Its side-effect-free counterpart is ‘h-sel-keys’."
  (maphash
   (lambda (key _value)
     (unless (member key keys)
       (remhash key table)))
   table))

;;;;;;;; sel

(xht--describe
  "Guideline for the 'sel family' of functions:

- 'sel' means 'select: a nil in the result of FUN excludes KEY
               from the results.'

- an added '-' means 'anaphoric: enter a form, not a lambda'

- an added '*' means 'recurse: apply the values-function or -form
                      to the values whenever these are hash tables'

- an added '!' means 'destructive: modify TABLE instead of
                      creating a fresh one'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a fresh table
|-------------------------+-------------+---------------------|
|                         | a lambda    | a form (anaphoric)  |
|-------------------------+-------------+---------------------|
| simple, doesn't recurse | ‘h-sel’     | ‘h--sel’            |
| nesting-aware, recurses | ‘h-sel*’    | ‘h--sel*’           |
|-------------------------+-------------+---------------------|

Destructive: modify original TABLE, return nil
|-------------------------+-------------+---------------------|
|                         | a lambda    | a form (anaphoric)  |
|-------------------------+-------------+---------------------|
| simple, doesn't recurse | ‘h-sel!’    | ‘h--sel!’           |
| nesting-aware, recurses | ‘h-sel*!’   | ‘h--sel*!’          |
|-------------------------+-------------+---------------------|")

(defmacro h-sel (fun table)
  "Return a table with pairs from TABLE for which FUN returns non-nil.
Function FUN is called with two arguments, key and value.

This function is similar to current ‘ht-select’.

This is a side-effect-free function.
Its destructive counterpart is ‘h-sel!’.
Its anaphoric counterpart is ‘h--sel’."
  `(h--hmap (when (funcall ,fun key value) key)
            value ,table))

(defmacro h--sel (form table)
  "Anaphoric version of ‘h-sel’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is ‘h--sel!’."
  `(h--hmap (when ,form key)
            value ,table))

(defmacro h-sel! (fun table)
  "Update TABLE by keeping only pairs for which FUN returns non-nil.
Function FUN is called with two arguments, key and value.

This is a destructive function.
Its side-effect-free counterpart is ‘h-sel’.
Its anaphoric counterpart is ‘h--sel!’."
  `(h--hmap! (when (funcall ,fun key value) key)
             value ,table))

(defmacro h--sel! (form table)
  "Anaphoric version of ‘h-sel!’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is ‘h--sel’."
  `(h--hmap! (when ,form key)
             value ,table))

(defun h-sel* (fun table)
  "Return a table with pairs from TABLE for which FUN returns non-nil.
Function FUN is called with two arguments, key and value.

Just like ‘h-sel’, but recurses wherever TABLE is nested.

This means that for any VALUE that is itself a hash table, instead
of returning it as it is when FUNCTION returns non-nil, as ‘h-sel
would do, VALUE will be the very ‘h-sel*’ recursively applied to
that hash table with the same FUN.

This is a side-effect-free function.
Its destructive counterpart is ‘h-sel*!’.
Its anaphoric counterpart is ‘h--sel*’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-clr table)))
    (maphash
     (lambda (key value)
       (when (funcall fun key value)
         (h-put! result key (if (ht? value)
                                (h-sel* fun value)
                              ;; ^ recurses
                              value))))
     table)
    result))

(defmacro h--sel* (form table)
  "Anaphoric version of ‘h-sel*’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is ‘h--sel*!’."
  `(h-sel* (lambda (key value) (ignore key value) ,form)
           ,table))

(defun h-sel*! (fun table)
  "Update TABLE by keeping only pairs for which FUN returns non-nil.
Function FUN is called with two arguments, key and value.

This is a destructive function.
Its side-effect-free counterpart is ‘h-sel*’.
Its anaphoric counterpart is ‘h--sel*!’."
  (maphash
   (lambda (key value)
     (if (funcall fun key value)
         (when (ht? value)
           (h-put! table key
                   ;; recurses:
                   (h-sel* fun value)))
       (h-rem! table key)))
   table))

(defmacro h--sel*! (form table)
  "Anaphoric version of ‘h-sel*!’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is ‘h--sel*’."
  `(h-sel*! (lambda (key value) (ignore key value) ,form)
            ,table))

;;;;;;; Rejection

(xht--describe
  "Functions that create a hash table that has all but the specified keys.")

;;;;;;;; rej-keys

(xht--describe "Summary:
|-------------------------+-----------------+---------------|
|                         | Non-destructive | Destructive   |
|-------------------------+-----------------+---------------|
| simple, doesn't recurse | ‘h-rej-keys’    | ‘h-rej-keys!’ |
|-------------------------+-----------------+---------------|")

(defun h-rej-keys (table keys)
  "Return a copy of TABLE with all but the pairs specified by KEYS.
KEYS is a list.

This is a side-effect-free function.
Its destructive counterpart is ‘h-rej-keys!’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-clr table)))
    (maphash
     (lambda (key _value)
       (unless (member key keys)
         (puthash key (gethash key table) result)))
     table)
    result))

(defun h-rej-keys! (table keys)
  "Update TABLE by removing the pairs specified by KEYS.
KEYS is a list.

This is a destructive function.
Its side-effect-free counterpart is ‘h-rej-keys’."
  (dolist (key keys)
    (remhash key table)))

;;;;;;;; rej

(xht--describe
  "Guideline for the 'rej family' of functions:

- 'rej' means 'reject: non-nil in the result of FUN excludes KEY
               from the results.'

- an added '-' means 'anaphoric: enter a form, not a lambda'

- an added '*' means 'recurse: apply the values-function or -form
                      to the values whenever these are hash tables'

- an added '!' means 'destructive: modify TABLE instead of
                      creating a fresh one'

In the anaphoric versions, if you don't use any or either of the
let-bound variables key and value in any or either of the forms,
it's ok — no warnings.

Summary:

Side-effect-free: don't modify original TABLE, return a fresh table
|-------------------------+-------------+---------------------|
|                         | a lambda    | a form (anaphoric)  |
|-------------------------+-------------+---------------------|
| simple, doesn't recurse | ‘h-rej’     | ‘h--rej’            |
| nesting-aware, recurses | ‘h-rej*’    | ‘h--rej*’           |
|-------------------------+-------------+---------------------|

Destructive: modify original TABLE, return nil
|-------------------------+-------------+---------------------|
|                         | a lambda    | a form (anaphoric)  |
|-------------------------+-------------+---------------------|
| simple, doesn't recurse | ‘h-rej!’    | ‘h--rej!’           |
| nesting-aware, recurses | ‘h-rej*!’   | ‘h--rej*!’          |
|-------------------------+-------------+---------------------|")

(defmacro h-rej (fun table)
  "Return a table with pairs from TABLE for which FUN returns nil.
Function FUN is called with two arguments, key and value.

This function is similar to current ‘ht-reject’.

This is a side-effect-free function.
Its destructive counterpart is ‘h-rej!’.
Its anaphoric counterpart is ‘h--rej’."
  `(h--hmap (unless (funcall ,fun key value) key)
            value ,table))

(defmacro h--rej (form table)
  "Anaphoric version of ‘h-rej’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is ‘h--rej!’."
  `(h--hmap (unless ,form key)
            value ,table))

(defmacro h-rej! (fun table)
  "Update TABLE by keeping only pairs for which FUN returns nil.
Function FUN is called with two arguments, key and value.

This function is similar to current ‘ht-reject!’.

This is a destructive function.
Its side-effect-free counterpart is ‘h-rej’.
Its anaphoric counterpart is ‘h--rej!’."
  `(h--hmap! (unless (funcall ,fun key value) key)
             value ,table))

(defmacro h--rej! (form table)
  "Anaphoric version of ‘h-rej!’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is ‘h--rej’."
  `(h--hmap! (unless ,form key)
             value ,table))

(defun h-rej* (fun table)
  "Return a table with pairs from TABLE for which FUN returns nil.
Function FUN is called with two arguments, key and value.

Just like ‘h-rej’, but recurses wherever TABLE is nested.

This means that for any VALUE that is itself a hash table, instead
of returning it as it is when FUN returns nil, as ‘h-rej’ would do,
VALUE will be the very ‘h-rej*’ recursively applied to that hash
table with the same FUN.

This is a side-effect-free function.
Its destructive counterpart is ‘h-rej*!’.
Its anaphoric counterpart is ‘h--rej*’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-clr table)))
    (maphash
     (lambda (key value)
       (unless (funcall fun key value)
         (h-put! result key (if (ht? value)
                                (h-rej* fun value)
                              ;; ^ recurses
                              value))))
     table)
    result))

(defmacro h--rej* (form table)
  "Anaphoric version of ‘h-rej*’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a side-effect-free function.
Its destructive counterpart is ‘h--rej*!’."
  `(h-rej* (lambda (key value) (ignore key value) ,form)
           ,table))

(defun h-rej*! (fun table)
  "Update TABLE by keeping only pairs for which FUN returns nil.
Function FUN is called with two arguments, key and value.

This is a destructive function.
Its side-effect-free counterpart is ‘h-rej*’.
Its anaphoric counterpart is ‘h--rej*!’."
  (maphash
   (lambda (key value)
     (if (funcall fun key value)
         (h-rem! table key)
       (when (ht? value)
         (h-put! table key
                 ;; recurses:
                 (h-rej* fun value)))))
   table))

(defmacro h--rej*! (form table)
  "Anaphoric version of ‘h-rej*!’.
For every key–value pair in TABLE, evaluate FORM with the variables
key and value bound.

This is a destructive function.
Its side-effect-free counterpart is ‘h--rej*’."
  `(h-rej*! (lambda (key value) (ignore key value) ,form)
            ,table))

;;;;;;; Reverse order

(xht--describe
  "Functions to reverse the order in which the items are stored in
the table.

Summary:
|-------------------------+-----------------|---------------+
|                         | non-destructive | destructive   |
|-------------------------+-----------------|---------------+
| simple, doesn't recurse | ‘h-reverse’     | ‘h-reverse!’  |
| nesting-aware, recurses | ‘h-reverse*’    | ‘h-reverse*!’ |
|-------------------------+-----------------|---------------+")

(defun h-reverse (table)
  "Create a hash table identical to TABLE with key order reversed."
  (let ((r (h-clone* table)))
    (xht--reverse*! r nil)
    r))

(defun h-reverse! (table)
  "Reverse the key order of hash table TABLE."
  (xht--reverse*! table nil))

(defun h-reverse* (table)
  "Create a hash table identical to TABLE with key order reversed.
When TABLE is nested, do the same to all values that are hash
tables."
  (let ((r (h-clone* table)))
    (xht--reverse*! r 'recurse)
    r))

(defun h-reverse*! (table)
  "Reverse the key order of hash table TABLE.
When TABLE is nested, do the same to all values that are hash
tables."
  (xht--reverse*! table 'recurse))

(defun xht--reverse*! (table recurse)
  "Reverse the key order of hash table TABLE.
If RECURSE is non-nil, recurse.

Helper function for:
h-reverse!’, ‘h-reverse’, ‘h-reverse*!’, and ‘h-reverse*’."
  (let ((items (h-vitems table 'reverse))
        (size  (h-size table))
        (idx   0))
    (h-clr! table)
    (while (< idx size)
      (-let [(key value) (aref items idx)]
        (and recurse
             (h? value)
             (xht--reverse*! value recurse))
        (h-put! table key value)
        (setq idx (1+ idx))))))

;;;;;;; Change numerical value, including #'1+ and #'1-

(xht--describe
  "Functions to alter values of keys when these values are numeric or
number-like.

Summary:
|               | Non-destructive   | Destructive        |
|---------------+-------------------+--------------------|
| Generic       | ‘h-put-num-with*’ | ‘h-put-num-with*!’ |
| Increase by 1 | ‘h-put-inc*’      | ‘h-put-inc*!’      |
| Decrease by 1 | ‘h-put-dec*’      | ‘h-put-dec*!’      |")

(defun h-put-num-with* (fun table &rest keys)
  "Return copy of TABLE with FUN applied to numeric value of KEYS.
KEYS is a chain of keys to access the key of an internal hash table
if TABLE is nested — just as with ‘h-get*’. If it's a simple,
non-nested TABLE, or if the value is at the 'first level', enter
just one key.

FUN is a unary function that can be applied to numerical values.
These are some examples: ‘1+’, ‘1-’, ‘sqrt’, ‘log10’.

You may also create partial application ones, such as:

#+begin_src emacs-lisp
  (-partial '+ 10)
  (-partial '* 2)
#+end_src

or:

#+begin_src emacs-lisp
  (lambda (x) (expt x 2))
#+end_src

The value must, of course, be a number — but the function is
flexible about what a number is. If it quacks like a number, we
increase it. So it may be found stored also as a string or keyword,
and it will be increased while maintaining the type. Moreover, it
tries to preserve original leading zeros when they were there in
strings, keywords, or symbols.

So \"041\", receiving #'1+, becomes \"042\", and so on.

When value is nil or key is not found, interpret it as being 0.

The function uses base 10.

This is a side-effect-free function.
Its destructive counterpart is ‘h-put-num-with*!’."
  (declare (pure t) (side-effect-free t))
  (let ((htbl (h-clone* table)))
    (apply #'h-put-num-with*! fun htbl keys)
    htbl))

(defun h-put-num-with*! (fun table &rest keys)
  "Apply FUN to the numeric value of KEYS in possibly nested hash TABLE.
This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-put-num-with*’."
  (apply #'h-put*! table
         (-snoc keys (xht--number-with
                      fun
                      (apply #'h-get* table keys)))))

(defun h-put-inc* (table &rest keys)
  "Return a copy of TABLE with 1 added to the value of KEYS.
KEYS is a chain of keys to access the key of an internal hash table
if TABLE is nested — just as with ‘h-get*’. If it's a simple,
non-nested TABLE, or if the value is at the 'first level', enter
just one key.

The value must, of course, be a number — but the function is
flexible about what a number is. If it quacks like a number, we
increase it. So it may be found stored also as a string or keyword,
and it will be increased while maintaining the type. Moreover, it
tries to preserve original leading zeros when they were there in
strings, keywords, or symbols.

So \"041\" becomes \"042\", and so on.

When value is nil or key is not found, interpret it as being 0.

The function uses base 10.

This is a side-effect-free function.
Its destructive counterpart is ‘h-put-inc*!’."
  (declare (pure t) (side-effect-free t))
  (apply #'h-put-num-with* #'1+ table keys))

(defun h-put-inc*! (table &rest keys)
  "Add 1 to the value of KEYS in possibly nested hash TABLE.
See ‘h-put-inc*’ for more information.

This function returns nil."
  (apply #'h-put-num-with*! #'1+ table keys))

(defun h-put-dec* (table &rest keys)
  "Return a copy of TABLE with 1 subtracted from the value of KEYS.
KEYS is a chain of keys to access the key of an internal hash table
if TABLE is nested — just as with ‘h-get*’. If it's a simple,
non-nested TABLE, or if the value is at the 'first level', enter
just one key.

The value must, of course, be a number — but the function is
flexible about what a number is. If it quacks like a number, we
decrease it. So it may be found stored also as a string or keyword,
and it will be increased while maintaining the type. Moreover, it
tries to preserve original leading zeros when they were there in
strings, keywords, or symbols.

So \"043\" becomes \"042\", and so on.

The function uses base 10.

This is a side-effect-free function.
Its destructive counterpart is ‘h-put-dec*!’."
  (declare (pure t) (side-effect-free t))
  (apply #'h-put-num-with* #'1- table keys))

(defun h-put-dec*! (table &rest keys)
  "Subtract 1 from the value of KEYS in possibly nested hash TABLE.
See ‘h-put-dec*’ for more information.

This function returns nil."
  (apply #'h-put-num-with*! #'1- table keys))

;;;;;;;; helper

(defun xht--number-with (fun n)
  "Apply function FUN to number-like creature N.
Whatever quacks like a number should accept it.

Return it in the same type it was found, and preserving original
leading zeros when they were there in strings, keywords, or
symbols.

Interpret nil as 0.

Helper function to ‘xht--number-with-1+’, ‘xht--number-with-1-’."
  (declare (pure t) (side-effect-free t))
  (let* ((err-msg "Doesn't look like a number — nothing done")
         (as-num-maybe (cond
                        ((null     n)                             0)
                        ((numberp  n)                             n)
                        ((stringp  n) (read                       n))
                        ((keywordp n) (read (xht--keyword->string n)))
                        ((symbolp  n) (read (format "%s"          n)))
                        (t (user-error err-msg)))))
    (unless (numberp as-num-maybe)
      (user-error err-msg))
    (let ((res (funcall fun as-num-maybe)))
      (if (or (numberp n) (null n))
          res
        (let* ((num-was (->> n (format "%s") (s-chop-prefix ":")))
               (num-fmt
                (cond
                 ;; Floats: to avoid spurious loss of precision after
                 ;; a series of applications, we won't try to truncate
                 ;; the numbers to the significant digits of the
                 ;; original — you round it when you needed it.
                 ((floatp   res) "%s")
                 ;; Integers: if no leading 0s in the original, we'll
                 ;; just return the results; otherwise we'll check the
                 ;; original's width and set it as a minimum. Note
                 ;; that if the original was stored as integers, no
                 ;; leading 0 would have been possible — this is for
                 ;; strings or keywords etc.
                 ((integerp res) (if (/= ?0 (aref num-was 0))
                                     "%s"
                                   (format "%%0%sd"
                                           (->> num-was  length
                                                (format "%s")))))
                 (t (error "‘xht--number-with’: %s"
                           "neither integer nor float?!")))))
          (--> (format num-fmt res)
               (cond ((stringp  n) it)
                     ((keywordp n) (xht--string->keyword it))
                     ((symbolp  n) (make-symbol it)))))))))

(defsubst xht--number-with-1+ (n)
  "Apply #'1+ to number-like creature N.
Helper function to ‘h-put-inc*’ and ‘h-put-inc*!’."
  (xht--number-with #'1+ n))

(defsubst xht--number-with-1- (n)
  "Apply #'1- to number-like creature N.
Helper function to ‘h-put-dec*’ and ‘h-put-dec*!’."
  (xht--number-with #'1- n))

;;;;;;; Miscellaneous key, value, pair lookups

(xht--describe
  "Other functions about checking keys, values, and pairs.")

;;;;;;;; Has this key, this value, or this pair at some level?

(define-inline h-has-key? (table key)
  "Return t if TABLE has KEY.
This function is just like current ‘ht-contains?’"
  (declare (pure t) (side-effect-free t))
  (inline-quote
   (let ((nf (make-symbol "xht--not-found")))
     (not (eq nf (gethash ,key ,table nf))))))

(defun h-has-key*? (table key)
  "Whether possibly-nested TABLE has KEY at some level.
Similar to ‘h-has-key?’, but if TABLE is nested look for KEY also
in the internal hash tables of TABLE."
  (declare (pure t) (side-effect-free t))
  (if (h-has-key? table key)
      t           ;; recurse:
    (xht--has-x*? #'h-has-key*?
                  table key)))

(define-inline h-has-value? (table value)
  "Return t if TABLE has some key whose value is VALUE."
  (declare (pure t) (side-effect-free t))
  (inline-quote
   (when (h-first (lambda (_k v)
                    (equal v ,value))
                  ,table)
     t)))

(defun h-has-value*? (table value)
  "Whether possibly-nested TABLE has at some level key–VALUE pair.
Similar to ‘h-has-value?’, but if TABLE is nested look for VALUE
also in the internal hash tables of TABLE."
  (declare (pure t) (side-effect-free t))
  (if (h-has-value? table value)
      t           ;; recurse:
    (xht--has-x*? #'h-has-value*?
                  table value)))

(define-inline h-has-pair? (table key value)
  "Return t if TABLE has KEY and its value is VALUE."
  (declare (pure t) (side-effect-free t))
  (inline-quote
   (equal ,value
          (gethash ,key ,table
                   (make-symbol "xht--not-found")))))

(defun h-has-pair*? (table key value)
  "Whether possibly-nested TABLE has at some level KEY–VALUE pair.
Similar to ‘h-has-pair?’, but if TABLE is nested look for KEY–VALUE
pair also in the internal hash tables of TABLE."
  (declare (pure t) (side-effect-free t))
  (if (h-has-pair? table key value)
      t           ;; recurse:
    (xht--has-x*? #'h-has-pair*?
                  table key value)))

(defun xht--has-x*? (fun table &rest x)
  "Generic helper for ‘h-has-key*?’, ‘h-has-value*?’, ‘h-has-pair*?’.
FUN is one of these functions above.
Whether possibly-nested TABLE has at some level X."
  (declare (pure t) (side-effect-free t))
  (when-let ((ht-vals (-filter #'hash-table-p
                               (h-lvalues table 'reverse))))
    (let (found) ;; 'reverse is actually cheaper ^
      (while (and ht-vals (not found))
        (setq found (apply fun (pop ht-vals) x)))
      found)))

;;;;;;;; Has all these keys, these values, or these pairs?

(defun h-has-keys? (table keys)
  "Return t if TABLE has all KEYS.
KEYS is either a single key or a list of keys."
  (declare (pure t) (side-effect-free t))
  (--all? (h-has-key? table it) (-list keys)))

(defun h-has-values? (table values)
  "Return t if TABLE has all VALUES.
VALUES is either a single value or a list of values."
  (declare (pure t) (side-effect-free t))
  (--all? (h-has-value? table it) (-list values)))

(defun h-has-pairs? (table pairs)
  "Return t if TABLE has all PAIRS.
PAIRS is either a single pair (a two-item list) or a list of such
two-item lists."
  (declare (pure t) (side-effect-free t))
  (--all? (h-has-pair? table (car it) (cadr it))
          (if (-all? #'listp pairs)
              pairs
            (if (= 2 (length pairs))
                (list pairs)
              (user-error "Malformed list of pairs")))))

;; I currently see no need for writing (and testing) nestedness-aware versions
;; of these previous three. In any case, it seems to me they could be
;; constructed in the exact same way, just adding the ?*.

;;;;;; Multi-table operations

(xht--describe
  "Functions that act on two or more hash tables simultaneously.

XHT loves short function names.

Sometimes, however, their meaning might not be obvious at first
sight. Thankfully, this is easy to fix — and by third sight they'll
already be familiar to you.

With this table, it'll take you but a minute to figure out the
meaning of the multi-table functions you'll see next:

| Function | Implying...                | Operation    |
|----------+----------------------------+--------------|
| h-mix    | Mixing the tables together | Union        |
| h-dif    | Difference between tables  | Difference   |
| h-cmn    | Commonality between tables | Intersection |

Easy?

Let's see them.")

;;;;;;; Union

(xht--describe
  "Functions that combine tables, either destructively or side-effects-free.

Note that:

- An '*' means it works with nesting; its absence means only first level.

- A  '!' means destructive: it changes the original table; its absence
  means that a new table is created, free of side-effects to the original.

- When input has multiple tables, the first one is 'updated' with the
  others, which are processed from left to right. When not destructive,
  a shallow copy of the first is updated.

  So you can always think of it as: values of table1 updated with those of
  table2; the result updated with those of table3; the result updated with
  those of table4; and so on. The difference is that when there's a '!'
  table1 will be itself modified; otherwise not.

- Functions for side-effect ('!') return nil; otherwise, the (1st) table.")

;;;;;;;; mix

(xht--describe
  "The destructive ‘h-mix!’ is similar to ht library's ‘ht-update!’, whereas
the side-effect-free ‘h-mix’ is similar to ‘ht-merge’.")

(defun h-mix! (table &rest from-tables)
  "Update TABLE according to every key–value pair in FROM-TABLES.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-mix! table1 table2 table3 table4) will:
- modify table1 with the data from table2; then
- modify table1 with the data from table3; then
- modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-mix’.

This function only works at 'the first level' — no nesting.
Its also-destructive nesting-aware counterpart is ‘h-mix*!’.

For nesting-awareness but no side-effects, use ‘h-mix*’."
  (dolist (from-table from-tables)
    (xht--mix! table from-table)))

(defun h-mix (table &rest from-tables)
  "Return a table that has all key–value pairs from the tables.
The tables (TABLE plus FROM-TABLES) are processed from left to
right, and no table is modified.

So (h-mix table1 table2 table3 table4) will:
- create a clone of table1 (let's call it 'table0')
- modify table0 with the data from table2; then
- modify table0 with the data from table3; then
- modify table0 with the data from table4;
- return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is ‘h-mix!’.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is ‘h-mix*’.

For nesting-awareness with side-effects, use ‘h-mix*!’."
  (declare (pure t) (side-effect-free t))
  (let ((mixed (h-clone* table)))
    (dolist (from-table from-tables mixed)
      (h-mix! mixed from-table))))

(defun h-mix*! (table &rest from-tables)
  "Update TABLE by adding every key–value pair in FROM-TABLES.
Unlike ‘h-mix!’ and ‘ht-update!’, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the ‘h-mix*!’ of them.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-mix! table1 table2 table3 table4) will:
- modify table1 with the data from table2; then
- modify table1 with the data from table3; then
- modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-mix*’.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is ‘h-mix!’.

For no nesting-awareness and no side-effects, use ‘h-mix’."
  (dolist (from-table from-tables)
    (xht--mix*! table from-table)))

(defun h-mix* (table &rest from-tables)
  "Return a table that has all key–value pairs from the tables.
Unlike ‘h-mix’ and ‘ht-merge’, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the ‘h-mix*’ of them.

The tables (TABLE plus FROM-TABLES) are processed from left to
right, and no table is modified.

So (h-mix* table1 table2 table3 table4) will:
- create a clone of table1 (let's call it 'table0')
- modify table0 with the data from table2; then
- modify table0 with the data from table3; then
- modify table0 with the data from table4;
- return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is ‘h-mix*!’.

This function is nesting-aware.

Its also-side-effect-free but only-first-level counterpart is ‘h-mix’.

For no nesting-awareness but with side-effects, use ‘h-mix!’."
  (declare (pure t) (side-effect-free t))
  (let ((mixed (h-clone* table)))
    (dolist (from-table from-tables mixed)
      (h-mix*! mixed from-table))))

;;;;;;;;; helper
(defun xht--mix! (table from-table)
  "Update TABLE by adding every key–value pair in FROM-TABLE.
\(Destructive, no nesting, only two tables.)

This function returns nil.

This is EXACTLY equivalent to current ‘ht-update!’. It's used as basis
for ‘h-mix!’, ‘h-mix’, ‘h-mix*!’, and ‘h-mix*’, which see."
  (ignore
   (maphash (lambda (key value)
              (puthash key value table))
            from-table)))

(defun xht--mix*! (table from-table)
  "Update TABLE by adding every key–value pair in FROM-TABLE.
\(Destructive, accepts nesting, only two tables.)

This function returns nil.

Helper function for ‘h-mix*!’, which see."
  (ignore
   (maphash (lambda (key value)
              (puthash key
                       (let ((v-dest (gethash key table)))
                         (if (and (ht? v-dest)
                                  (ht? value))
                             (progn (xht--mix*! v-dest value)
                                    ;; ^ recursive
                                    v-dest)
                           value))
                       table))
            from-table)))

;;;;;;; Difference

(xht--describe
  "Functions that subtract tables, either destructively or side-effects-free.")

;;;;;;;; dif

(defun h-dif! (table &rest from-tables)
  "Update TABLE by removing all matching key–value pairs in FROM-TABLES.

That is: the removal only happens when both the key and the value
in one of the tables in FROM-TABLES matches that found in TABLE.
If only the key is matched, there's no removal.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-dif! table1 table2 table3 table4) will:
- modify table1 with the data from table2; then
- modify table1 with the data from table3; then
- modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-dif’.

This function only works at 'the first level' — no nesting.
Its also-destructive nesting-aware counterpart is ‘h-dif*!’.

For nesting-awareness but no side-effects, use ‘h-dif*’."
  (dolist (from-table from-tables)
    (xht--dif! table from-table)))

(defun h-dif (table &rest from-tables)
  "Return a table of all key–value pairs in TABLE not in FROM-TABLES.

That is: all pairs in TABLE appear in the results, except those that also
appear in one of the tables in FROM-TABLES. If only the key is matched,
there's no removal.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-dif table1 table2 table3 table4) will:
- create a clone of table1 (let's call it 'table0')
- modify table0 with the data from table2; then
- modify table0 with the data from table3; then
- modify table0 with the data from table4;
- return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is ‘h-dif!’.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is ‘h-dif*’.

For nesting-awareness with side-effects, use ‘h-dif*!’."
  (declare (pure t) (side-effect-free t))
  (let ((diffed (h-clone* table)))
    (dolist (from-table from-tables diffed)
      (h-dif! diffed from-table))))

(defun h-dif*! (table &rest from-tables)
  "Update TABLE by removing all matching key–value pairs in FROM-TABLES.

That is: the removal only happens when both the key and the value
in one of the tables in FROM-TABLES matches that found in TABLE.
If only the key is matched, there's no removal.

Unlike ‘h-dif!’, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the ‘h-dif*!’ of them.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-dif! table1 table2 table3 table4) will:
- modify table1 with the data from table2; then
- modify table1 with the data from table3; then
- modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-dif*’.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is ‘h-dif!’.

For no nesting-awareness and no side-effects, use ‘h-dif’."
  (dolist (from-table from-tables)
    (xht--dif*! table from-table)))

(defun h-dif* (table &rest from-tables)
  "Return a table of all key–value pairs in TABLE not in FROM-TABLES.

That is: all pairs in TABLE appear in the results, except those that also
appear in one of the tables in FROM-TABLES. If only the key is matched,
there's no removal.

Unlike ‘h-dif’, it updates nested levels: so if both values are
themselves hash tables, the new value will be the ‘h-dif*’ of them.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-dif* table1 table2 table3 table4) will:
- create a clone of table1 (let's call it 'table0')
- modify table0 with the data from table2; then
- modify table0 with the data from table3; then
- modify table0 with the data from table4;
- return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is ‘h-dif*!’.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is ‘h-dif’.

For no nesting-awareness but with side-effects, use ‘h-dif!’."
  (declare (pure t) (side-effect-free t))
  (let ((diffed (h-clone* table)))
    (dolist (from-table from-tables diffed)
      (h-dif*! diffed from-table))))

;;;;;;;;; helper
(defun xht--dif! (table from-table)
  "Update TABLE by removing every key–value pair in FROM-TABLE.
\(Destructive, no nesting, only two tables.)

This function returns nil.

It's used as basis for ‘h-dif!’, ‘h-dif’, ‘h-dif*!’,
and ‘h-dif*’, which see."
  (ignore
   (maphash (lambda (key value)
              (when (h-it= (gethash key table) value)
                (remhash key table)))
            from-table)))

(defun xht--dif*! (table from-table)
  "Update TABLE by removing every key–value pair in FROM-TABLE.
\(Destructive, accepts nesting, only two tables.)

This function returns nil.

Helper function for ‘h-dif*!’, which see."
  (ignore
   (maphash (lambda (key value)
              (let* ((v-dest (gethash key table)))
                (if (h-it= v-dest value)
                    (remhash key table)
                  (and (ht? v-dest)
                       (ht? value)
                       (puthash key
                                ;; recursive:
                                (progn (xht--dif*! v-dest value)
                                       v-dest)
                                table)))))
            from-table)))

;;;;;;; Intersection

(xht--describe
  "Functions that build a table with the pairs common to all tables, either
destructively or side-effects-free.")

;;;;;;;; cmn

(xht--describe
  "cmn as in: 'common' to all tables.")

(defun h-cmn! (table &rest from-tables)
  "Update TABLE by keeping only the intersection with FROM-TABLES.

That is: the key is kept only when the pair is found in all TABLES.
If only the key is matched, not kept.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-cmn! table1 table2 table3 table4) will:
- modify table1 with the data from table2; then
- modify table1 with the data from table3; then
- modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-cmn’.

This function only works at 'the first level' — no nesting.
Its also-destructive nesting-aware counterpart is ‘h-cmn*!’.

For nesting-awareness but no side-effects, use ‘h-cmn*’."
  (dolist (from-table from-tables)
    (xht--cmn! table from-table)))

(defun h-cmn (table &rest from-tables)
  "Return a table of all key–value pairs in TABLE also in FROM-TABLES.

That is: no pairs in TABLE appear in the results, except those that
appear in all the tables in FROM-TABLES. If only the key is
matched, there's no removal.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-cmn table1 table2 table3 table4) will:
- create a clone of table1 (let's call it 'table0')
- modify table0 with the data from table2; then
- modify table0 with the data from table3; then
- modify table0 with the data from table4;
- return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is ‘h-cmn!’.

This function is not nesting-aware.
Its also-side-effect-free but nesting-aware counterpart is ‘h-cmn*’.

For nesting-awareness with side-effects, use ‘h-cmn*!’."
  (declare (pure t) (side-effect-free t))
  (let ((intersection (h-clone* table)))
    (dolist (from-table from-tables intersection)
      (h-cmn! intersection from-table))))

(defun h-cmn*! (table &rest from-tables)
  "Update TABLE by keeping only the intersection with FROM-TABLES.

That is: the key is kept only when the pair is found in all TABLES.
If only the key is matched, not kept.

Unlike ‘h-cmn!’, it updates nested levels:
so if both values are themselves hash tables, the new value
will be the ‘h-cmn*!’ of them.

FROM-TABLES are processed from left to right, and
only the first table will be modified — not the others.

So (h-cmn! table1 table2 table3 table4) will:
- modify table1 with the data from table2; then
- modify table1 with the data from table3; then
- modify table1 with the data from table4.

This function returns nil.

This is a destructive function.
Its side-effect-free counterpart is ‘h-cmn*’.

This function is nesting-aware.
Its also-destructive but only-first-level counterpart is ‘h-cmn!’.

For no nesting-awareness and no side-effects, use ‘h-cmn’."
  (dolist (from-table from-tables)
    (xht--cmn*! table from-table)))

(defun h-cmn* (table &rest from-tables)
  "Return a table of all key–value pairs in TABLE also in FROM-TABLES.

That is: no pairs in TABLE appear in the results, except those that
appear in all the tables in FROM-TABLES. If only the key is
matched, there's no removal.

Unlike ‘h-cmn’, it updates nested levels: so if both values are
themselves hash tables, the new value will be the ‘h-cmn*’ of them.

FROM-TABLES are processed from left to right, and no table is modified.

So (h-cmn* table1 table2 table3 table4) will:
- create a clone of table1 (let's call it 'table0')
- modify table0 with the data from table2; then
- modify table0 with the data from table3; then
- modify table0 with the data from table4;
- return table0.

Tables 1 to 4 haven't changed.

This is a side-effect-free function.
Its destructive counterpart is ‘h-cmn*!’.

This function is nesting-aware.
Its also-side-effect-free but only-first-level counterpart is ‘h-cmn’.

For no nesting-awareness but with side-effects, use ‘h-cmn!’."
  (declare (pure t) (side-effect-free t))
  (let ((intersection (h-clone* table)))
    (dolist (from-table from-tables intersection)
      (h-cmn*! intersection from-table))))

;;;;;;;;; helper
(defun xht--cmn! (table from-table)
  "Update TABLE by keeping only the intersection with FROM-TABLE.
\(Destructive, no nesting, only two tables.)

This function returns nil.

It's used as basis for ‘h-cmn!’, ‘h-cmn’, ‘h-cmn*!’, and ‘h-cmn*’,
which see."
  ;; Based on the following mathematical equality:
  ;;
  ;; A ∩ B = A - (A - B)
  ;; (h-dif! table
  ;;           (h-dif table
  ;;                  from-table)))
  ;;
  ;; ^ The above is fine, but the below
  ;;   should be faster and replaced it:
  (ignore
   (let ((sentinel (make-symbol "xht--sentinel")))
     (maphash (lambda (key value)
                (let ((v-from (gethash key from-table sentinel)))
                  (when (or (eq v-from sentinel)
                            (not (h-it= value v-from)))
                    (remhash key table))))
              table))))

(defun xht--cmn*! (table from-table)
  "Update TABLE by keeping only the intersection with FROM-TABLE.
\(Destructive, accepts nesting, only two tables.)

This function returns nil.

Helper function for ‘h-cmn*!’, which see."
  (ignore
   (let ((sentinel (make-symbol "xht--sentinel")))
     (maphash (lambda (key value)
                (let ((v-from (gethash key from-table sentinel)))
                  (if (eq v-from sentinel)
                      (remhash key table)
                    (unless (h-it= value v-from)
                      (if (and (ht? v-from)
                               (ht? value))
                          (puthash key
                                   ;; recursive:
                                   (progn (xht--cmn*! value v-from)
                                          value)
                                   table)
                        (remhash key table))))))
              table))))

;;;;;;; Comparison / Equality / Equivalence

;;;;;;;; 1D: implicit    (Vector, List, Lines) (indices as keys)

(xht--describe
  "Not needed.

Vectors, lists, and strings can be compared with ‘equal’.
Examples:

#+begin_src emacs-lisp
  (equal '(a x) '(a x))      ;=> t
  (equal [a x] [a x])        ;=> t
  (equal \"a\\nx\\n\" \"a\\nx\\n\")  ;=> t
#+end_src

And they can only be equal to exactly themselves.

Also note that we'll keep these two (one of which with extra
trailing newline) not being equal:

#+begin_src emacs-lisp
  (equal \"a\\nx\\n\" \"a\\nx\")    ;=> nil
#+end_src")

;;;;;;;; 1D: explicit    (KVL, Cons Cell)

(xht--describe
  "Functions that compare if two or more objects are equal.

Key–value pairs are of dimension 1: not nested.

No need for a function to compare cons cells: equal will do:

#+begin_src emacs-lisp
  (equal '(a . 1) '(a . 1)) ;=> t
#+end_src")

(defun h-kvl= (kvl1 kvl2 &rest more-kvls)
  "Return t only if all KEY–VALUE LINES have same KEY–VALUE pairs.
It accepts any number ≥2 of KEY–VALUE LINES as input."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-kvl
         kvl1 kvl2 more-kvls))

;;;;;;;; ≥1D unspecified (Hash table)          (possibly nested)

(xht--describe
  "Functions that compare if two or more hash tables are equivalent.
Key–value pairs are of unspecified dimension >1 (nested).

Many distinctions here, so a summary of what the operators mean
follows. These tests return t if:

| Operator | Condition for returning a truthy value                    |
|----------+-----------------------------------------------------------|
| ‘h==’    | tables have same keys in THE SAME order, and the values:  |
|          | - when both are hash tables, are also ‘h==’.              |
|          | - when both are of some different type, they are ‘equal’. |
|----------+-----------------------------------------------------------|
| ‘h-pr==’ | same as the previous, but stricter: properties*           |
|          | also match: same size, test function, etc.                |
|----------+-----------------------------------------------------------|
| ‘h=’     | tables have same keys in ANY order, and the values:       |
|          | - when both are hash tables, are also ‘h=’.               |
|          | - when both are of some different type, they are ‘equal’. |
|----------+-----------------------------------------------------------|
| ‘h-pr=’  | same as the previous, but stricter: properties*           |
|          | also match: same size, test function, etc.                |
|----------+-----------------------------------------------------------|
| ‘h_=’    | tables have same keys in ANY order, and the values:       |
|          | - are ‘equal’, OR:                                        |
|          | -- when both are hash tables, are also ‘h_=’.             |
|          | -- when both are of some different type, they return t    |
|          | when tested with their respective equality function;      |
|          | so, for example, if they're both alists, comparison       |
|          | of them using ‘h-alist=’ returns t; when both TSVs.       |
|          | comparison of them using ‘h-tsv=’ returns t; etc.         |
|----------+-----------------------------------------------------------|
| ‘h~=’    | tables have same keys in ANY order, and the values:       |
|          | - are ‘equal’, OR:                                        |
|          | -- when both are hash tables, are also ‘h~=’.             |
|          | -- when both are of some different type, they             |
|          | return t when tested with ‘h-it~’, which in               |
|          | turn means that they can even be of different             |
|          | types but, when converted to a hash table,                |
|          | become ‘h~=’.                                             |

So there's a gradation of strictness of equivalence, where:
- ‘h-pr==’ > ‘h==’ > ‘h=’ > ‘h_=’ > ‘h~=’.
and:
- ‘h-pr=’  > ‘h=

And so ‘h~=’ is the most permissive, where equivalences of each
item are themselves deeply tested.

The ‘h==’ one may be useful, for example, for deciding whether
two hash tables would return the exact same results when mapping or
converting.

We could say that ‘h=’ is to ‘h-pr=’ as ‘equal’ is to
equal-including-properties’.

When in doubt, ‘h=’ is likely the best choice, because:

- It accepts different ordering of keys inside the tables. Order is
  not supposed to matter in hash tables, so this is most likely
  what you want: being stricter with ‘h==’ or any of the -pr
  would deem unequal tables that for most regular purposes are
  equivalent.

- When nested, it applies the same logic to any hash tables stored
  in it as values, ignoring order: suffices that their keys are the
  same and that the values are either ‘equal’ or ‘h=’.

- Any non–hash-table values are tested with ‘equal’, so you don't
  run the risk of false equivalence of lists that were not intended
  as plists (and therefore sensitive to ordering and to repeated
  items), and other such oddities.")

(defun h-pr== (table1 table2 &rest more-tables)
  "One of the ways to see if all hash tables TABLES are equivalent.

| Compare properties?                 | YES     |
| Compare keys order?                 | YES     |
| Compare non–hash-table values with… | #'equal |

When the values are hash tables, they are compared with
this very function, recursively.

For more details, see the helper function ‘xht--equal?’, as well as
the section heading description in the code source.

See all: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (apply #'xht--equal-many #'xht---pr== #'identity
         table1 table2 more-tables))

(defun h-pr= (table1 table2 &rest more-tables)
  "One of the ways to see if all hash tables TABLES are equivalent.

| Compare properties?                 | YES     |
| Compare keys order?                 | NO      |
| Compare non–hash-table values with… | #'equal |

When the values are hash tables, they are compared with this very
function, recursively.

For more details, see the helper function ‘xht--equal?’, as well as
the section heading description in the code source.

See all: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (apply #'xht--equal-many #'xht---pr= #'identity
         table1 table2 more-tables))

(defun h== (table1 table2 &rest more-tables)
  "One of the ways to see if all hash tables TABLES are equivalent.

| Compare properties?                 | NO      |
| Compare keys order?                 | YES     |
| Compare non–hash-table values with… | #'equal |

When the values are hash tables, they are compared with
this very function, recursively.

For more details, see the helper function ‘xht--equal?’, as well as
the section heading description in the code source.

See all: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (apply #'xht--equal-many #'xht--== #'identity
         table1 table2 more-tables))

(defun h= (table1 table2 &rest more-tables)
  "One of the ways to see if all hash tables TABLES are equivalent.

| Compare properties?                 | NO      |
| Compare keys order?                 | NO      |
| Compare non–hash-table values with… | #'equal |

When the values are hash tables, they are compared with this very
function, recursively.

When in doubt, use this one.

This function can be thought of as a variadic version of
ht-equal?’: it accepts any number ≥2 of tables as input.

Moreover, it returns t when comparing two indistinguishable nested
hash tables (of same key–value pairs), for which ‘ht-equal?’ until
v2.4 returns nil:

#+begin_src emacs-lisp
  (ht-equal? (ht (:a 1))
             (ht (:a 1)))           ;=> t

  (ht-equal? (ht (:a (ht (:b 2))))
             (ht (:a (ht (:b 2))))) ;=> nil !!
#+end_src

For more details, see the helper function ‘xht--equal?’, as well as
the section heading description in the code source.

See all: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (apply #'xht--equal-many #'xht--= #'identity
         table1 table2 more-tables))

(defun h_= (table1 table2 &rest more-tables)
  "One of the ways to see if all hash tables TABLES are equivalent.

| Compare properties?                 | NO      |
| Compare keys order?                 | NO      |
| Compare non–hash-table values with… | #'h-it= |

When the values are hash tables, they are compared with this very
function, recursively.

For more details, see the helper function ‘xht--equal?’, as well as
the section heading description in the code source.

See all: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (apply #'xht--equal-many #'xht--_= #'identity
         table1 table2 more-tables))

(defun h~= (table1 table2 &rest more-tables)
  "One of the ways to see if all hash tables TABLES are equivalent.

| Compare properties?                 | NO      |
| Compare keys order?                 | NO      |
| Compare non–hash-table values with… | #'h-it~ |

When the values are hash tables, they are compared with this very
function, recursively.

For more details, see the helper function ‘xht--equal?’, as well as
the section heading description in the code source.

See all: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (apply #'xht--equal-many #'xht--~= #'identity
         table1 table2 more-tables))

(defun h-props= (table1 table2 &rest more-tables)
  "Whether all hash tables TABLES have the same properties.
Note: but not necessarily the same key–value pairs, just the properties.

| Compare properties? | YES |
| Compare data?       | NO  |

See also: ‘h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--props= #'identity
         table1 table2 more-tables))


;;;;;;;;; helper
;; Note: ‘xht--equal?’ fixes ‘ht-equal?’ returning nil for two nested HTs.
;; It also doesn't return error when either arg isn't a hash table —
;; returns nil instead. I think this is fine: ‘equal’ itself returns
;; nil when different types: (equal '(:a 1) [:a 1]) ;=> nil

(defun xht--equal? (table1 table2 &optional strictness)
  "Return t if TABLE1 and TABLE2 have the same keys and values.
Also works with nested hash tables.

This is a helper function to build these:
h-pr==’, ‘h-pr=’, ‘h==’, ‘h=’, ‘h_=’, ‘h~=’.

\(Note: when in doubt, ‘h=’ is probably what you want.)

When STRICTNESS is nil, default to '=.

When STRICTNESS is the symbol:
- '-pr==
- test their properties with ‘xht--props=’.
- test the keys order of both.
- test non–hash-table values with ‘equal’.
- test hash-table values with (‘xht--equal?’ v1 v2 'pr==).

- '-pr=
- test their properties with ‘xht--props=’.
- test non–hash-table values with ‘equal’.
- test hash-table values with (‘xht--equal?’ v1 v2 'pr=).

- '==
- test the keys order of both.
- test non–hash-table values with ‘equal’.
- test hash-table values with (‘xht--equal?’ v1 v2 '==).

- '=
- test non–hash-table values with ‘equal’.
- test hash-table values with (‘xht--equal?’ v1 v2 '=).

- '_=
- test non–hash-table values with ‘h-it=’.
- test hash-table values with (‘xht--equal?’ v1 v2 '_=).

- '~=
- test non–hash-table values with ‘h-it~’.
- test hash-table values with (‘xht--equal?’ v1 v2 '~=).

For any chosen option, all tests must pass to return t.

Comparison of properties mean: besides the data, they must all be
of same nominal size, test function, weakness, rehash-size and
rehash-threshold.

Some comments.

So if '_= is chosen, when the values of matching keys are of same
type but aren't equal, try to convert them to hash tables and
compare again. They could be, for example, equivalent plists or
equivalent alists or equivalent JSONs or equivalent org tables:
with just the order changed or updated (repeated) keys not deleted
— but equivalent. In this case, values will be considered equal. So
when this argument is '_= any pair among the following would return
t to this function:

#+begin_src emacs-lisp
  (h* :a 1 :b '(:c 3 :d 4))
  (h* :a 1 :b '(:d 4 :c 3)))
  (h* :a 1 :b '(:d 4 :c 3 :c 1 :d 2)))
#+end_src

and the same for when 2D: org tables, lists of lists, TSVs, etc.

And if '~= is chosen, it's like the previous but allow for the
values' types to be different. So when this argument is '~= any
pair among the following would return t to this function:

#+begin_src emacs-lisp
  (h* :a 1 :b '(:c 3 :d 4))                   ; plists
  (h* :a 1 :b '(:d 4 :c 3))                   ;
  (h* :a 1 :b '(:d 4 :c 3 :c 1 :d 2))         ;
  (h* :a 1 :b '((:c . 3) (:d . 4)))           ; alists
  (h* :a 1 :b '((:d . 4) (:c . 3) (:c . 20))) ;
  (h* :a 1 :b '(h* :c 3 :d 4))                ;
  (h* :a 1 :b \":c\t3\n:d\t4\")               ; key–value lines
#+end_src

and the same for when 2D: org tables, lists of lists, TSVs, etc.

Why bother with a separate function? Isn't it always desirable to
have these equivalent things being considered equivalent?

Not necessarily. Take the case of a value that looks like this:

#+begin_src emacs-lisp
  '(a f n nil b x b nil a m w v a k)
#+end_src

Who knows what this list is supposed to mean? It may represent the
first letter of the names of cats in your building from lowest- to
highest-numbered apartments — and thus NOT intended to be a plist.

So the list above would NOT be equivalent to:

#+begin_src emacs-lisp
  '(a f n nil b x w v)
#+end_src

Doing that would have replaced the cat in apartment 107; and added
one cat to 108, whose resident is allergic and won't be pleased.

What about alists? Wouldn't it be safe to do it automatically to them?

Also not necessarily. Perhaps you want to keep track of items as they
are replaced. Maybe this:

#+begin_src emacs-lisp
  '((Alice . 10) (Bob . 5)
    (Alice . 15) (Bob . 2)
    (Alice . 0)  (Bob . 0))
#+end_src

represents how much money you owed your friends Alice and Bob at
the end of three consecutive days, which you want to keep a record
of AND be able to ‘assoc’ the alist to get the current values (10
and 5, respectively). And you could get the value of the previous
day using (nth 2 list) and (nth 3 list).

What about org tables? Same thing. Maybe the one you have stored is
not at all about some tabular data structure of unique IDs, and you
don't want to reduce it to one.

So we have separate functions, to be used according to what you'd
like to consider equivalent. This depends on the data, and also on
what you're doing."
  (declare (pure t) (side-effect-free t))
  (if strictness
      (unless (memq strictness '(-pr==  -pr=  ==  =  _=  ~=))
        (user-error "‘xht--equal?’: Unrecognized strictness option %s"
                    strictness))
    (setq strictness '=))
  (and
   (h? table1)
   (h? table2)
   (pcase strictness
     ((or '-pr== '-pr=) (xht--props= table1 table2))
     (_ t))
   (let ((keys1 (h-lkeys table1 'rev))
         (keys2 (h-lkeys table2 'rev)))
     (and
      (pcase strictness
        ((or '-pr== '==) (equal keys1 keys2))
        (_ t))
      (equal (length keys1)
             (length keys2))
      (equal (->> keys1  (--map (format "%S" it))  (-sort #'string<))
             (->> keys2  (--map (format "%S" it))  (-sort #'string<)))
      (--all? (let* ((sentinel (make-symbol "xht--sentinel"))
                     (val1     (h-get table1 it))
                     (val2     (h-get table2 it sentinel)))
                (if (and (h? val1)
                         (h? val2))
                    ;; recurse:
                    (xht--equal? val1 val2 strictness)
                  (pcase strictness
                    ('-pr== (equal    val1 val2))
                    ('-pr=  (equal    val1 val2))
                    ('==    (equal    val1 val2))
                    ('=     (equal    val1 val2))
                    ('_=    (xht--it= val1 val2))
                    ('~=    (xht--it~ val1 val2))
                    (_      (error "‘xht--equal?’: impossible option")))))
              ;; Note:
              ;; - ‘xht--it~’ calls ‘h~=’ which is...
              ;; (xht--equal? h1 h2 '~=), where h1 and h2 will be the
              ;; hash tables found in the conversion.
              ;;
              ;; - ‘xht--it=’ calls ‘h=’ which is...
              ;; (xht--equal? h1 h2 '=), where h1 and h2 will be the
              ;; hash tables found in the conversion.
              ;;
              ;; Eventually these recursions end up in two things that can
              ;; be compared with ‘equal’ instead of converted.
              ;;
              ;; I suppose ‘h_=’ would be more consistent for the second
              ;; case, but this seems more than good enough. There are more
              ;; distinctions that could be made, such as what happens when
              ;; the hash tables have an alist that has a plist that itself
              ;; has a hash table with a JSON object storing two TSVs that may
              ;; or not be equivalent — but if you find yourself staring at
              ;; that, you may be better off reviewing your data structure...
              keys1)))))

(defun xht--props= (table1 table2)
  "Whether TABLE1 and TABLE2 have the same properties.
But not necessarily the same key–value pairs.

Helper function for ‘xht--props-and-data=’ and ‘h-props=’."
  (declare (pure t) (side-effect-free t))
  (h= (h-props table1)
      (h-props table2)))

(defsubst xht---pr== (table1 table2)
  "Whether TABLE1 and TABLE2 are ‘xht--equal?’ with strictness '-pr==."
  (xht--equal?                    table1 table2                '-pr==))
(defsubst xht---pr=  (table1 table2)
  "Whether TABLE1 and TABLE2 are ‘xht--equal?’ with strictness '-pr=."
  (xht--equal?                    table1 table2                '-pr=))
(defsubst xht--==    (table1 table2)
  "Whether TABLE1 and TABLE2 are ‘xht--equal?’ with strictness '==."
  (xht--equal?                    table1 table2                '==))
(defsubst xht--=     (table1 table2)
  "Whether TABLE1 and TABLE2 are ‘xht--equal?’ with strictness '=."
  (xht--equal?                    table1 table2                '=))
(defsubst xht--_=    (table1 table2)
  "Whether TABLE1 and TABLE2 are ‘xht--equal?’ with strictness '_=."
  (xht--equal?                    table1 table2                '_=))
(defsubst xht--~=    (table1 table2)
  "Whether TABLE1 and TABLE2 are ‘xht--equal?’ with strictness '~=."
  (xht--equal?                    table1 table2                '~=))

;;;;;;;; ≥1D unspecified (Alist, Plist, JSON)  (possibly nested)

(xht--describe
  "Functions that compare if two or more alists, plists, or json objects are
equivalent. Key–value pairs are of unspecified dimension >1 (nested).")

(defun h-alist= (alist1 alist2 &rest more-alists)
  "Return t only if all ALISTs have same KEY–VALUE pairs.
It accepts any number ≥2 of ALISTs as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-alist*
         alist1 alist2 more-alists))

(defun h-plist= (plist1 plist2 &rest more-plists)
  "Return t only if all PLISTs have same KEY–VALUE pairs.
It accepts any number ≥2 of PLISTs as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-plist*
         plist1 plist2 more-plists))

(defun h-json= (json1 json2 &rest more-jsons)
  "Return t only if all JSONs have same KEY–VALUE pairs.
It accepts any number ≥2 of JSONs as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-json*
         json1 json2 more-jsons))

;;;;;;;; 2D              (LOL, Org table, TSV, CSV, SSV)

(xht--describe
  "Functions that compare if two or more objects are equal.
Key–value pairs are of dimension 2: tabular.")

(defun h-lol= (lol1 lol2 &rest more-lols)
  "Return t only if all LISTS OF LISTS have same KEY–VALUE pairs.
It accepts any number ≥2 of LISTS OF LISTS as input.

Note that regular lists and vectors are the same if they have the
same elements at the same indices, so they can be compared with
plain ‘equal’.

Two LISTS OF LISTS, however, may, for our purposes, be equal while
being nominally different.

For example, these two:

#+begin_src emacs-lisp
  '((:id  :name  :age)   '((:id  :name  :age)
    (1    alice  42)       (2    bob    30)
    (2    bob    30)       (1    alice  42))
    (1    alice  21))
#+end_src

As with alists and plists, the top-most element of same ID wins,
and the order between different items doesn't matter. Each unique
ID returns the same information. The same applied when we convert
from Org Table, TSV, CSV, SSV, or JSON."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-lol
         lol1 lol2 more-lols))

(defun h-orgtbl= (orgtbl1 orgtbl2 &rest more-orgtbls)
  "Return t only if all ORG TABLES have same KEY–VALUE pairs.
It accepts any number ≥2 of ORG TABLES as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-orgtbl
         orgtbl1 orgtbl2 more-orgtbls))

(defun h-tsv= (tsv1 tsv2 &rest more-tsvs)
  "Return t only if all TSVs have same KEY–VALUE pairs.
It accepts any number ≥2 of TSVs as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-tsv
         tsv1 tsv2 more-tsvs))

(defun h-csv= (csv1 csv2 &rest more-csvs)
  "Return t only if all CSVs have same KEY–VALUE pairs.
It accepts any number ≥2 of CSVs as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-csv
         csv1 csv2 more-csvs))

(defun h-ssv= (ssv1 ssv2 &rest more-ssvs)
  "Return t only if all SSVs have same KEY–VALUE pairs.
It accepts any number ≥2 of SSVs as input.

See ‘h-lol=’ for more information on equivalence, which also
applies here."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--= #'h<-ssv
         ssv1 ssv2 more-ssvs))


;;;;;;;; Unknown thing

(xht--describe
  "Functions that compare if two or more objects are equal.
Objects are unspecified.")

(defun h-type (obj)
  "Detect object OBJ's type-for-conversion. Return a keyword.

This isn't the same as ‘type-of’; it's more concerned with
types of interest for conversions and lookups.

So, for example, it'd return :tsv for a TSV, while ‘type-of
would return 'string.

Testing is done in a selected order to best guess some potentially
ambiguous types by trying to match the most specific of them first.

- So LOL is tested for before alist and plist, which are also
  tested before list.

- Moreover, if, while testing, the value of ‘h-kvl-sep-re’ matches
  TAB, then ‘h-kvl?’ could return t if all lines have a single TAB.
  Since by default this variable matches \" *= *\", it'll be
  interpreted that it's being let-bound, and that the OBJ is a KVL
  rather than a two-column TSV. Likewise in case the value of
h-kvl-sep-re’ matches a comma \(preference over CSV) or simply
  two or more spaces \(preference over SSV).

  It's worth reminding that the difference between a two-column *sv
  and a KVL of matching delimiter is that the latter has no headers
  \(one-dimensional: just key and value), whereas the former has a
  header \(two-dimensional: key, field, and value)"
  (declare (pure t) (side-effect-free t))
  (let ((type (type-of obj)))
    (pcase type
      ('hash-table                     :ht)
      ('vector                         :vector)
      ('cons       (cond
                    ((-cons-pair? obj) :cons-pair)
                    ((h-lol?      obj) :lol)
                    ((h-alist?    obj) :alist)
                    ((h-plist?    obj) :plist)
                    (t                 :list)))
      ('string     (cond
                    ((string= ""  obj) :empty)
                    ((h-orgtbl?   obj) :orgtbl)
                    ((h-json?     obj) :json)
                    ((h-kvl?      obj) :kvl)
                    ((h-tsv?      obj) :tsv)
                    ((h-csv?      obj) :csv)
                    ((h-ssv?      obj) :ssv)
                    (t                 :lines)))
      ('symbol     (cond
                    ((null        obj) :null)
                    (t                 type)))
      (_                               type))))

(defun h-type= (obj1 obj2 &rest more-objs)
  "Are all OBJECTS of same type?
For example, are they all hash tables? Or all org tables?
Or all lists of lists? Or all alists? Or all TSVs? JSONs?

Return type.

This isn't the same as ‘type-of’; it's more concerned with
types of interest for this conversion and lookup.

So, for example, it'd return :tsv for a TSV, while ‘type-of
would return 'string."
  (declare (pure t) (side-effect-free t))
  (when (apply #'xht--equal-many #'xht--type= #'identity
               obj1 obj2 more-objs)
    (h-type obj1)))

(defun h-it= (obj1 obj2 &rest more-objs)
  "Are all OBJECTS, all of same type, reducible to each other?
Try to guess what they are.

The following tests are applied, and if any of them return t for
every pair of objects, they are considered same-type equals:
equal’      ‘h-tsv=’     ‘h-json=
h=’         ‘h-csv=’     ‘h-alist=
h-lol=’     ‘h-ssv=’     ‘h-plist=
h-orgtbl=’.
If so, return type. Otherwise, return nil."
  (declare (pure t) (side-effect-free t))
  (when (apply #'xht--equal-many #'xht--it= #'identity
               obj1 obj2 more-objs)
    (h-type obj1)))

(defun h-it~ (obj1 obj2 &rest more-objs)
  "Are all OBJECTS equivalent according to XHT tests?
They are if, and only if, either they are equal or, when converted
to a hash table, ‘h~=’ returns t for the pair. This means that
repeated keys (in alists, plists, TSVs etc) are dealt with, and
that, after that, ordering is irrelevant."
  (declare (pure t) (side-effect-free t))
  (apply #'xht--equal-many #'xht--it~ #'identity
         obj1 obj2 more-objs))

;;;;;;;;; helper

(defun xht--type= (obj1 obj2)
  "Are OBJ1 and OBJ2 of same type?"
  (declare (pure t) (side-effect-free t))
  (equal (h-type obj1)
         (h-type obj2)))

(defun xht--it= (obj1 obj2)
  "Are OBJ1 and OBJ2, of same type, reducible to each other?
If so, return type. Otherwise, return nil."
  (declare (pure t) (side-effect-free t))
  (let ((t1 (h-type obj1))
        (t2 (h-type obj2)))
    (and (equal t1 t2)
         (pcase t1
           (:ht         (h=         obj1  obj2))
           (:lol        (h-lol=     obj1  obj2))
           (:tsv        (h-tsv=     obj1  obj2))
           (:csv        (h-csv=     obj1  obj2))
           (:ssv        (h-ssv=     obj1  obj2))
           (:kvl        (h-kvl=     obj1  obj2))
           (:json       (h-json=    obj1  obj2))
           (:alist      (h-alist=   obj1  obj2))
           (:plist      (h-plist=   obj1  obj2))
           (:orgtbl     (h-orgtbl=  obj1  obj2))
           (:empty      t)
           (:null       t)
           (:list       (equal      obj1  obj2))
           (:lines      (equal      obj1  obj2))
           (:vector     (equal      obj1  obj2))
           (:cons-pair  (equal      obj1  obj2))
           (_           (equal      obj1  obj2)))
         t1)))

(defun xht--it~ (obj1 obj2)
  "Are OBJ1 and OBJ2 equivalents according to XHT tests?
They are if, and only if, when converted to a hash table, ‘h~=
returns t for the pair."
  (declare (pure t) (side-effect-free t))
  (or (equal obj1 obj2)
      (let ((h1 (ignore-errors (h<-it obj1)))
            (h2 (ignore-errors (h<-it obj2))))
        (h~= h1 h2))))

;;;;;;;; Helper
(defun xht--equal-many (fn-compare fn-transform obj1 obj2 &rest more-objs)
  "Compare OBJECTS with FN-COMPARE after applying FN-TRANSFORM on them."
  (declare (pure t) (side-effect-free t))
  ;; Note: choice of obj1 and obj2 is to guarantee that at least 2 objects are
  ;; passed; and repetition below was chosen in order to avoid the unnecessary
  ;; consing that would come with the following simpler abstraction:
  ;; (--all? (funcall fn-compare
  ;;                  (funcall fn-transform obj1)
  ;;                  (funcall fn-transform it))
  ;;         (cons obj2 more-objs)))
  (and (funcall fn-compare
                (funcall fn-transform obj1)
                (funcall fn-transform obj2))
       (--all? (funcall fn-compare
                        (funcall fn-transform obj1)
                        (funcall fn-transform it))
               more-objs)))

;;;;;; Conversion

(xht--describe
  "Functions to convert from/to hash tables.

To hash tables:
h<-kvl’    ‘h<-lines
h<-vector’ ‘h<-list
h<-lol’    ‘h<-orgtbl
h<-alist’  ‘h<-plist’  ‘h<-alist*’  ‘h<-plist*’  ‘h<-json*
h<-tsv’    ‘h<-csv’    ‘h<-ssv

From hash tables:
h->kvl’    ‘h->lines
h->vector’ ‘h->list
h->lol’    ‘h->orgtbl
h->alist’  ‘h->plist’  ‘h->alist*’  ‘h->plist*’  ‘h->json*
h->tsv’    ‘h->csv’    ‘h->ssv

You can navigate functions easily with:
M-. and M-, (elisp-slime-nav-mode)

Note that when results on both sides evaluate to hash tables, we use
for equality comparison the ‘H=>’ symbol (which uses ‘h=’).

For getting (‘alist-get’, ‘plist-get’, ‘h-get’, etc.):
| alist, plist:        | Top-most values override bottom-most values |
| lol, org-table, *sv: | Top-most values override bottom-most values |
| kvl:                 | Bottom-most values override top-most values |
| vector, list, lines: | Uniqueness guaranteed by index              |

Their nature:
| kvl:                 | no nesting,     keys are given                     |
| lines:               | no nesting,     keys = line number (starting at 0) |
| vector, list:        | may be nested¹, keys = indices                     |
| alist, plist, json:  | may be nested², keys are given                     |
| lol, org-table, *sv: | tabular, assumed no nesting (no lols inside lols)  |

¹ treatment of nested not implemented for vectors and lists.
² treatment of nested implemented for alists, plists, and jsons —
  both ways.")

;;;;;;; Hash table to hash table

(xht--describe
  "Functions that read a hash table and return a hash table.

For shallow copying, ‘h-copy’ is offered as an alias to
copy-hash-table’. However, this function is not at all appropriate
for dealing with nested hash tables. See ‘h-clone*’ for more.")

(defun h-clone* (table &optional sz ts we rs rt)
  "Create a deep copy of possibly-nested hash table TABLE.
Optional arguments after TABLE are SZ, TS, WE, RS, and RT — which
correspond to, respectively, size, test, weakness, rehash-size,
rehash-threshold. When nil, they match that of TABLE.

The result has the same elements and structure of TABLE, but any
nested (internal) hash tables are recursively replaced by new ones.
This allows you to modify the internal hash tables without altering
the original one.

This is not possible with ‘copy-hash-table’, whose copy is shallow
and any internal tables that might be present in the copy will
point to the very same objects of the original one. Likewise with
ht-copy’, which uses ‘copy-hash-table’, and ‘h-copy’, which is an
alias to the latter.

You also shouldn't use shallow copying if you intend to apply
destructive functions such as ‘sort’, ‘nconc’, or ‘nreverse’ to any
list values of the copied table, as this will likely modify the
original. To avoid that, either use non-destructive alternatives
such as ‘-sort’ and ‘reverse’; or ‘h-clone*’ the original instead
of using ‘h-copy’.

See also: ‘h<-ht’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-empty-clone table sz ts we rs rt)))
    (maphash (lambda (key value)
               (puthash key
                        (if (h? value)
                            (h-clone* value)
                          ;; ^ recurses
                          value)
                        result))
             table)
    result))
;; ^ Could have used this:
;;     (h-hmap* (lambda (key _value) key)
;;              (lambda (_key value) value)
;;              table)
;;   but:
;;   1. h-hmap* removes nil keys (rare case, but still, we're cloning)
;;   2. chosen one is more direct, possibly faster, no need for funcalling

(defun h<-ht (table &optional sz ts we rs rt)
  "Given hash table TABLE, return it or a clone, depending on params.
Optional arguments after TABLE are SZ, TS, WE, RS, and RT — which
correspond to, respectively, size, test, weakness, rehash-size,
rehash-threshold. When nil, they match that of TABLE.

If any of the optional arguments passed differs from the TABLE's
corresponding properties, ‘h-clone*’ the table with these
parameters. Otherwise just return TABLE.

This is a non-destructive function: TABLE isn't changed.

See also: ‘h-clone*’."
  (declare (pure t) (side-effect-free t))
  (let ((rest  (list sz ts we rs rt))
        (props (-> table  h-props  h-lvalues)))
    (if (and (equal (nth 2 rest)
                    (nth 2 props))
             ;; This ^ because the below is not sufficient when passed
             ;; weakness is nil and TABLE's isn't. The other parameters, on
             ;; the other hand, cannot have nil as value in the table, so a
             ;; passed nil is just omission.
             (--all? (-let [(r . p) it]
                       (or (not r)
                           (equal r p)))
                     (-zip-with #'cons rest props)))
        table
      (h-clone* table sz ts we rs rt))))

(defun h-2d<-1d (table &optional f1 f2 size test)
  "Make 2D the 1D hash table TABLE by adding header fields F1, F2.
If F1 is nil, use the string \"key\". If F2 is nil, use \"value\".

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

Alias: ‘h-1d->2d’."
  (declare (pure t) (side-effect-free t))
  (let* ((sznow (h-size table))
         (size  (or size (xht--init-size sznow)))
         (htbl  (h-new size test)))
    (h--each table
      (h-put! htbl key
              (h* (or f1 "key")   key
                  (or f2 "value") value)))
    htbl))

(defun h-1d<-2d (table2d &optional size test)
  "Make 1D (KVL-like) the 2D hash table TABLE2D.
Infer current header with ‘h-2d-header’.
If header isn't of length 2, signal error.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’.

Alias: ‘h-2d->1d’."
  (declare (pure t) (side-effect-free t))
  (let ((header (h-2d-header table2d)))
    (unless (= 2 (length header))
      (error "Header length of hash table isn't 2"))
    (let* ((sznow (h-size table2d))
           (size  (or size (xht--init-size sznow)))
           (htbl  (h-new size test)))
      (h--each table2d
        (h-put! htbl key
                (h-get* table2d key
                        (cadr header))))
      htbl)))

;;;;;;; 1D: implicit    (indices as keys)

(xht--describe
  "Functions in this category convert from other formats to hash table or
vice-versa. Key–value pairs are of dimension 1: not nested. Indices serve as
keys.")

;;;;;;;; Vectors

(defun h<-vector (vector &optional size test)
  "Convert vector VECTOR to a hash table using VECTOR's indices as keys.

Keys are by default of integer type, not strings, and start at 0.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (let* ((sznow (length vector))
         (size  (or size (xht--init-size sznow)))
         (htbl  (h-new size test))
         (idx   0))
    (while (< idx sznow)
      (h-put! htbl idx (aref vector idx))
      (setq idx (1+ idx)))
    htbl))

(defun h->vector (table)
  "Convert hash table TABLE to vector.
Do the opposite of ‘h<-vector’, which see. Return vector.

Select only those keys that are natural numbers (an integer ≥0) or
strings that could be converted to a natural number, which it
automatically does.

Any holes in the number sequence from 0 to max(keys) are filled
in the vector with nil.

If the table has keys that reduce to the same integer, such as
having both 3 and \"3\" as keys, behavior is unpredictable."
  (declare (pure t) (side-effect-free t))
  ;; `keys` is another hash table, where each pair is (newkey key):
  ;; e.g. (3 "3")
  ;; It maps the post-natural-num keys to the original keys.
  (let* ((tkeys (h-keys table))
         (keys  (h-new (xht--init-size (length tkeys))))
         maxkey  vector  keys-keys)
    (dolist (key tkeys)
      (when-let ((keyN (xht--thing-to-nat key)))
        (h-put! keys keyN key)))
    (setq keys-keys (h-keys keys))
    (if (null keys-keys)
        []
      (setq maxkey (->> keys-keys  (apply #'max))
            vector (make-vector (1+ maxkey) nil))
      (h--each keys
        (aset vector key (h-get table value)))
      vector)))

(defun xht--thing-to-nat (obj)
  "Return OBJ as a natural number if it looks like one.
If it's a string, read it. Then check if it's a natural number.
If so, return it. Otherwise, return nil."
  (declare (pure t) (side-effect-free t))
  (if (natnump obj)
      obj
    (--> obj  h-as-string  read
         (when (natnump it) it))))

;;;;;;;; Lists

(defun h<-list (list &optional size test)
  "Convert LIST to a hash table using LIST's indices as keys.
Keys are by default of integer type, not strings, and start at 0.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (let* ((sznow (length list))
         (size  (or size (xht--init-size sznow)))
         (htbl  (h-new size test))
         (idx   0))
    (while (< idx sznow)
      (h-put! htbl idx (elt list idx))
      (setq idx (1+ idx)))
    htbl))

(defun h->list (table)
  "Convert hash table TABLE to list.
Select only those keys that are natural numbers (an integer ≥0)
or strings that could be converted to a natural number, which it
automatically does.

Any holes in the number sequence from 0 to max(keys) are filled
in the list with nil.

If the table has keys that reduce to the same integer, such as
having both 3 and \"3\" as keys, behavior is unpredictable."
  (declare (pure t) (side-effect-free t))
  (--> table
       (h->vector it)
       (append it nil)))

;;;;;;;; Lines

(defun h<-lines (str &optional size test)
  "Convert string STR to a hash table using line numbers as keys.
Keys are by default of integer type, not strings, and start at 0.

The choice of zero for the first line number (instead of 1) was due
to transitivity: a plain conversion from lines to hash table to
vector to lines should yield the initial string. The alternatives
seemed worse.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (h<-list (s-lines str) size test))

(defun h->lines (table)
  "Convert hash table TABLE whose keys are numbers to string.
Select only those keys that are natural numbers (an integer ≥0)
or strings that could be converted to a natural number, which it
automatically does.

Any holes in the number sequence from 0 to max(keys) are filled
in the string with empty lines.

If the table has keys that reduce to the same integer, such as
having both 3 and \"3\" as keys, behavior is unpredictable."
  (declare (pure t) (side-effect-free t))
  (->> table  h->list
       (s-join "\n")))

;;;;;;; 1D: explicit

(xht--describe
  "Functions in this category convert from other formats to hash table or
vice-versa. Strings with one key–value pair per line are of dimension 1: not
nested. Likewise cons pairs.")

;;;;;;;; Key–Value Lines

(defun h<-kvl (kvl &optional size test obsolete)
  "Convert key–value lines KVL to a hash table.
Optional arguments SIZE and TEST may be passed.

A KVL is the equivalent of a non-nested alist translated to a
simple flat config file as used in Unix-like systems.

It should look, for example, like this:

--- data begins --->
key1 = val1
key2 = val2
key3 = val3
<--- data ends -----

One pair per line. There is no header. If there were, it'd be taken
as another key–value line.

The field delimiter (separator) to be used is given by the value of
the regular expression in the variable ‘h-kvl-sep-re’, which see.
By default it's equal sign surrounded or not by spaces. The
variable can be temporarily let-bound when change is desirable.

Comments are non-destructively stripped before conversion. Comments
are anything matching ‘h-kvl-comment-re’ (which see), which can
also be temporarily let-bound when some other comment regex is
expected in KVL.

Note that sections (as used in .ini files and some types of .conf)
aren't implemented or dealt with here. KVLs are to be considered
flat. So lines with [Section name] are not considered as nodes, and
no tree-like structure will be generated. These section lines, if
present, are ignored by the default ‘h-kvl-section-re’ (which see)
as if they were comments, since this may be useful if the file
you're processing has sections only for information purposes and
the keys inside them are unique. If this is not the case, then this
function is not suitable for this conversion. An option would be to
pre-convert the input to JSON using some other tool, and then apply
h<-json*’ to the result.

Also note that in an alist, when two elements have equal key, the
first is used. With KVL data, we'll consider that the last row
wins. This is because in an alist a new pair is pushed to the top,
whereas KVL files are usually (but not always, as there are varying
standards) read from top to bottom.

An OBSOLETE form of calling this function is
  \(h<-kvl kvl &optional sep size test)
where the optional arg SEP specified the field delimiter. This use
is deprecated. The delimiter is now passed by the variable
h-kvl-sep-re’, which should be let-bound when the default is not
the one expected in the KVL to be converted."
  (declare (advertised-calling-convention (kvl &optional size test) "2.0")
           (pure t) (side-effect-free t))
  (let (delim-re strip-re lines htbl)
    ;; Backward compatibility with obsolete calling convention:
    (when (or (stringp size) (natnump test) obsolete)
      (setq delim-re size
            size     test
            test     obsolete))
    (setq strip-re (format "%s\\|%s" h-kvl-comment-re h-kvl-section-re)
          delim-re (or delim-re h-kvl-sep-re)
          lines    (s-lines kvl)
          size     (or size (xht--init-size (length lines)))
          htbl     (h-new size test))
    (while lines
      (--> (pop lines)
           (s-replace-regexp strip-re "" it)
           (unless (s-blank-str? it)
             (-let [(k v) (s-split delim-re (s-trim it))]
               (h-put! htbl k v)))))
    htbl))

(defun h->kvl (table &optional sep)
  "Convert hash table TABLE to SEP-separated key–value lines.
The optional separator SEP is a literal string, and could be, for
example, \" = \" or \":\" or \"\t\". When nil, default to \"=\"."
  (declare (pure t) (side-effect-free t))
  (with-output-to-string
    (h--each table
      (princ (format "%s%s%s\n"
                     key (or sep "=") value)))))

;;;;;;;; Cons Pairs

(defun h<-cons-pair (cons-pair &optional size test)
  "Convert CONS-PAIR to hash table.
SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (unless (-cons-pair? cons-pair)
    (error "%s is not a cons pair" cons-pair))
  (-let [(key . value) cons-pair]
    (h-st* (or size (xht--init-size 1)) test
           key value)))

(defun h->cons-pair (table)
  "Convert hash table TABLE to cons pair."
  (declare (pure t) (side-effect-free t))
  (pcase (h-size table)
    (1 (car (h--lmap `(,key  . ,value) table)))
    (_ (error "Can only convert to cons pair hash tables of size 1"))))

;;;;;;; ≥1D unspecific  (possibly nested)

(xht--describe
  "Functions in this category convert from other formats to hash table or
vice-versa. Key–value pairs are of undetermined dimension >1 (nested).")

;;;;;;;; Association lists
;;;;;;;;; from alist

(defun xht<--alist* (alist &optional size test recurse)
  "Convert ALIST to hash table, maybe recursing.
Helper function, basis for ‘h<-alist’ and ‘h<-alist*’.

Optional arguments SIZE and TEST may be passed.
If RECURSE is non-nil, recurse."
  (declare (pure t) (side-effect-free t))
  (let ((htbl (h-new (or size
                         (-> alist  length  xht--init-size))
                     test)))
    (dolist (pair alist htbl)
      (-let [(key . value) pair]
        (when (eq 'xht:new-one (h-get htbl key 'xht:new-one))
          ;; ^ Fastest solution I can think of to both preserve the original
          ;; ordering and exclude eventual repeats. The cost of this extra
          ;; h-get instead of processing it in reverse is ~1 μs, and I'm fine
          ;; with an extra 1 ms for processing some (rare) 1000-item list if
          ;; it preserves the list's order.
          (h-put! htbl key (if (and recurse
                                    (h-alist? value))
                               (xht<--alist* value nil test recurse)
                             ;; sublevel sizes auto^ ; recursive^
                             value)))))))

(defun h<-alist (alist &optional size test)
  "Convert ALIST to hash table.
Similar to ‘ht<-alist’, except that it preserves original order and
SIZE can also be passed as an argument. When SIZE is nil, it
defaults to the result of applying ‘xht--init-size’ to half the
plist's length.

For the meaning of TEST, see ‘h-new’.

Like ‘ht<-alist’, this function is not aware of nested alists. Its
nesting-aware counterpart is ‘h<-alist*’."
  (declare (pure t) (side-effect-free t))
  (xht<--alist* alist size test nil))

(defun h<-alist* (alist &optional size test)
  "Convert possibly nested ALIST to hash table.
If the ALIST is nested, each alist found as value will be
recursively converted into a hash table as well.

SIZE is the nominal initial size of the table to be created.
When SIZE is nil, it defaults to the result of applying
xht--init-size’ to the alist's length.

For the meaning of TEST, see ‘h-new’.

This function is aware of nested alists. Its non–nesting-aware
counterpart is ‘h<-alist’."
  (declare (pure t) (side-effect-free t))
  (xht<--alist* alist size test 'recurse))

;;;;;;;;; to alist

(defun xht-->alist* (table &optional recurse)
  "Convert hash table TABLE to alist, maybe recursing.
Helper function, basis for ‘h->alist’ and ‘h->alist*’.

Optional arguments SIZE and TEST may be passed.
If RECURSE is non-nil, recurse."
  (declare (pure t) (side-effect-free t))
  (h--lmap  (cons key
                  (if (and recurse
                           (ht? value))
                      (xht-->alist* value recurse)
                    ;; recursive^
                    value))
            table))

(defun h->alist (table)
  "Convert hash table TABLE to alist.
Similar to current ‘ht->alist’, with the difference that it
preserves the hash table's order of keys.

Like ‘ht->alist’, this function is not aware of nested alists. Its
nesting-aware counterpart is ‘h->alist*’."
  (declare (pure t) (side-effect-free t))
  (xht-->alist* table nil))

(defun h->alist* (table)
  "Convert hash table TABLE to alist, maybe recursing.
If the ALIST is nested, each alist found as value will be
recursively converted into a hash table as well.

This function is aware of nested alists. Its non–nesting-aware
counterpart is ‘h->alist’."
  (declare (pure t) (side-effect-free t))
  (xht-->alist* table 'recurse))

;;;;;;;; Property lists
;;;;;;;;; from plist

(defun xht<--plist* (plist &optional size test recurse)
  "Convert PLIST to hash table, maybe recursing.
Helper function, basis for ‘h<-plist’ and ‘h<-plist*’.

Optional arguments SIZE and TEST may be passed.
If RECURSE is non-nil, recurse."
  (declare (pure t) (side-effect-free t))
  (let* ((lenpl (length plist))
         (htbl  (h-new (or size
                           (-> lenpl  (/ 2)  xht--init-size))
                       test))
         (even  (if (= 0 (% lenpl 2))
                    (-copy plist)
                  (-butlast plist))))
    (while even
      (let ((key   (pop even))
            (value (pop even)))
        (when (eq 'xht:new-one (h-get htbl key 'xht:new-one))
          ;; ^ Fastest solution I can think of to both preserve the original
          ;; ordering and exclude eventual repeats. The cost of this extra
          ;; h-get instead of processing it in reverse is ~1 μs, and I'm fine
          ;; with an extra 1 ms for processing some (rare) 1000-item list if
          ;; it preserves the list's order.
          (h-put! htbl key (if (and recurse
                                    (h-plist? value))
                               (xht<--plist* value nil test recurse)
                             ;; sublevel sizes auto^ ; recursive^
                             value)))))
    htbl))

(defun h<-plist (plist &optional size test)
  "Convert PLIST to hash table.
Similar to ‘ht<-plist’, except that it preserves original order and
SIZE can also be passed as an argument. When SIZE is nil, it
defaults to the result of applying ‘xht--init-size’ to half the
plist's length.

For the meaning of TEST, see ‘h-new’.

This function is not aware of nested plists. Its nesting-aware
counterpart is ‘h<-plist*’."
  (declare (pure t) (side-effect-free t))
  (xht<--plist* plist size test nil))

(defun h<-plist* (plist &optional size test)
  "Convert possibly nested PLIST to hash table.
If the PLIST is nested, each plist found as value will be
recursively converted into a hash table as well.

SIZE is the nominal initial size of the table to be created.
When SIZE is nil, it defaults to the result of applying
xht--init-size’ to half the plist's length.

For the meaning of TEST, see ‘h-new’.

This function is aware of nested plists. Its non–nesting-aware
counterpart is ‘h<-plist’."
  (declare (pure t) (side-effect-free t))
  (xht<--plist* plist size test 'recurse))

;;;;;;;;; to plist

(defun xht-->plist* (table &optional recurse)
  "Convert hash table TABLE to plist, maybe recursing.
Helper function, basis for ‘h->plist’ and ‘h->plist*’.

If RECURSE is non-nil, recurse."
  (declare (pure t) (side-effect-free t))
  (apply #'append
         (h--lmap (list key
                        (if (and recurse
                                 (ht? value))
                            (xht-->plist* value recurse)
                          ;; recursive^
                          value))
                  table)))

(defun h->plist (table)
  "Convert hash table TABLE to plist.
Similar to current ‘ht->plist’, with the difference that it
preserves the hash table's order of keys.

This function is not aware of nested plists. Its nesting-aware
counterpart is ‘h->plist*’."
  (declare (pure t) (side-effect-free t))
  (xht-->plist* table nil))

(defun h->plist* (table)
  "Convert hash table TABLE to plist, maybe recursing.
If the PLIST is nested, each plist found as value will be
recursively converted into a hash table as well.

This function is aware of nested plists. Its non–nesting-aware
counterpart is ‘h->plist’."
  (declare (pure t) (side-effect-free t))
  (xht-->plist* table 'recurse))

;;;;;;;; JSON

(defun h<-json* (json &optional size test)
  "Convert possibly nested JSON to hash table.

If JSON is a string enclosed in [] instead of {}, the first step of
the conversion will output a vector instead of directly a hash
table. The vector will be inspected, and:

- If every item is a hash table, guess from it the ID for creating
  a 2D hash table, and return such table.

- Otherwise, treat it as vector and convert to hash table as usual:
  indices as numeric keys, items as values.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (let* ((json-object-type 'hash-table)
         (converted (json-read-from-string json)))
    (cond
     ((ht? converted)
      (h<-ht converted size (or test 'equal)))
     ((vectorp converted)
      (let ((values (append converted nil)))
        (if (-all? #'ht? values)
            (let* ((headers-lol (-map    #'h-keys        values))
                   (common      (-reduce #'-intersection headers-lol)))
              (unless common
                (error "h<-json*: dim < 2D? Don't know how to convert"))
              (let ((id   (car common))
                    (htbl (h-new size test)))
                (--each values
                  (h-put! htbl (h-get it id) it))
                htbl))
          (h<-vector converted)))))))

(defun h->json* (table &optional no-pp sort)
  "Convert hash table TABLE to JSON, maybe recursing.
If NO-PP is nil, return formatted (pretty-printed string).
If NO-PP is non-nil, return as is (compact string).

If SORT is non-nil, return it sorted. Otherwise don't sort it."
  (declare (pure t) (side-effect-free t))
  ;; It seems JSON needs keys to be strings or symbols.
  ;; No numbers as keys, for example.
  (let ((json-encoding-object-sort-predicate (when sort 'string<))
        (json-encoding-pretty-print (not no-pp))
        (htbl (h-new (h-size table))))
    ;; Problem: ‘json-encode-hash-table’ reverses the key order of the hash
    ;; table, and of its values (also hash tables) — but we'd rather preserve
    ;; the order when sort is nil (it won't matter if we choose to sort it, of
    ;; course). So I thought it was time to write some functions to reverse
    ;; the key order of a hash table...
    (h--each table
      (h-put! htbl (h-as-string key) value))
    (json-encode-hash-table (if sort
                                htbl
                              (h-reverse* htbl)))))

;;;;;;; 2D              (tabular)

(xht--describe
  "Functions in this category convert from other formats to hash table or
vice-versa. Key–value pairs are of dimension 2: tabular.")

;;;;;;;; Lisp tables (lists of lists)

(defun h<-lol (lol &optional size test)
  "Create a hash table with initial values according to list of lists LOL.

- The first element of the LOL is a list of keys.

- The other elements of the LOL are their respective values.

- The values of the first column of the LOL are used as unique IDs:
  keys to the first hash table.

- The value of each unique ID is a hash table composed of all
  key–value pairs associated with this unique ID.

As with alists, in case of repeated unique IDs the one uppermost in
the list has preference for returning values.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (let* ((sentinel (make-symbol "xht--sentinel"))
         (size     (or size (xht--init-size (length lol))))
         (htbl     (h-new size test))
         (header   (car lol))
         key)
    (dolist (row (cdr lol) htbl)
      (setq key (car row))
      (when (eq sentinel (h-get htbl key sentinel))
        ;; ^ Fastest solution I can think of to both preserve the original
        ;; ordering and exclude eventual repeats. The cost of this extra h-get
        ;; instead of processing it in reverse is ~1 μs, and I'm fine with an
        ;; extra 1 ms for processing some (rare) 1000-item list if it
        ;; preserves the list's order.
        (h-put! htbl key (h-zip-lists header row))))))

(defun h->lol (table2d &optional reverse)
  "Do the opposite of ‘h<-lol’, which see. Return list of lists.
Input is TABLE2D.

If REVERSE is non-nil, reverse the display order of all rows after
header. Notice that the internal order of items depends on where
the data came from and whether it has been updated."
  (declare (pure t) (side-effect-free t))
  (unless (h-empty? table2d)
    (let* ((header (h-2d-header table2d))
           (rest   (h-lmap (lambda (_k value)
                             (--map (ht-get value it) header))
                           table2d 'reverse))) ;<--more efficient
      (cons header (if reverse
                       rest
                     (reverse rest))))))

;;;;;;;; Org tables

(defun h<-orgtbl (orgtbl &optional size test)
  "Create a hash table with initial values according to org table ORGTBL.
Does to an org table what ‘h<-lol’ does to Lisp tables (lists of lists).

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (autoload #'org-table-to-lisp "org-table")
  (let ((lol (->> orgtbl
                  org-table-to-lisp
                  (-remove-item 'hline))))
    (h<-lol lol size test)))

(defun h->orgtbl (table2d &optional reverse)
  "Do the opposite of ‘h<-orgtbl’, which see. Return org table as string.
Input is TABLE2D.

If REVERSE is non-nil, reverse the display order of all rows after
header. Notice that the internal order of items depends on where
the data came from and whether it has been updated."
  (declare (pure t) (side-effect-free t))
  (if (ht-empty? table2d)
      ""
    (with-temp-buffer
      (let* ((standard-output (current-buffer))
             (header (h-2d-header table2d))
             (rest   (h-lmap (lambda (_k value)
                               (--map (ht-get value it) header))
                             table2d 'reverse))) ;<-- more efficient
        (princ (xht--list-to-orgtbl-row header))
        (princ "|----------|\n")
        (dolist (item (if reverse
                          rest
                        (reverse rest)))
          ;; That's right ^ because if it came from a lol or
          ;; org table or tsv, it was originally read from
          ;; bottom to top, so it's already internally reversed.
          (princ (xht--list-to-orgtbl-row item))))
      ;; Without org it will also work — but the org table
      ;; output, although valid, will remain unformatted.
      (ignore-errors (org-mode))
      (when (equal major-mode 'org-mode)
        (goto-char 3)
        (call-interactively #'org-ctrl-c-ctrl-c))
      (s-trim (xht--buff-str-no-prop)))))

(defun xht--list-to-orgtbl-row (list)
  "Convert a flat LIST to an org table's row."
  (declare (pure t) (side-effect-free t))
  (->> (-map #'h-as-string list)
       (s-join " | ")
       (format "| %s |\n")))

;;;;;;;; TSV, CSV, SSV
;;;;;;;;; Without using org (preferred)

(defun h<-tsv (tsv &optional size test)
  "Create a hash table with initial values according to TSV.
SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (let (lol)
    (--> (s-lines tsv)
         (dolist (row it (nreverse lol))
           (unless (s-blank-str? row)
             (push (s-split "\t" row) lol)))
         (h<-lol it size test))))
;; This ^ should be faster and more direct than using org, and without limits:
;; org-table-convert-region’ uses ‘org-table-convert-region-max-lines’,
;; normally set to 999.

(defun h->tsv (table2d &optional reverse)
  "Do the opposite of ‘h<-tsv’, which see. Return tsv.
Input is TABLE2D.

If REVERSE is non-nil, reverse the display order of all rows after
header. Notice that the internal order of items depends on where
the data came from and whether it has been updated."
  (declare (pure t) (side-effect-free t))
  (if (h-empty? table2d)
      ""
    (with-temp-buffer
      (let* ((standard-output (current-buffer))
             (header (h-2d-header table2d))
             (rest   (h-lmap (lambda (_k v)
                               (h-values v))
                             table2d 'reverse))) ;<-- more efficient
        (princ (xht--list-to-tsv-row header))
        (dolist (item (if reverse
                          rest
                        (reverse rest)))
          ;; That's right ^ because if it came from a lol or
          ;; org table or tsv, it was originally read from
          ;; bottom to top, so it's already internally reversed.
          (princ (xht--list-to-tsv-row item))))
      (->> (xht--buff-str-no-prop)
           s-trim-right
           (format "%s\n")))))

(defun xht--list-to-tsv-row (list)
  "Convert a flat LIST to a TSV's row."
  (declare (pure t) (side-effect-free t))
  (->> (-map #'h-as-string list)
       (s-join   "\t")
       (s-append "\n")))

(defun h<-ssv (ssv &optional size test)
  "Create a hash table with initial values according to SSV.
SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (let (lol)
    (--> (s-lines ssv)
         (dolist (row it (nreverse lol))
           (unless (s-blank-str? row)
             (push (s-split "  +" row) lol)))
         (h<-lol it size test))))

(defun h->ssv (table2d &optional reverse)
  "Do the opposite of ‘h<-ssv’, which see. Return ssv.
Input is TABLE2D.

If REVERSE is non-nil, reverse the display order of all rows after
header. Notice that the internal order of items depends on where
the data came from and whether it has been updated."
  (declare (pure t) (side-effect-free t))
  (if (h-empty? table2d)
      ""
    (with-temp-buffer
      (let* ((standard-output (current-buffer))
             (header  (h-2d-header table2d))
             (rest    (h-lmap (lambda (_k v)
                                (h-values v))
                              table2d 'reverse)) ;<--more efficient
             (widths  (--map (xht--2d-col-width table2d it) header)))
        (princ (xht--list-to-ssv-row header widths))
        (dolist (item (if reverse
                          rest
                        (reverse rest)))
          ;; That's right ^ so we only reverse it once; and if it came from a
          ;; lol or org table or ssv, it was originally read from bottom to
          ;; top, so it's already internally reversed.
          (princ (xht--list-to-ssv-row item widths))))
      (->> (xht--buff-str-no-prop)
           s-trim-right
           (format "%s\n")))))

(defun xht--2d-col-width (table2d field)
  "Maximum string length of column FIELD of 2D hash table TABLE2D.
For example, in the SSV below, resulting widths are, respectively:
2, 5, 3.

id  name   age
01  alice  42
02  bob    30

So passing \"name\" as FIELD for an equivalent 2D table returns 5."
  (declare (pure t) (side-effect-free t))
  (->> (h-2d-col table2d field)
       (--map (->> it  h-as-string  length))
       -max))

(defun xht--list-to-ssv-row (list &optional widths)
  "Convert a flat LIST to an SSV's row.
If WIDTHS is nil, just use two spaces as separator.
If however a list of WIDTHS is provided, fields will be padded to
these widths before adding the two spaces."
  (declare (pure t) (side-effect-free t))
  (->> (h-zip-lists (-map #'h-as-string list)
                    (or widths (-repeat (length list) 1)))
       (h--lmap (format (format "%%-%ds"
                                value)
                        key))
       (s-join "  ")  ;join with 2 spaces
       (s-trim-right) ;no need for trailing spaces in the row
       (s-append "\n")))

;; h<-csv’: aliased to ‘xht--csv’.

(defun h->csv ()
  "Do the opposite of ‘h<-csv’, which see. Return csv.
NOTE: This function has not yet been implemented."
  (message "h->csv: not yet implemented"))

;;;;;;;;; Using org

(defun xht<--*sv (input arg &optional size test)
  "Read a tsv, csv, or ssv INPUT and convert it to a hash table.
INPUT can be a file or a buffer or a string.

For the meaning of ARG, see ‘org-table-convert-region’.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (if (xht--str-nw? input)
      (with-temp-buffer
        (autoload #'org-table-convert-region "org-table")
        (insert input)
        (org-table-convert-region (point-min) (point-max) arg)
        (h<-orgtbl (xht--buff-str-no-prop) size test))
    (h-new size test)))

(defun xht<--tsv (input &optional size test)
  "Create a hash table with initial values according to TSV.
INPUT can be a file or a buffer or a string.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (xht<--*sv input '(16) size test))

(defun xht<--csv (input &optional size test)
  "Create a hash table with initial values according to CSV.
INPUT can be a file or a buffer or a string.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (xht<--*sv input '(4) size test))

(defun xht<--ssv (input &optional size test)
  "Create a hash table with initial values according to SSV.
INPUT can be a file or a buffer or a string.

SSV here means a table using 2+ spaces, or a TAB, as field
separators.

SIZE is the nominal initial size of the table to be created.
If nil, it defaults to the result of ‘xht--init-size’, which see.

For the meaning of TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (xht<--*sv input 2 size test))

;;;;;;; Unknown thing

(xht--describe
  "Functions that read an unknown object, try to guess what it is, then
convert it to a hash table.")

(defun h<-it (thing &rest rest)
  "Convert THING to hash table. Try to guess what it is.

When in doubt, it prefers higher dimension. So if type is alist,it
uses ‘h<-alist*’, not ‘h<-alist’. Likewise ‘h<-plist*’ instead of
h-plist’. If you prefer the latter, be explicit, passing the exact
function instead.

And since it uses h-type for type-detection, it tries to match:
- lol, alist, and plist before list — so when you prefer plain
  lists, be explicit, passing ‘h<-list’ instead.

- *sv before lines — so when you prefer plain lines, be explicit,
  passing ‘h<-lines’ instead.

REST are arguments that might be taken by the respective conversion
functions."
  (declare (pure t) (side-effect-free t))
  (let ((type (h-type thing)))
    (pcase type
      (:ht         (apply #'h<-ht         thing  rest))
      (:lol        (apply #'h<-lol        thing  rest))
      (:tsv        (apply #'h<-tsv        thing  rest))
      (:csv        (apply #'h<-csv        thing  rest))
      (:ssv        (apply #'h<-ssv        thing  rest))
      (:kvl        (apply #'h<-kvl        thing  rest))
      (:null       (apply #'h<-list       thing  rest))
      (:list       (apply #'h<-list       thing  rest))
      (:json       (apply #'h<-json*      thing  rest))
      (:alist      (apply #'h<-alist*     thing  rest))
      (:plist      (apply #'h<-plist*     thing  rest))
      (:empty      (apply #'h<-tsv        thing  rest))
      (:lines      (apply #'h<-lines      thing  rest))
      (:orgtbl     (apply #'h<-orgtbl     thing  rest))
      (:vector     (apply #'h<-vector     thing  rest))
      (:cons-pair  (apply #'h<-cons-pair  thing  rest))
      (_ (user-error "Cannot convert %s to hash table" type)))))

;;;;;; Predicates

(xht--describe
  "Miscellaneous predicates, mostly about checking type for conversion
purposes.")

;;;;;;; Type

(xht--describe
  "For hash tables, use ‘h?’, which is the same as ‘ht?’: an alias to Emacs'
internal ‘hash-table-p’.")

(defun h-kvl? (str &optional obsolete)
  "Could string STR be a KVL?
Only if, after stripped of any comments and section headers, and
trimmed of any leading and trailing whitespace, every line has
exactly one common separator. Comments and section headers are
matched with ‘h-kvl-comment-re’ and ‘h-kvl-section-re’,
respectively, which see.

An OBSOLETE form is
  \(h-kvl? str &optional sep)
where the optional arg SEP specified the field delimiter. This use
is deprecated. The delimiter is now passed by the variable
h-kvl-sep-re’ (which see), which should be let-bound when the
default value is not expected."
  (declare (advertised-calling-convention (str) "2.0")
           (pure t) (side-effect-free t))
  (let ((strip-re (format "%s\\|%s" h-kvl-comment-re h-kvl-section-re))
        (delim-re (or obsolete h-kvl-sep-re))
        (lines    (s-lines str))
        (kvl?     t))
    (while (and lines kvl?)
      (--> (pop lines)
           (s-replace-regexp strip-re "" it)
           s-trim
           (or (s-blank-str? it)
               (= 1 (s-count-matches delim-re it))
               (setq kvl? nil))))
    kvl?))

(defun h-alist? (list)
  "Non-nil if and only if LIST is a non-nil alist with simple keys."
  ;; Adapted from json.el's ‘json-alist-p’.
  (declare (pure t) (side-effect-free t))
  (when list
    (let ((l list))
      (while (consp l)
        (setq l (if (and (consp (car l))
                         (atom (caar l)))
                    (cdr l)
                  'not-alist)))
      (null l))))

(defun h-plist? (list)
  "Non-nil if and only if LIST is a non-nil plist with non-nil atom keys."
  (declare (pure t) (side-effect-free t))
  (when list
    (let ((l list))
      (while (consp l)
        (setq l (if (consp (cdr l))
                    (cddr l)
                  'not-plist)))
      (null l))))

(defun h-lol? (obj)
  "Return t if OBJ is a list of lists."
  (declare (pure t) (side-effect-free t))
  (and obj
       (listp obj)
       ;; we don't want it if it's only header (more likely it'd be an alist)
       (> (length obj) 1)
       ;; then all items must be lists:
       (--all? (and (listp it)
                    ;; and none of them cons cells:
                    (listp (cdr it))
                    ;; for now we're assuming exactly 2D (not higher), so no
                    ;; item of the internal lists should itself be a list;
                    ;; this distinguishes it from 2D alists — but will break
                    ;; if we ever want to make it 3D, in which case we'd need
                    ;; better detection.
                    (-all? #'nlistp it))
               obj)
       ;; for our purposes, no item's length should be larger than the
       ;; header's (but ok if they're shorter: trailing nils)
       (--> (-map #'length obj)
            (= (car it) (-max it)))))

(defun h-orgtbl? (str)
  "Could string STR be an Org Table?"
  (autoload #'org-at-table-p "org")
  (with-temp-buffer
    (insert str)
    (goto-char 2)
    (org-at-table-p)))

(defun h-json? (str)
  "Could string STR be a JSON object?
Return nil if, after trimming, it isn't enclosed in {} or [].
Return t if inside is either empty or returns non-nil when
converted with ‘json-read-from-string’ ignoring errors."
  (let ((trimmed (s-trim str)))
    (unless (string= "" trimmed)
      (let ((1st  (aref trimmed 0))
            (last (aref trimmed (1- (length trimmed)))))
        (and (or (and (= 1st  ?{) (= last  ?}))
                 (and (= 1st ?\[) (= last ?\])))
             (or (not (xht--str-nw? (substring trimmed 1 -1)))
                 (ignore-errors (json-read-from-string str)))
             t)))))

(defun h-tsv? (str)
  "Could string STR be a TSV?
Only if all non-commented, non-blank lines have at least one tab
not in the beginning of the line. Commented means `^\s*#` in the
line.

Note that if the TSV has two fields, and therefore exactly one tab
per line, it can't be distinguished from a KVL-with-tab using tab
as separator, since it can't be detected whether the first line is
a TSV header or a KVL entry. So functions such as ‘h<-it’, ‘h-it=’,
h-type’, and others may guess wrongly."
  (declare (pure t) (side-effect-free t))
  (xht--*sv? "\t" str))

(defun h-csv? (str)
  "Could string STR be a CSV?
Only if all non-commented, non-blank lines have at least one comma
not in the beginning of the line. Commented means `^\s*#` in the
line."
  (declare (pure t) (side-effect-free t))
  (xht--*sv? "\"?[ \t]*,[ \t]*\"?" str))

(defun h-ssv? (str)
  "Could string STR be an SSV?
Only if all non-commented, non-blank lines have at least TWO
whitespaces or a tab not in the beginning of the line. Commented
means `^\s*#` in the line."
  (h-tsv? (s-replace-regexp "  +" "\t" str)))

(defun xht--*sv? (sep str)
  "Could string STR be a *SV?
SEP is a regex representing the separator.
Helper for ‘h-tsv?’, ‘h-csv?’, ‘h-ssv?’."
  (declare (pure t) (side-effect-free t))
  (->> str  s-lines
       (--remove (or (not (xht--str-nw? it))
                     (s-matches? "^[ \t]*#" it)))
       (--all? (s-matches? sep it))))

;;;;;;; Dimension

(xht--describe
  "See ‘h-1d?’, ‘h-2d?’, ‘h-nested?’.")

;;;;;; Properties and dimension

(xht--describe
  "Functions to look up properties and dimension of hash tables (and of other
objects convertible to hash tables).

See also: ‘xht--props=’, ‘xht--props-and-data=’, ‘h-props=’")

;;;;;;; Hash tables

(defun h-prop (table prop)
  "Return the value of property PROP of hash table TABLE.

Valid values for PROP are:
- size
- test
- weakness
- rehash-size
- rehash-threshold

PROP may be given as a symbol, string or keyword.
So any of these work: 'test   \"test\"  :test

Note that 'size' is the nominal size, which defaults to 65
when created. For the actual size, use ‘ht-size’."
  (declare (pure t) (side-effect-free t))
  (when (member (h-as-symbol prop)
                '(size test weakness rehash-size rehash-threshold))
    (--> (h-as-string prop)
         (s-lex-format "hash-table-${it}")
         read
         (funcall it table))))

(defun h-props (table)
  "Return a hash table containing properties of hash table TABLE.
The property 'data isn't included. The reason is that while all
other properties can be compared directly, the value of 'data is a
plist, and plists with different ordering can be equivalent. This
would create an exception for functions using this function. For
comparing just the data from two hash tables, use ‘h=’."
  (declare (pure t) (side-effect-free t))
  (let* ((props '(size
                  test
                  weakness
                  rehash-size
                  rehash-threshold))
         (lprops (length props))
         (hprops (h-new (xht--init-size lprops) 'equal)))
    (dolist (prop props hprops)
      (h-put! hprops prop (h-prop table prop)))))

(defun h-dim (table)
  "Determine dimension of hash table TABLE.
Return:
- error if not a hash table.
- 0     if empty hash table.
- 1     if simple key–value: no value is itself a hash table.
- 1.5   if *some* values are hash tables of dimension 1, and some
        values aren't hash tables.
- 2     if ‘h-2d?’ returns t
- 2.5   if *every* value is a hash table of dimension at least 2,
        but some are higher.
- 3     if *every* value is a hash table of dimension 2.
... etc.

Generally: 1+ (minimum common dimension of hash table values) +
              (if any ht has dim > mcd(dims), then 0.5, else 0)

Regarding conversions to hash table, when:
- from KVL, vector, list: should be of dim = 1.
- from lol, orgtbl, *sv:  should be of dim = 2.
- from alist, plist: 1 when simple, and usually 1.5 when nested.
                      (but could be 2+ with the same logic)"
  (declare (pure t) (side-effect-free t))
  (cond
   ((not (ht? table)) (error "Not a hash table"))
   ((h-empty? table) 0)
   (t (let ((values (h-values table 'reverse)))
        (cond
         ((-none? #'ht? values) 1)
         ((-all?  #'ht? values) (let* ((dims (-map #'h-dim
                                                   values))
                                       (mind (-min dims))
                                       (maxd (-max dims))
                                       (frac (if (equal mind maxd)
                                                 0
                                               0.5)))
                                  (+ 1 mind frac)))
         ((-some? #'ht? values) 1.5)
         (t (error "h-dim error: ?")))))))

(defun h-1d? (obj)
  "Return t if OBJ is a 1D hash table, nil otherwise.
It's 1D if, and only if:

1. it's a hash table AND

2. none of the values are hash tables."
  (declare (pure t) (side-effect-free t))
  (and (ht? obj)
       (= 1 (h-dim obj))))

(defun h-2d? (obj)
  "Return t if OBJ is a 2D hash table, nil otherwise.
Return t if, and only if:

1. it's a hash table AND

2. all values are regular hash tables AND

3. none of the keys of these values (the \"header labels\") are
   themselves hash tables AND

4. at least one of these keys has as property: its value is equal
   to OBJ's key for all of OBJ's keys. In other words, an id ID
   exists if, and only if, (h-get* key id) returns key for all
   keys."
  (declare (pure t) (side-effect-free t))
  (and (ht? obj)
       (h-2d-header obj)
       t))

(defun h-nested? (obj)
  "Return t if OBJ is a nested hash table, nil otherwise.
It's nested if, and only if:

1. it's a hash table AND

2. at least one value is a hash table."
  (declare (pure t) (side-effect-free t))
  (and (h? obj)
       (--any (h? it)
              (h-values obj 'reverse))
       t))

;;;;;;; Generalization to other types

(defun h-it-dim (obj)
  "Determine dimension of object OBJ.
This is a generalization of ‘h-dim’ (which see) to other objects."
  (declare (pure t) (side-effect-free t))
  (xht--itify #'h-dim obj))

(defun h-it-empty? (obj)
  "Return t if OBJ is empty, nil otherwise.
This is a generalization of ‘h-empty?’ (which see) to other objects."
  (declare (pure t) (side-effect-free t))
  (xht--itify #'h-empty? obj))

(defun h-it-1d? (obj)
  "Return t if OBJ is of dimension 1, nil otherwise.
This is a generalization of ‘h-1d?’ (which see) to other objects."
  (declare (pure t) (side-effect-free t))
  (xht--itify #'h-1d? obj))

(defun h-it-2d? (obj)
  "Return t if OBJ is of dimension 2, nil otherwise.
This is a generalization of ‘h-2d?’ (which see) to other objects."
  (declare (pure t) (side-effect-free t))
  (xht--itify #'h-2d? obj))

(defun h-it-nested? (obj)
  "Return t if OBJ is nested, nil otherwise.
This is a generalization of ‘h-nested?’ (which see) to other
objects."
  (declare (pure t) (side-effect-free t))
  (xht--itify #'h-nested? obj))

(defun xht--itify (fun-of-table obj)
  "Generalize function f(table) FUN-OF-TABLE to f(OBJ).
Helper function."
  (declare (pure t) (side-effect-free t))
  (funcall fun-of-table (if (ht? obj)
                            obj
                          (h<-it obj))))

;;;;;; Writing and reading
;;;;;;; Write to file

(xht--describe
  "Functions that write to file.")

(defun h-write! (filename obj &optional mustbenew)
  "Write OBJ to file FILENAME.
OBJ may represent any of the input and output data structures of
the conversion functions.

- If they are of string type (as kvl, *sv, org tables), they are
  written as they are — strings.

- Anything else (numbers, lists, vectors, hash tables) is written
  as it looks like — as an s-expression.

Filename is created if nonexistent, and overwritten if existent —
but the parent directory, if it doesn't exist, is only created if
the 'f library is installed and loaded.

The optional arg MUSTBENEW, if non-nil, insists on a check for an
existing file with the same name. If MUSTBENEW is ‘excl’, that
means you get an error if the file already exists; never overwrite.
If MUSTBENEW is neither nil nor ‘excl’, that means ask for
confirmation before overwriting, but do go ahead and overwrite the
file if you confirm.

Note that hash table objects are written using their
#s(hash-table…) raw representation. For finer control of how hash
tables are written, including the option of using (h*…) or (ht…)
representations, see the specialized ‘h-write-ht-like!’."
  (when (fboundp 'f-parent)
    (mkdir (f-parent filename) '-p))
  (write-region (h-as-string obj)
                nil filename nil nil nil mustbenew))

(defun h-write-ht-like! (filename format ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as FORMAT.
For more about FILENAME and MUSTBENEW, see ‘h-write!’.

HT-LIKE may come in any of the following autodetected formats:

As Lisp objects:
1. #s(hash-table…) = regular representation
2. (ht…) form
3. (h*…), (h-s*…), (h-t*…), or (h-st*…) form

or any of the above formatted as string (presumably with %S)

FORMAT is how you want your hash table to be written to FILENAME,
and may be any one of the following:
1. htbl-cm  :: #s(hash-table…) = compact representation
2. h*-cm    :: (h*…) in compact format
3. ht-cm    :: (ht…) in compact format

4. htbl-pp  :: #s(hash-table…) = in pretty-printed format
5. h*-pp    :: (h*…) in pretty-printed format
6. ht-pp    :: (ht…) in pretty-printed format

For convenience, you may drop the -pp and it will be implied:
4. htbl     :: #s(hash-table…) = in pretty-printed format
5. h*       :: (h*…) in pretty-printed format
6. ht       :: (ht…) in pretty-printed format

\(It's implicit that these are strings, since we're writing to a
file, so we can dispense '-str' suffixes)

Any of these FORMAT options may be passed as either a string, a
keyword, or a symbol — as you prefer. So, for example, any among
\"h*-pp\", :h*-pp, or 'h*-pp works.

When the table being read has an equality test different than
'equal and either h*-cm or h*-pp is chosen, the output form will be
made into (h-st*…), for which size and test are explicit, thereby
preserving the original test equality information. (This can't be
done with the (ht…) form, for which the equality test, if not
'equal, would be implicitly converted to 'equal.)


Which of the five should you choose?

- Either of the pretty-printed h-formats are much more readable by
  humans and can be easily browsed in the command line with the
  'less' command line utility. Disadvantages:

  1. conversion to and from h-formats costs a bit of time, which
     might become noticeable when dealing with larger tables.

  2. less portable, depends on this library.

  3. occupies a tiny bit more of disk space (about which you
     probably don't care) because of the added whitespace.


- The compact regular form and the compact h-forms often occupy a
  single line (unless their keys or values have line breaks), and
  as such aren't suitable for using with 'less' — it'll flow
  straight to the end. And if you read any of them into a buffer,
  you may run into Emacs problem of displaying buffers with very
  long lines.

- The regular form has the advantage of being universal, which may
  be preferable if the file is shared or if for some reason you
  don't have access to this and/or the 'ht libraries. But it's
  harder for human reading, especially when nested, because it's
  displayed unformatted in one line, and because of its metadata.

- The regular form is also faster. If you're dealing with huge hash
  tables, it'll be faster to write and read regular compact forms
  than converting them.

- The h-forms may have more security issues when reading files from
  untrustworthy sources. Consider that this:

  #+begin_src emacs-lisp
    #s(hash-table test equal data (:a (+ 2 3)))
  #+end_src

  has as value for :a the quoted expression '(+ 2 3), whereas any
  of these:

  #+begin_src emacs-lisp
    (h*  :a (+ 2 3))
    (ht (:a (+ 2 3)))
  #+end_src

  will see it evaluated to 5 if you read with ‘h-read-h’ a file
  containing it. This seems unlikely to be a problem if you control
  the reading and writing. In any case, you can inspect it by using
h-read-list’ instead — which will assume it's just a plain list
  and return it to you quoted, without evaluating it. If it's a
  huge expression you may want to apply pp to it. Compare the
  results of:

  #+begin_src emacs-lisp
    (h-read-h        \"(ht (:a (+ 2 3)))\")
    (h-read-list     \"(ht (:a (+ 2 3)))\")
    (pp (h-read-list \"(ht (:a (+ 2 3)))\"))
  #+end_src

Any of the h-forms can be easily read and re-converted to any
hash-table–like format with ‘h-format’, which see.

For convenience, partial-application functions are available for
the different formats:
- ‘h-write-htbl-cm!’,  ‘h-write-htbl-pp!’,
- ‘h-write-h*-cm!’,    ‘h-write-h*-pp!’,
- ‘h-write-ht-cm!’,    ‘h-write-ht-pp!

The pp ones are aliased, respectively, to the shorter:
- ‘h-write-htbl!’, ‘h-write-h*!’ and ‘h-write-ht!’.

See also: ‘h-read-h’ and ‘h-read’."
  (h-write! filename
            (h-format (pcase (h-as-keyword format)
                        (:h*-cm              :h*-cm-str)
                        (:ht-cm              :ht-cm-str)
                        (:htbl-cm            :htbl-cm-str)
                        ((or :h* :h*-pp)     :h*-pp-str)
                        ((or :ht :ht-pp)     :ht-pp-str)
                        ((or :htbl :htbl-pp) :htbl-pp-str)
                        (_ (user-error "‘h-write-ht!’: Invalid format %s"
                                       format)))
                      ht-like)
            mustbenew))

(defsubst h-write-htbl-cm! (filename ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as 'htbl-cm.
For more about this output format, see ‘h-write-ht!’.
For more about FILENAME and MUSTBENEW, see ‘h-write!’."
  (h-write-ht-like! filename 'htbl-cm  ht-like mustbenew))

(defsubst h-write-htbl-pp! (filename ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as 'htbl-pp.
For more about this output format, see ‘h-write-ht!’.
For more about FILENAME and MUSTBENEW, see ‘h-write!’."
  (h-write-ht-like! filename 'htbl-pp  ht-like mustbenew))

(defsubst h-write-h*-cm! (filename ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as 'h*-cm.
For more about this output format, see ‘h-write-ht!’.
For more about FILENAME and MUSTBENEW, see ‘h-write!’."
  (h-write-ht-like! filename 'h*-cm ht-like mustbenew))

(defsubst h-write-ht-cm! (filename ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as 'ht-cm.
For more about this output format, see ‘h-write-ht!’.
For more about FILENAME and MUSTBENEW, see ‘h-write!’."
  (h-write-ht-like! filename 'ht-cm ht-like mustbenew))

(defsubst h-write-h*-pp! (filename ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as 'h*-pp.
For more about this output format, see ‘h-write-ht!’.
For more about FILENAME and MUSTBENEW, see ‘h-write!’.

Alias: ‘h-write-h*!’."
  (h-write-ht-like! filename 'h*-pp ht-like mustbenew))

(defsubst h-write-ht-pp! (filename ht-like &optional mustbenew)
  "Write hash table HT-LIKE to file FILENAME formatted as 'ht-pp.
For more about this output format, see ‘h-write-ht!’.
For more about FILENAME and MUSTBENEW, see ‘h-write!’.

Alias: ‘h-write-ht!’."
  (h-write-ht-like! filename 'ht-pp ht-like mustbenew))

;;;;;;; Read

(xht--describe
  "Functions that read objects, possible from a file or a buffer.
The generic function is ‘h-read’, with others that take one argument offered
for convenience — so, for example:

#+begin_src emacs-lisp
  (h-read-alist obj)
#+end_src

is the same as

#+begin_src emacs-lisp
  (h-read 'alist obj)
#+end_src

Note that ‘h-read-htbl’ expects OBJ to be of regular hash table type:
#s(hash-table…). But we also have a ‘h-read-h’ that also accepts, quoted,
the h-forms: '(ht…), '(h*…), '(h-s*…), '(h-t*…), or '(h-st*…). It also accepts
any of these passed as a string: \"#s(hash-table…)\", \"(h*…)\", etc.

This makes it flexible to read from files, and to have hash tables stored in
files as h-forms.")

(defun h-read (type obj)
  "Deal with object OBJ, expected to be of type TYPE.
- If OBJ, a string, is a valid path of an existing file,
  read as string the contents of file.

- If OBJ is a buffer, read as string the contents of buffer.

- Otherwise, take OBJ as it is.

Then read it and test it according to ‘xht--to-type’, which see.

TYPE is a symbol, and can be one of:
- 's     OR  'str   OR  'string
- 'vec   OR  'vector
- 'alist
- 'plist
- 'list
- 'cons-pair
- 'lol
- 'htbl
- 'ht-like

When dealing with TSV, CSV, SSV, Org Table, or JSON:
use 'str (or 's, or 'string)
or the respective associated functions — same thing.

When dealing with hash tables, you have two options:
- 'htbl expects something looking strictly like #s(hash-table…),
   and signals error otherwise.
- 'ht-like is flexible and accepts any ht-like object — which is a
   Lisp object or string that looks like or contains one among:
   #s(hash-table…), (ht…), (h*…), (h-s*…), (h-t*…), or (h-st*…).

TYPE represents the expectation of what type the string input
should be, so it signals error when mismatched. So if, for example,
you expect your input to be a hash table #s(hash-table…), this:

#+begin_src emacs-lisp
  (->> \"/path/to/hash-table.el\"  (h-read 'htbl)  h->lol)
#+end_src

would read this file looking for a hash table, and return it as
such (as Lisp object, no longer a string), to be converted to a
list of lists as specified. If it found, say, an alist in the file
it would signal error.

You could also use the equivalent:

#+begin_src emacs-lisp
  (-> \"/path/to/hash-table.el\"  h-read-htbl  h->lol)
#+end_src

and this also works:

#+begin_src emacs-lisp
  (-> (ht (:a 1) (:b 2))  h-read-htbl  h->lol)
#+end_src

In this case, ‘h-read-htbl’ returns the very hash table read, like
identity’. No problem, but unnecessary — this would suffice:

#+begin_src emacs-lisp
  (-> (ht (:a 1) (:b 2))  h->lol)
#+end_src

For convenience, partial-application functions are available which
receive only OBJ as argument:

h-read-s

plus

h-read-kvl’, ‘h-read-tsv’, ‘h-read-csv’, ‘h-read-ssv’,
h-read-json’, ‘h-read-orgtbl’, ‘h-lines’ (all are ‘h-read-s’),

plus

h-read-lol’, ‘h-read-alist’, ‘h-read-plist’, ‘h-read-list’,
h-read-cons-pair’, ‘h-read-vector

and for hash tables:
- ‘h-read-htbl’ for (h-read 'htbl…)    = #s(hash-table…) only
- ‘h-read-h’    for (h-read 'ht-like…) = any ht-like form.

See also: ‘h-write!’ and ‘h-write-ht-like!’."
  (declare (side-effect-free t))
  (cond ((stringp obj)      (xht--to-type
                             (cond ((not (xht--str-nw? obj)) obj)
                                   ((file-exists-p obj)
                                    (xht--file-str-no-prop obj))
                                   (t obj))
                             type))
        ((bufferp obj)      (xht--to-type (xht--buff-str-no-prop)
                                          type))
        ((eq type 'ht-like) (xht--to-type (format "%S" obj) type))
        (t             obj)))

(defsubst h-read-ht-like (obj)
  "Deal with object OBJ, expected to be ht-like.
For more, see ‘h-read’.
Alias: ‘h-read-h’."
  (h-read 'ht-like obj))

(defsubst h-read-htbl (obj)
  "Deal with object OBJ, expected to be a hash table.
For more, see ‘h-read’."
  (h-read 'htbl obj))

(defsubst h-read-s (obj)
  "Deal with object OBJ, expected to be a string.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-kvl (obj)
  "Deal with object OBJ, expected to be a KVL.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-tsv (obj)
  "Deal with object OBJ, expected to be a TSV.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-csv (obj)
  "Deal with object OBJ, expected to be a CSV.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-ssv (obj)
  "Deal with object OBJ, expected to be a SSV.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-json (obj)
  "Deal with object OBJ, expected to be JSON.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-lines (obj)
  "Deal with object OBJ, expected to be lines of text.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-orgtbl (obj)
  "Deal with object OBJ, expected to be an org table.
For more, see ‘h-read’."
  (h-read 'str obj))

(defsubst h-read-lol (obj)
  "Deal with object OBJ, expected to be a list of lists.
For more, see ‘h-read’."
  (h-read 'lol obj))

(defsubst h-read-alist (obj)
  "Deal with object OBJ, expected to be an association list.
For more, see ‘h-read’."
  (h-read 'alist obj))

(defsubst h-read-plist (obj)
  "Deal with object OBJ, expected to be a property list.
For more, see ‘h-read’."
  (h-read 'plist obj))

(defsubst h-read-list (obj)
  "Deal with object OBJ, expected to be a list.
For more, see ‘h-read’."
  (h-read 'list obj))

(defsubst h-read-cons-pair (obj)
  "Deal with object OBJ, expected to be a cons pair.
For more, see ‘h-read’."
  (h-read 'cons-pair obj))

(defsubst h-read-vector (obj)
  "Deal with object OBJ, expected to be a vector.
For more, see ‘h-read’."
  (h-read 'vector obj))

;;;;;; Miscellaneous
;;;;;;; Format object as symbol, string, keyword, or number

(xht--describe
  "Functions to turn object into a desirable format.

These functions were initially internal only (and therefore named
as 'xht--as-string', etc), since they weren't directly about hash
tables.

But they are useful for formatting keys and values, which is a
common enough operation — so I renamed them and made them public.
The alternative would have been to have you either write the code
yourself every time you wanted to do these operations, or else use
the longer-named internal functions, both of which seemed worse.")

(defun h-as-string (obj)
  "Return OBJ as a string."
  (declare (pure t) (side-effect-free t))
  (cond ((keywordp obj) (xht--keyword->string obj))
        ((symbolp  obj) (format "%s" obj))
        ((stringp  obj) obj)
        (t              (format "%S" obj))))

(defun h-as-keyword (obj)
  "Return OBJ as a keyword."
  (declare (pure t) (side-effect-free t))
  (cond ((keywordp obj) obj)
        ((symbolp  obj) (xht--symbol->keyword obj))
        ((stringp  obj) (xht--string->keyword obj))
        (t              (xht--string->keyword (format "%S" obj)))))

(defun h-as-symbol (obj)
  "Return OBJ as an interned symbol."
  (declare (pure t) (side-effect-free t))
  (cond ((keywordp obj) (xht--keyword->symbol obj))
        ((symbolp  obj) obj)
        ((stringp  obj) (intern obj))
        (t              (intern (format "%S" obj)))))

(defun h-as-number (obj)
  "Return OBJ as a number, if convertible.
If not convertible, signal error."
  (declare (pure t) (side-effect-free t))
  (cond ((numberp obj) obj)
        ((stringp obj) (xht--string->number  obj))
        ((symbolp obj) (xht--symbol->number  obj)) ; same for keyword
        (t             (error "h-as-number: Can't convert %s" obj))))

;;;;; Specialized hash table operations
;;;;;; “Hash tabular”: 2D-specific operations

(xht--describe
  "Functions to operate on two-dimensional hash tables.

A 2D hash table looks like this:

#+begin_src emacs-lisp
  (h* \"n01\" (h* \"n\" \"n01\"
                \"name\" \"Alice\")
                \"age\" \"42\"
                \"editor\" \"Emacs\"
      \"n02\" (h* \"n\" \"n02\"
                \"name\" \"Emily\"
                \"age\" \"21\"
                \"editor\" \"Nano\"))
#+end_src

It looks like this when converted to an org table:

#+begin_src org
| n   | name  | age | editor |
|-----+-------+-----+--------|
| n01 | Alice |  42 | Emacs  |
| n02 | Emily |  21 | Nano   |
#+end_src

or to a list of lists:

#+begin_src emacs-lisp
  '((\"n\"   \"name\"  \"age\" \"editor\")
    (\"n01\" \"Alice\" \"42\"  \"Emacs\")
    (\"n02\" \"Emily\" \"21\"  \"Nano\"))
#+end_src

or to SSV:

#+begin_example
n    name   age  editor
n01  Alice  42   Emacs
n02  Emily  21   Nano
#+end_example

or to JSON:

#+begin_src javascript
{
  \"n01\": {
    \"n\":      \"n01\",
    \"name\":   \"Alice\"
    \"age\":    \"42\",
    \"editor\": \"Emacs\",
  },
  \"n02\": {
    \"n\":      \"n02\",
    \"name\":   \"Emily\"
    \"age\":    \"21\",
    \"editor\": \"Nano\",
  }
}
#+end_src

Name of its elements (I'm removing the string quotes here):
- header: '(n name age editor)
- idcol:  '(n n01 n02)
- id:     n

Either of these three triads of labels could be given in reference
to \"Alice\" in the example above:

| n01 | name         | Alice          |
|-----+--------------+----------------|
| key | key of value | value of value |
| key | field        | value          |
| row | column       | cell           |

- The first is exact but awkward.

- In code I often opted for the second.

- The third is also nice, but row and column most often alludes to the entire
  row and column, not to the first item of either — to which we want to refer
  here. But it's understood if we say that the \"intersection of row 'n01' and
  column 'name' is 'Alice'\". This is \"field 'name' of key 'n01'\" (second)
  or the awkward \"the value of the key 'name' of the value of key 'n01'\"
  (first).

See also: ‘h-1d<-2d’ and ‘h-2d<-1d’ under
General hash table operations > Conversion > 2D > Hash table to hash table.")

;;;;;;; Creation

(xht--describe "Functions for creating 2D hash tables.")

(defun h-2d-new (keys header &optional size test)
  "Return a 2D hash table from KEYS and HEADER.
Both must be lists.

If either is nil, or not a list, return empty hash table.

All field values are set to nil, where field is
an element of header.

For the meaning of SIZE and TEST, see ‘h-new’."
  (declare (pure t) (side-effect-free t))
  (if (not (and keys header (listp keys) (listp header)))
      (h-new)
    (let* ((id    (car header))
           (htbl  (h-new (or size (length keys))
                         test))
           (nils  (-repeat (length header) nil))
           (hnils (h<-plist (-interleave header nils))))
      (dolist (key keys htbl)
        (h-put! htbl key
                (h-put hnils id key))))))

;;;;;;; Information

(xht--describe
  "Functions for retrieving information about 2D hash tables.
Note: ‘h-2d-keys’ is an alias to ‘h-lkeys’ (for consistency).")

(defun h-2d-header (table2d)
  "Return the header of 2D hash table TABLE2d.
The header is the union of all keys-of-keys of TABLE2D.

This function does the heavy work to correctly guess what the
table's ID would be — which ‘h-2d-id’ then uses."
  (declare (pure t) (side-effect-free t))
  (let (values headers-lol common union candidates keys id)
    (when (h? table2d)
      (unless (h-empty? table2d)
        (setq values (h-values table2d))
        (when (-all? #'h? values)
          (setq headers-lol (-map    #'h-keys        values)
                common      (-reduce #'-intersection headers-lol)
                union       (-reduce #'-union        headers-lol))
          (unless (-any? #'h? union)
            (when common
              (setq keys (h-keys table2d))
              (dolist (candidate common)
                (when (-none? #'null
                              (--map (equal it (ht-get*
                                                table2d it candidate))
                                     keys))
                  (push candidate candidates)))
              (setq id (-last-item candidates))
              (when id
                (->> union  (-remove-item id)  (cons id))))))))))

(defun h-2d-idcol (table2d)
  "The ID column of 2D hash table TABLE2D, as list. It's ID + keys."
  (declare (pure t) (side-effect-free t))
  (let ((id (h-2d-id table2d)))
    (when id
      (cons id (h-2d-keys table2d)))))

(defun h-2d-id (table2d)
  "Find the ID-like element of the header of 2D hash table TABLE2D.
It's the one associated with the table's keys.
If none is found, return nil, in which case it's not a 2D hash table.
If more than one is found (unlikely), use the first."
  (declare (pure t) (side-effect-free t))
  (car (h-2d-header table2d)))

(defun h-2d-row (table2d key &optional header)
  "List row from 2D hash table TABLE2D given KEY and HEADER.
Example: (h-2d-row tb2 \"id01\" '(:id :name :age)) could
return: '(\"id01\" \"Mary\" 27).

HEADER can be given in some desirable permutation.
If HEADER is nil, it will be inferred with ‘h-2d-header’."
  (declare (pure t) (side-effect-free t))
  (let ((id (h-2d-id table2d))
        res)
    (when id
      (unless header
        (setq header (h-2d-header table2d)))
      (if (equal key id)
          header
        (dolist (field (cdr header) (cons key (nreverse res)))
          (push (ht-get* table2d key field) res))))))

(defun h-2d-col (table2d col &optional idcol)
  "List column from 2D hash table TABLE2D given COL and IDCOL.
Example: (h-2d-col tb2 :name '(:id id01 id03 id02)) could
return: '(:name \"Mary\" \"John\" \"Jane\").

IDCOL is the ID plus keys.
It can be given in some desirable permutation.
If IDCOL is nil, it will be inferred with ‘h-2d-idcol’."
  (declare (pure t) (side-effect-free t))
  (let ((id (h-2d-id table2d))
        res)
    (when id
      (unless idcol
        (setq idcol (h-2d-idcol table2d)))
      (if (equal col id)
          idcol
        (dolist (key (cdr idcol) (cons col (nreverse res)))
          (push (ht-get* table2d key col) res))))))

;;;;;;; Mapping

(xht--describe
  "Functions for mapping 2D hash tables.")

;;;;;;;; to hash table 2D

(xht--describe
 "Functions that apply three functions or forms to every key–field–value
triad of a 2D hash table and write the results to a hash table.

You can think of the key as the label of the row of the equivalent
org table it would produce; of the field as the label of the
column; and of the value as the contents of the cell.

For example, if you want to produce a fresh table2d whose keys are
upcased, fields are downcased, and the values \(currently integers)
are increased by 1, you could run:

#+begin_src emacs-lisp
  (h-2d-hmap (lambda (key _field _value) (upcase key))
             (lambda (_key field _value) (downcase field))
             (lambda (_key _field value) (1+ value))
             table2d)
#+end_src

The underlines above are so that there's no warnings about unused
variables.

The fact that in each function the only variable used was that
which it changes needed not be so. You could have well
had (upcase (concat key \"-\" field)) in the first one, for
example. But it's unlikely you'd want that. Most often you'd want
to only use key for the first one, only field for the second one,
and the third one could have any combination.

Finally, note that when the field is the table's ID (the table's
\"first column\"), the value will be the newly calculated key
instead of applying the value function to it. This is almost
certainly what you wanted to do: apply the value functions to all
columns but the first one, which is what you'd do in a spreadsheet
with labeled rows and columns.

Note that the item is only added if both KEY-FUN and FIE-FUN return
non-nil. So no spurious (nil, something) triad results from some
reasonable conditional KEY-FUN such as:

#+begin_src emacs-lisp
  (lambda (key, field, value) (when (> value 2) key))
#+end_src

In the rare case where for some reason you chose to have nil as a
key, you'll have to treat it specially.

IMPORTANT: unlike mapping to a list, mapping to a hash table
demands that the results of the keys be unique. So you must pay
attention to possible collisions. If, for example, in the case
above the original table had both \"a\" and \"A\" as original keys,
one of them would end up overwritten, because both return \"A\"
when upcased. Which one will depend on the key order. So:

  actual size of resulting table  ≤  size of original table.

In mathematical terms, excepting the rare case of a nil key in the
original, the number of keys in the resulting hash table will match
the number of keys in the original one if, and only if:

1. there's a bijection between the set of keys and the set of
   key-fun(key,field,value).

2. there's no (key,field,value) for which either key-fun(key,field,value) or
   fie-fun(key,field,value) returns nil.


IMPORTANT: Just because you can pass three arbitrary functions
using all the three variables in each of them doesn't mean you
should. If you, for example, selectively change a field for some
keys but not others, you'll likely split the column, so one of them
will have empty values where the other has something, and
vice-versa. And other such odd operations. It seems unlikely that
you'd want that.


Guideline for the '2d-hmap family' of functions:

- the added 'h' means 'result is a hash table'

-  an added '-' means 'anaphoric: enter 3 forms, not 3 lambdas'

-  an added '!' means 'destructive: modify TABLE instead of
                       creating a fresh one'

In the anaphoric versions, if you don't use any of the let-bound
variables key, field, or value in any of the forms, it's ok — no
warnings.

Summary:

|-----------------+--------------+---------------------|
|                 | 3 lambdas    | 3 forms (anaphoric) |
|-----------------+--------------+---------------------|
| non-destructive | ‘h-2d-hmap’  | ‘h--2d-hmap’        |
| destructive     | ‘h-2d-hmap!’ | ‘h--2d-hmap!’       |
|-----------------+--------------+---------------------|")

(defun h-2d-hmap (key-fun fie-fun val-fun table2d)
  "Return a 2D table from applying functions to each triad of TABLE2D.
KEY-FUN, FIE-FUN, and VAL-FUN are each called with three arguments:
key, field, and value.
- The result of KEY-FUN is the new key.
- The result of FIE-FUN is the new field.
- The result of VAL-FUN is the new value.

Its anaphoric counterpart is ‘h--2d-hmap’."
  (declare (pure t) (side-effect-free t))
  (let ((id     (h-2d-id table2d))
        (result (h-clr   table2d)))
    (maphash (lambda (key t2d-v)
               (maphash (lambda (field value)
                          (let ((newk (funcall key-fun key field value))
                                (newf (funcall fie-fun key field value)))
                            (and newk
                                 newf
                                 (h-put*!
                                  result newk newf
                                  (if (equal field id)
                                      newk
                                    (funcall val-fun key field value))))))
                        t2d-v))
             table2d)
    result))

(defmacro h--2d-hmap (key-form fie-form val-form table2d)
  "Anaphoric version of ‘h-2d-hmap’.
For every key–value pair in TABLE2D, evaluate KEY-FORM, FIE-FORM,
VAL-FORM with the variables key, field, and value bound."
  `(h-2d-hmap (lambda (key field value) (ignore key field value) ,key-form)
              (lambda (key field value) (ignore key field value) ,fie-form)
              (lambda (key field value) (ignore key field value) ,val-form)
              ,table2d))

(defun h-2d-hmap! (key-fun fie-fun val-fun table2d)
  "Update TABLE2D by applying functions to each of its triads.
Functions are KEY-FUN, FIE-FUN, VAL-FUN.

Just like ‘h-2d-hmap’ (which see), but modifies TABLE2D instead of
producing a fresh one.

Its anaphoric counterpart is ‘h--2d-hmap!’."
  (let ((id (h-2d-id table2d)))
    (maphash (lambda (key t2d-v)
               (remhash key table2d)
               (maphash (lambda (field value)
                          (let ((newk (funcall key-fun key field value))
                                (newf (funcall fie-fun key field value)))
                            (and newk
                                 newf
                                 (h-put*!
                                  table2d newk newf
                                  (if (equal field id)
                                      newk
                                    (funcall val-fun key field value))))))
                        t2d-v))
             table2d)))

(defmacro h--2d-hmap! (key-form fie-form val-form table2d)
  "Anaphoric version of ‘h-2d-hmap!’.
For every key–value pair in TABLE2D, evaluate KEY-FORM, FIE-FORM,
VAL-FORM with the variables key, field, and value bound."
  `(h-2d-hmap! (lambda (key field value) (ignore key field value) ,key-form)
               (lambda (key field value) (ignore key field value) ,fie-form)
               (lambda (key field value) (ignore key field value) ,val-form)
               ,table2d))

;;;;;;; Selection or rejection of columns

(xht--describe
 "Functions for selecting or rejecting columns from 2D hash tables.")


(defun h-2d-sel-cols (table2d cols)
  "Return a table with only selected named columns COLS of TABLE2D.
Any column of COLS that isn't in TABLE2D is ignored.

The first column (IDCOL), containing the keys, is always implicitly
selected: otherwise non-repeatability of keys wouldn't be
guaranteed.

This function doesn't modify TABLE2D. Its destructive counterpart
is ‘h-2d-sel-cols!’."
  (declare (pure t) (side-effect-free t))
  (when (xht--ht-and-empty-or-2d+-or-error table2d)
    (let ((htbl (h-clone* table2d)))
      (h-2d-sel-cols! htbl cols)
      htbl)))

(defun h-2d-sel-cols! (table2d cols)
  "Remove all but named columns COLS from 2D hash table TABLE2D.
The first column (IDCOL), containing the keys, is always implicitly
selected: otherwise non-repeatability of keys wouldn't be
guaranteed.

This function modifies TABLE2D. Its non-destructive counterpart
is ‘h-2d-sel-cols’."
  (when (xht--ht-and-empty-or-2d+-or-error table2d)
    (let ((cs (-uniq (cons (h-2d-id table2d) (-list cols)))))
      (h--hmap! key (h-sel (lambda (k _v)
                             (member k cs))
                           value)
                table2d))))

(defun h-2d-rej-cols (table2d cols)
  "Return a table without the named columns COLS of TABLE2D.
Any column of COLS that isn't in TABLE2D is ignored.

The first column (IDCOL), containing the keys, is never rejected:
otherwise non-repeatability of keys wouldn't be guaranteed.

This function doesn't modify TABLE2D. Its destructive counterpart
is ‘h-2d-rej-cols!’."
  (declare (pure t) (side-effect-free t))
  (when (xht--ht-and-empty-or-2d+-or-error table2d)
    (let ((htbl (h-clone* table2d))
          (cs   (-uniq (-remove-item (h-2d-id table2d) (-list cols)))))
      (when cs
        (h-each table2d
          (lambda (key value)
            (h-put! htbl key (dolist (col cs value)
                               (h-rem! value col))))))
      htbl)))

(defun h-2d-rej-cols! (table2d cols)
  "Remove named columns COLS from 2D hash table TABLE2D.
Any column of COLS that isn't in TABLE2D is ignored.

The first column (IDCOL), containing the keys, is never rejected:
otherwise non-repeatability of keys wouldn't be guaranteed.

This function modifies TABLE2D. Its non-destructive counterpart
is ‘h-2d-rej-cols’."
  (when (xht--ht-and-empty-or-2d+-or-error table2d)
    (let ((cs (-uniq (-remove-item (h-2d-id table2d) (-list cols)))))
      (when cs
        (h-each table2d
          (lambda (key value)
            (h-put! table2d key (dolist (col cs value)
                                  (h-rem! value col))))))
      table2d)))

;;;;;;; Transposition

(xht--describe "Functions for transposing 2D hash tables.")

(defun h-2d-transpose (table2d)
  "Return a hash table that is TABLE2D transposed."
  (declare (pure t) (side-effect-free t))
  (when (xht--ht-and-empty-or-2d+-or-error table2d)
    (let (header keys id res)
      (setq header (h-2d-header table2d)
            keys   (h-keys      table2d)
            res    (h-new (length header) (h-prop table2d 'test))
            id     (h-2d-id     table2d))
      (dolist (field (cdr header) res)
        (dolist (key keys)
          (h-put*! res field id field)
          (h-put*! res field key
                   (h-get* table2d key field)))))))

(defun h-2d-transpose! (table2d)
  "Transpose TABLE2D."
  (let ((htbl (h-2d-transpose table2d)))
    (h-clr! table2d)
    (h-mix! table2d htbl)))

;;;;;;; helper

(defun xht--ht-and-empty-or-2d+-or-error (obj)
  "Error if OBJ isn't hash table or of dimension 0 or 2+. Else t."
  (declare (pure t) (side-effect-free t))
  (let ((type (h-type obj))
        dim)
    (unless (equal type :ht)
      (user-error "Input is %s, not hash table" type))
    (setq dim (h-dim obj))
    (if (< 0 dim 2)
        (user-error "Input is %dD hash-table, can't do it" dim)
      t)))

;;;;;; Hash tables as sets

(xht--describe
 "Functions for creating and manipulating hash tables as sets.
Values are all set to t, and all that matters is the presence of keys.

Note: once created, hash-tables-as-sets can be manipulated with the
same functions for regular ones, including union (‘h-mix’),
difference (‘h-dif’), and intersection (‘h-cmn’) and their
destructive counterparts.")

(defun h-as-set<-vector (vector &optional size test)
  "Create a hash table as set from VECTOR.
The KEYS will be the elements of the vector, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-new (or size (length vector))
                       (or test 'equal)))
        (idx 0))
    (while (< idx (length vector))
      (h-put! result (aref vector idx) t)
      (setq idx (1+ idx)))
    result))

(defun h-as-set<-list (list &optional size test)
  "Create a hash table as set from LIST.
The KEYS will be the elements of the list, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-new (or size (length list))
                       (or test 'equal))))
    (dolist (key list result)
      (h-put! result key t))))

(defun h-as-set<-lines (str &optional size test)
  "Create a hash table as set from the lines of string STR.
The KEYS will be the lines, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (h-as-set<-list (s-lines str) size test))

(defun h-as-set<-words (str &optional size test)
  "Create a hash table as set from the words of string STR.
The KEYS will be the words, and all VALUES will be t.

Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (h-as-set<-list (s-split-words str) size test))

;;;;;; Hash tables for counting

(xht--describe
 "Functions for using hash tables for counting.")

(defun h-count<-vector (vector &optional size test)
  "Create a hash table with the elements of VECTOR and their count.
Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-new (or size (length vector))
                       (or test 'equal)))
        (idx 0)
        key)
    (while (< idx (length vector))
      (setq key (aref vector idx))
      (h-put! result key
              (1+ (or (h-get result key) 0)))
      (setq idx (1+ idx)))
    result))

(defun h-count<-list (list &optional size test)
  "Create a hash table with the elements of LIST and their count.
Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-new (or size (length list))
                       (or test 'equal))))
    (dolist (key list result)
      (h-put! result key
              (1+ (or (h-get result key) 0))))))

(defun h-count<-lines (str &optional size test)
  "Create a hash table with the lines of string STR and their count.
Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (h-count<-list (s-lines str) size test))

(defun h-count<-words (str &optional size test)
  "Create a hash table with the words of string STR and their count.
Optional arguments SIZE and TEST may be passed."
  (declare (pure t) (side-effect-free t))
  (h-count<-list (s-split-words str) size test))

(defun h-count-put (table &rest objs)
  "Create a hash table with objects OBJS with the counts as values.
Each OBJ might be:

- a hash table with counts for values: will be added to the count
  of TABLE.

- a vector: each element will add 1 to the count of TABLE.

- a list:   each element will add 1 to the count of TABLE.

- anything else: added as element: if new, with count 1, or
  otherwise added to the count of TABLE.

This function is free of side-effects. Its destructive counterpart
is ‘h-count-put!’."
  (declare (pure t) (side-effect-free t))
  (let ((result (h-clone* table)))
    (apply #'h-count-put! result objs)
    result))

(defun h-count-put! (table &rest objs)
  "Add counts of objects OBJS to the counts of hash table TABLE.
This function modifies TABLE. Its side-effects–free counterpart is
h-count-put’."
  (dolist (obj objs)
    (pcase (type-of obj)
      ('hash-table (xht--count-put-ht!     table obj))
      ('cons       (xht--count-put-list!   table obj))
      ('vector     (xht--count-put-vector! table obj))
      (_           (xht--count-put-elem!   table obj)))))

;;;;;;; helper

(defun xht--count-put-ht! (table table2)
  "Add counts of TABLE2 to TABLE."
  (h--each table2
    (h-put-num-with*! (-partial #'+ (h-as-number value))
                      table key)))
;; Above we had this:
;;   (h-put! table key
;;           (+ value (or (ht-get table key) 0)))))
;; Changed because  ‘h-put-num-with*!’ also changes strings etc.,
;; which table1 might have as value; more flexible.

(defun xht--count-put-vector! (table vector)
  "Add counts of VECTOR to TABLE."
  (let ((idx 0)
        key)
    (while (< idx (length vector))
      (setq key (aref vector idx))
      (h-put-inc*! table key)
      (setq idx (1+ idx)))))

(defun xht--count-put-list! (table list)
  "Add counts of LIST to TABLE."
  (dolist (key list)
    (h-put-inc*! table key)))

(defun xht--count-put-elem! (table elem)
  "Add 1 to count of ELEM in TABLE."
  (h-put-inc*! table elem))

;;;;; Helper
;;;;;; keyword <-> symbol <-> string <-> number

(defalias 'xht--string->symbol 'make-symbol
  "Convert string to symbol.")

(defun xht--keyword->symbol (keyword)
  "Convert KEYWORD to string."
  (->> keyword  xht--keyword->string  intern))

(defun xht--keyword->string (keyword)
  "Convert KEYWORD to string."
  (declare (pure t) (side-effect-free t))
  (->> keyword (format "%s") (s-chop-prefix ":")))

(defalias 'xht--symbol->string 'xht--keyword->string
  "Convert symbol to string.")

(defun xht--string->keyword (str)
  "Convert string STR to keyword."
  (->> str  (s-chop-prefix ":") (s-prepend ":")  intern))

(defun xht--symbol->keyword (symbol)
  "Convert SYMBOL to keyword."
  (->> symbol  (format "%s")  xht--string->keyword))

(defun xht--string->number (obj)
  "Convert string to number.
Unlike ‘string-to-number’, signal error if OBJ can't be converted."
  (declare (pure t) (side-effect-free t))
  (--> obj read
       (if (numberp it)
           it
         (error "‘xht--string->number’: Not a number: %s" it))))

(defalias 'xht--symbol->number 'xht--keyword->number
  "Convert symbol to number.")

(defun xht--keyword->number (obj)
  "Convert keyword to number.
Signal error if OBJ can't be converted."
  (declare (pure t) (side-effect-free t))
  (--> obj (format "%s" it) (s-chop-prefix ":" it) read
       (if (numberp it)
           it
         (error "‘xht--keyword->number’: Not a number: %s" it))))

(defalias 'xht--number->string 'number-to-string
  "Convert number to string.")

(defun xht--number->keyword (obj)
  "Convert number to keyword.
Signal error if OBJ can't be converted."
  (--> obj (format "%s" it) (s-prepend ":" it) intern))

(defun xht--number->symbol (obj)
  "Convert number to symbol.
Signal error if OBJ can't be converted."
  (--> obj (format "%s" it) intern))

;;;;;; Convert from string to types + types predicates
;;;;;;; string to type (selector)

(defun xht--to-type (str &optional type)
  "Convert string STR to TYPE.
If TYPE is 'htbl,    read STR as if it's a stringified #s(hash-table…).
If TYPE is 'ht-like, read STR as if it's an ht-like string, which is one
containing #s(hash-table…), (ht…), (h*…), (h-s*…), (h-t*…), or (h-st*…).

If TYPE is 'lol,       read STR as if it's a stringified ((list)(of)(lists)).
If TYPE is 'alist,     read STR as if it's a stringified ((a . list)).
If TYPE is 'plist,     read STR as if it's a stringified (p list).
If TYPE is 'list,      read STR as if it's a stringified (list).
If TYPE is 'cons-pair, read STR as if it's a stringified (cons . pair).
If TYPE is 'vector,    read STR as if it's a stringified [vector].
If TYPE is 'str or nil, return STR.
Otherwise signal error."
  (declare (pure t) (side-effect-free t))
  (cond ((memq type '(ht-like))       (xht--htstr-to-htbl      str))
        ((memq type '(htbl))          (xht--str-to-htbl        str))
        ((memq type '(lol))           (xht--str-to-lol         str))
        ((memq type '(alist))         (xht--str-to-alist       str))
        ((memq type '(plist))         (xht--str-to-plist       str))
        ((memq type '(list))          (xht--str-to-list        str))
        ((memq type '(cons-pair))     (xht--str-to-cons-pair   str))
        ((memq type '(vec vector))    (xht--str-to-vector      str))
        ((memq type '(s str string))  str)
        ((null type)                  str)
        (t  (error "‘xht--to-type’: Unknown type ‘%s’" type))))

;;;;;;; string to hash table

(defun xht--htstr-to-htbl (str)
  "Read STR as if it's an ht-like string.
An ht-like string is one whose contents look like one of these:
#s(hash-table…), (ht…), (h*…), (h-s*…), (h-t*…), or (h-st*…).

Look for objects that, when read, either already are a hash table
or will be evaluated to one through one of these canonical forms.
If one is found, format it as a hash table and return it. If not,
signal error.

See also: ‘xht--str-to-htbl’ looks strictly for
the #s(hash-table…) format."
  (declare (pure t) (side-effect-free t))
  (if (xht--str-nw? str)
      (h-format :htbl str)
    (h-new 1)))

(defun xht--str-to-htbl (str)
  "Read STR as if it's a stringified #s(hash-table…).
If STR is empty, return empty hash table.
If not empty but doesn't look like a hash table, signal error.

See also: ‘xht--htstr-to-htbl’ looks more broadly for ht-like formats."
  (declare (pure t) (side-effect-free t))
  (if (xht--str-nw? str)
      (->> str  read
           xht--htbl-or-error)
    (h-new)))

(defun xht--htbl-or-error (obj)
  "Test if OBJ is a hash table. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (ht? obj)
        obj
      (error "Not a hash table: %s" obj))))

;;;;;;; string to alist

(defun xht--str-to-alist (str)
  "Read STR as if it's a stringified ((a . list)).
If STR is empty, return '(()).
If not empty but doesn't look like an alist, signal error."
  (declare (pure t) (side-effect-free t))
  (when (xht--str-nw? str)
    (->> str  read
         xht--dequote-list
         xht--alist-or-error
         xht--canonicalize-alist)))

(defun xht--canonicalize-alist (alist)
  "Canonicalize ALIST."
  (declare (pure t) (side-effect-free t))
  (-non-nil alist))

(defun xht--alist-or-error (obj)
  "Test if OBJ is an alist. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (h-alist? obj)
        obj
      (error "Not an alist: %s" obj))))

;;;;;;; string to plist

(defun xht--str-to-plist (str)
  "Read STR as if it's a stringified '(p list).
If STR is empty, return '(()).
If not empty but doesn't look like an plist, signal error."
  (declare (pure t) (side-effect-free t))
  (when (xht--str-nw? str)
    (->> str  read
         xht--dequote-list
         xht--plist-or-error
         xht--canonicalize-plist)))

(defun xht--canonicalize-plist (plist)
  "Canonicalize PLIST."
  (declare (pure t) (side-effect-free t))
  plist)

(defun xht--plist-or-error (obj)
  "Test if OBJ is a plist. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (h-plist? obj)
        obj
      (error "Not a plist: %s" obj))))

;;;;;;; string to lol (list of lists)

(defun xht--str-to-lol (str)
  "Read STR as if it's a stringified ((list)(of)(lists)).
If STR is empty, return '(()).
If not empty but doesn't look like a lol, signal error."
  (declare (pure t) (side-effect-free t))
  (when (xht--str-nw? str)
    (->> str  read
         xht--dequote-list
         xht--lol-or-error
         xht--canonicalize-lol)))

(defun xht--canonicalize-lol (lol)
  "Canonicalize list of lists LOL.
Remove any 'hlines, make one-item lists of any atomic elements."
  (declare (pure t) (side-effect-free t))
  (->> lol
       (-remove-item 'hline)
       ;; ^ if it came from an org-table, it might have
       ;;   'hlines, which we remove.
       (--map (if (atom it)  ; this would probably work, too: (-map #'-list)
                  (list it)
                it))))

(defun xht--lol-or-error (obj)
  "Test if OBJ is a list of list. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (h-lol? obj)
        obj
      (error "Not a list of lists: %s" obj))))

(defun xht--dequote-list (list)
  "Dequote LIST recursively."
  ;; If in a file, for example, it is quoted, we remove that,
  ;; for it'll be quoted again when read from string.
  (declare (pure t) (side-effect-free t))
  (let ((l list))
    (while (equal 'quote (car l))
      (setq l (cadr l)))
    l))

;;;;;;; string to list

(defun xht--str-to-list (str)
  "Read STR as if it's a stringified (list).
If STR is empty, return '().
If not empty but doesn't look like a list, signal error."
  (declare (pure t) (side-effect-free t))
  (if (xht--str-nw? str)
      (->> str  read
           xht--dequote-list
           xht--list-or-error)
    '()))

(defun xht--list-or-error (obj)
  "Test if OBJ is a list. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (listp obj)
        obj
      (error "Not a list: %s" obj))))

;;;;;;; string to cons pair

(defun xht--str-to-cons-pair (str)
  "Read STR as if it's a stringified (cons . pair).
If STR is empty, return '().
If not empty but doesn't look like a cons pair, signal error."
  (declare (pure t) (side-effect-free t))
  (if (xht--str-nw? str)
      (->> str  read
           xht--dequote-list
           xht--cons-pair-or-error)
    '()))

(defun xht--cons-pair-or-error (obj)
  "Test if OBJ is a cons cell. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (-cons-pair? obj)
        obj
      (error "Not a cons pair: %s" obj))))

;;;;;;; string to vector

(defun xht--str-to-vector (str)
  "Read STR as if it's a stringified [vector].
If STR is empty, return [].
If not empty but doesn't look like a vector, signal error."
  (declare (pure t) (side-effect-free t))
  (if (xht--str-nw? str)
      (->> str  read
           xht--vector-or-error)
    []))

(defun xht--vector-or-error (obj)
  "Test if OBJ is a list. Return OBJ or signal error."
  (declare (pure t) (side-effect-free t))
  (when obj
    (if (vectorp obj)
        obj
      (error "Not a vector: %s" obj))))

;;;;;; Miscellaneous

(defun xht--str-nw? (str)
  "Test if string STR contains a non-whitespace character.
If so, return STR. Otherwise return nil."
  ;; Adapted from org-macs.el's ‘org-string-nw-p’.
  (declare (pure t) (side-effect-free t))
  (and (stringp str)
       (string-match-p "[^ \r\t\n]" str)
       str))

(defun xht--quote (obj)
  "Quote OBJ for printed representation when OBJ is symbol or list.
Strings, numbers, keywords, and vectors need no treatment."
  (declare (pure t) (side-effect-free t))
  (let ((print-quoted t))
    (pcase obj
      ((pred null)       nil)
      ((pred keywordp)   obj)
      ((pred symbolp) `',obj)
      ((pred listp)   `',obj)
      (_                 obj))))
;; Is this ^ robust enough? Any other more straightforward way?

(defun xht--non-aggressively-insert (&rest strings)
  "Insert STRINGS with ‘aggressive-indent-mode’ temporarily off."
  (if (bound-and-true-p aggressive-indent-mode)
      (prog2
          (aggressive-indent-mode -1)
          (apply #'insert strings)
        (aggressive-indent-mode 1))
    (apply #'insert strings)))

(defun xht--file-str-no-prop (file)
  "FILE contents as string, no properties."
  (with-temp-buffer
    ;; Better to make it ‘-literally’? Why?
    (insert-file-contents file)
    (xht--buff-str-no-prop)))

(defun xht--buff-str-no-prop ()
  "Buffer string, no properties.
\(Why doesn't Emacs have this handy function natively?)"
  (buffer-substring-no-properties (point-min) (point-max)))

;; Unused: decided to keep the behavior of ht<-alist, which returns
;; (ht (nil nil)) for alist '(()) instead of an empty hash list.
(defun xht--remove-nested-nils (obj)
  "Reduce to nil '(((...))) in OBJ."
  (declare (pure t) (side-effect-free t))
  (let ((x obj))
    (while (and x
                (listp x)
                (listp (car x))
                (null  (cdr x)))
      (setq x (car x)))
    (if x obj nil)))

;;;;; Miscellaneous
;;;;;; Additional examples

(xht--describe
 "Some additional examples that may be useful and don't fit any
particular function.")

(defun xht--misc-examples ()
  "This function does nothing.
It's just a hack to add to examples.el (and also to the README) a
few miscellaneous tests and examples that aren't attached to any
particular function."
  (ignore))

;;;; Commands
;;;;; XHT dispatcher

(xht--describe
  "With the dispatcher you can interactively convert, display, insert, or
write to disk whatever looks like a hash table or could be converted
from/into one. There're also some specialized functions that offer you
\"speed dial\" access to the dispatcher options.")

;;;;;; Main dispatcher

;;;###autoload
(defun xht-do (&optional ht-opts)
  "The main command for you to do all sorts of hash table things.

This is XHT's main dispatcher.
- Launch it to convert, display, insert, write to disk.

- It reads and write from (almost) everything that can be
  transformed from or into a hash table.

- Input can be the last sexp, some thing at point, region
  selection, a file, your clipboard, or the whole buffer you're in.

Try it.

You can also make your own function to call it non-interactively.
You must then provide HT-OPTS, a tiny hash table with options to
bypass the input of any specific questions. This hash table may
have any number of the up to six variables holding answers to the
questions asked, namely:

#+begin_src emacs-lisp
  '(do-what  where-from  where-to  what-to  format-to   file-to)
#+end_src

For example, if you frequently convert the last sexp to hash table,
or want to visualize the hash table before point as (h*…), and then
have this inserted at point. In fact, this one should be
particularly frequent, so we already have a function for it:
xht-do-h*-ify-last-sexp-to-point’, which is essentially this:

#+begin_src emacs-lisp
  (xht-do (h* :do-what    ?c
              :where-from ?x
              :where-to   ?i
              :what-to    ?h
              :format-to  ?1
              :file-to    nil))
#+end_src

That is: [c]onvert, from last se[x]p, [i]nsert at point, to [h]ash
table, format #[1] (which is pretty-printed h* form).

Try that command when point is after a #s(hash-table…) or alist.

With that in mind you can create a similar one adapted to your
needs. You can then key-bind it for quick access.

See also these related specialized commands:
- ‘xht-do-convert-dwim
- ‘xht-do-h*-ify-dwim-to-new-buffer
- ‘xht-do-h*-ify-dwim-to-point
  - ‘xht-do-h*-ify-selected-region-to-point
xht-do-h*-ify-thing-at-point-to-point
xht-do-h*-ify-last-sexp-to-point’"
  (interactive)
  (h-let (xht--do-ht-opts-cleanup ht-opts)
    (let* ((do-what    (or .do-what  ;;USER-INPUT
                           (xht--do-convert-or-as-is)))
           ;; from ------->
           (tap-type   (xht--do-type-of-thing-at-point))
           (where-from (or .where-from  ;;USER-INPUT
                           (xht--do-where-from tap-type)))
           (thing-from (pcase where-from
                         (?f (xht--do-read-file))
                         (?c (xht--do-read-clipboard))
                         (?x (xht--do-read-last-sexp))
                         (?b (xht--do-read-whole-buffer))
                         (?t (xht--do-read-thing-at-point))
                         (?r (xht--do-read-selected-region))))
           (boundaries (pcase where-from
                         (?x (xht--do-boundaries-of-last-sexp))
                         (?t (xht--do-boundaries-of-thing-at-point))
                         (?r (xht--do-boundaries-of-selected-region))))
           (read-from  (xht--do-read-from thing-from))
           (type-from  (h-type read-from))
           (as-ht-from (if (eq type-from :ht)
                           read-from
                         ;; ^ h<-it would take care of it all the same,
                         ;;   but this is more direct and explicit.
                         (h<-it read-from)))
           (dim-from   (h-dim  as-ht-from))
           ;; <------- from
           (where-to   (or .where-to  ;;USER-INPUT
                           (xht--do-where-to where-from tap-type)))
           (what-to-u  (and .what-to
                            ;; check whether non-interactively–entered
                            ;; user-input is possible for the dimension; if so
                            ;; return it, otherwise nil.
                            (memq .what-to
                                  (pcase dim-from
                                    (0 '(?h ?v ?l ?#))
                                    (1 (if (h--first (not (natnump key))
                                                     as-ht-from)
                                           '(?h ?k ?j ?a ?p)
                                         '(?h ?v ?l ?n ?k ?a ?p ?j)))
                                    (2 '(?h ?o ?l ?j ?a ?p ?t ?s))
                                    (_ '(?h ?a ?p ?j))))
                            .what-to))
           (what-to    (pcase do-what
                         (?a nil)
                         (?c (or what-to-u
                                 (pcase dim-from
                                   (0 (xht--do-what-to-0D))
                                   (1 (if (h--first (not (natnump key))
                                                    as-ht-from)
                                          (xht--do-what-to-1D-e)
                                        (xht--do-what-to-1D-i)))
                                   (2 (xht--do-what-to-2D))
                                   (_ (xht--do-what-to-nested)))))))
           (type-to    (pcase what-to
                         (?h :ht)         (?s :ssv)     (?j :json)
                         (?o :orgtbl)     (?c :csv)     (?a :alist)
                         (?v :vector)     (?t :tsv)     (?p :plist)
                         (?# 'empty)      (?k 'kvl)     (?n 'lines)
                         (?l (if (= 2 dim-from) :lol :list))))
           (conv-fun   (pcase type-to
                         (:ht     #'identity)  (:lol     #'h->lol)
                         ('lines  #'h->lines)  (:list    #'h->list)
                         ('empty  #'h->lines)  (:json    #'h->json*)
                         ('kvl    #'h->kvl)    (:alist   #'h->alist*)
                         (:csv    #'h->csv)    (:plist   #'h->plist*)
                         (:tsv    #'h->tsv)    (:orgtbl  #'h->orgtbl)
                         (:ssv    #'h->ssv)    (:vector  #'h->vector)))
           (converted  (if type-to
                           (funcall conv-fun as-ht-from)
                         (setq type-to type-from)
                         thing-from))
           (format-to  (or .format-to  ;; USER-INPUT:
                           (pcase where-to
                             (?f (pcase type-to
                                   (:ht (xht--do-format-to-ht-write))
                                   (_   (xht--do-format-to-non-ht-write))))
                             (_  (pcase type-to
                                   (:ht (xht--do-format-to-ht-all))
                                   (_   (xht--do-format-to-non-ht-all)))))))
           (file-to    (or .file-to   ;; USER-INPUT:
                           (pcase where-to
                             (?f (read-file-name "Save to: ")))) )
           (insert-fun #'xht--non-aggressively-insert-str-from-here)
           (final-str  (xht--do-make-final-string
                        converted type-to format-to where-to)))
      (pcase where-to
        (?c  (kill-new final-str))
        (?f  (h-write! file-to final-str 'overwrite?))
        (?b  (xht--to-display-buffer "*xht display*")
             (when (> (point-max) 3)
               (save-excursion (insert "\n\n\f\n\n")))
             (pcase type-to
               (:json   (let ((pr (make-progress-reporter
                                   "Turning on js-mode...")))
                          (ignore-errors (js-mode))
                          (progress-reporter-done pr)))
               (:orgtbl (let ((pr (make-progress-reporter
                                   "Turning on orgtbl-mode...")))
                          (ignore-errors (orgtbl-mode))
                          (progress-reporter-done pr))))
             (funcall insert-fun final-str))
        (?i  (barf-if-buffer-read-only)
             (pcase where-from
               ((or ?t ?r) (goto-char (cdr boundaries))))
             (funcall insert-fun final-str 'with-spacing))
        (?r  (barf-if-buffer-read-only)
             (save-excursion
               (atomic-change-group
                 (-let [(b . e) boundaries]
                   (goto-char b)
                   (delete-region b e)
                   (while (looking-back "'" (- (point) 2))
                     (delete-char -1)) ;get rid of quotes
                   (funcall insert-fun final-str)))))))))

;;;;;;; Helper functions
;;;;;;;; Non-interactive user input: cleanup

(defun xht--do-ht-opts-cleanup (&optional ht-opts)
  "Clean up some possibly inconsistent user-input options.
Optional HT-OPTS is a hash table with options.

Silently revert to nil any option that is unavailable. Not
exhaustive, since :where-to depends on dimension, which shows up
only later (but cleaned up in ‘xht-do’ itself)."
  (if ht-opts
      (h-let ht-opts
        (h--hmap! (h-as-keyword key) value ht-opts)
        (or (memq .do-what    '(nil ?c ?a))
            (h-rem! ht-opts :do-what))
        (or (memq .where-from '(nil ?f ?c ?b ?r ?t ?x))
            (h-rem! ht-opts :where-from))
        (or (memq .where-to   '(nil ?f ?c ?b ?r ?i))
            (h-rem! ht-opts :where-to))
        (or (memq .what-to    '(nil ?h ?l ?s ?j ?o ?c ?a
                                    ?v ?t ?p ?k ?n ?#))
            (h-rem! ht-opts :what-to))
        (pcase .where-to
          (?f (or (file-writable-p .file-to)
                  (h-rem! ht-opts :file-to)))
          (_  (h-rem! ht-opts :file-to)))
        (or (memq .format-to (pcase .what-to
                               (?h (-concat '(nil)
                                            (number-sequence ?1 ?9)
                                            (number-sequence ?a ?c)))
                               (_  '(nil ?c ?C ?p ?P))))
            (h-rem! ht-opts :format-to))
        ht-opts)
    (h-new 1)))

;;;;;;;; Interactive user input

(defun xht--do-input (strings chars)
  "Read characters given lists of STRINGS and CHARS."
  (read-char-choice
   (->> strings -non-nil (-remove-item "") (s-join "\n"))
   (->> chars   -non-nil)))

(defun xht--do-convert-or-as-is ()
  "Read characters to choose whether to convert."
  (xht--do-input
   '("Convert it, or use thing as it is? > "
     "[c] Convert it"
     "[a] Use it as it is  ")
   '(?c ?a)))

(defun xht--do-where-from (&optional tap-type)
  "Read characters to choose where input comes from.
TAP-TYPE is the type of the thing ate point, evaluated by
xht--do-type-of-thing-at-point’."
  (xht--do-input
   `("Input from where? > "
     ,(when tap-type
        (format "[t] Thing-at-point (%s)"
                (h-as-string tap-type)))
     ,(when (use-region-p) "[r] Region (selected)")
     "[x] Last sexp   [c] Clipboard"
     "[f] File        [b] Buffer (this)  ")
   `(,(when tap-type       ?t)
     ,(when (use-region-p) ?r)
     ?x ?c ?f ?b)))

(defun xht--do-where-to (where-from &optional tap-type)
  "Read characters to choose where to output to.
WHERE-FROM can be ?t, ?x, or ?r.
Optional TAP-TYPE is the type of thing at point."
  (let* ((type-s (if tap-type
                     (h-as-string tap-type)
                   ""))
         (repl   (pcase where-from
                   (?t (format "thing-at-point (%s)" type-s))
                   (?x "last-sexp")
                   (?r "selected region")))
         (insr   (pcase where-from
                   (?t (format "after thing-at-point (%s)" type-s))
                   (?x "after last sexp")
                   (?r "after selected region")
                   (_  "at point")))
         (repl-s (when repl
                   (format "[r] Replace %s" repl)))
         (insr-s (format "[i] Insert %s" insr)))
    (xht--do-input
     `("Output to where? > "
       ,repl-s
       ,insr-s
       "[c] Clipboard  [f] File  [b] Buffer (new)  ")
     `(,(when repl ?r)
       ?i ?c ?f ?b))))

(defun xht--do-what-to-0D ()
  "Read characters to choose which 0D output."
  (xht--do-input
   '("Input is 0D. Convert to what? > "
     "[h] Empty hash table"
     "[l] Empty list   [#] Empty string"
     "[v] Empty vector  ")
   '(?h ?# ?v ?l)))

(defun xht--do-what-to-1D-e ()
  "Read characters to choose which 1D explicit output."
  (xht--do-input
   '("Input is 1D. Convert to what? > "
     "[h] Hash table       [a] Association list"
     "[k] Key–value pairs  [p] Property list"
     "[j] JSON  ")
   '(?h ?k ?j ?a ?p)))

(defun xht--do-what-to-1D-i ()
  "Read characters to choose which 1D implicit output."
  (xht--do-input
   '("Input is (implicitly) 1D. Convert to what? > "
     "[h] Hash table     [v] Vector     [l] List"
     "[n] Lines of string    [k] Key–value pairs"
     "[a] Alist   [p] Plist  [j] JSON  ")
   '(?h ?v ?l ?n ?k ?a ?p ?j)))

(defun xht--do-what-to-2D ()
  "Read characters to choose which 2D output."
  (xht--do-input
   '("Input is 2D. Convert to what? > "
     "[h] Hash table     [j] JSON"
     "[o] Org table      [a] Alist"
     "[l] List of lists  [p] Plist"
     "[t] TSV            [s] SSV") ;no CSV yet
   '(?h ?o ?l ?j ?a ?p ?t ?s)))

(defun xht--do-what-to-nested ()
  "Read characters to choose which nested output."
  (xht--do-input
   '("Input is nested. Convert to what? > "
     "[h] Hash table"
     "[a] Association list"
     "[p] Property list"
     "[j] JSON  ")
   '(?h ?a ?p ?j)))

(defun xht--do-format-to-ht-all ()
  "Read characters to choose hash table options."
  (xht--do-input
   '("                    Lisp obj     String     "
     "| Which format?   | | pp | cm |  | pp | cm |"
     "|-----------------| |----+----|  |----+----|"
     "| (h*…)           | |  1 |  4 |  |  7 |  a |"
     "| (ht…)           | |  2 |  5 |  |  8 |  b |"
     "| #s(hash-table…) | |  3 |  6 |  |  9 |  c |"
     "pp = pretty-printed, cm = compact")
   '(?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9 ?a ?b ?c)))

(defun xht--do-format-to-ht-write ()
  "Read characters to choose hash table options."
  (xht--do-input
   '("| Which format?   | | pp | cm |"
     "|-----------------| |----+----|"
     "| (h*…)           | |  1 |  4 |"
     "| (ht…)           | |  2 |  5 |"
     "| #s(hash-table…) | |  3 |  6 |"
     "pp = pretty-printed, cm = compact")
   '(?1 ?2 ?3 ?4 ?5 ?6)))

(defun xht--do-format-to-non-ht-all ()
  "Read characters to choose: read string? pretty-print?"
  (xht--do-input
   '("| Which format?   | | pp | cm |"
     "|-----------------| |----+----|"
     "| Read it   (%s)  | |  p |  c |"
     "| As string (%S)  | |  P |  C |"
     "pp = pretty-printed, cm = compact")
   '(?p ?c ?P ?C)))

(defun xht--do-format-to-non-ht-write ()
  "Read characters to choose: read string? pretty-print?"
  (xht--do-input
   '("| Which format?   | | pp | cm |"
     "|-----------------| |----+----|"
     "| Read it   (%s)  | |  p |  c |"
     "pp = pretty-printed, cm = compact")
   '(?p ?c)))

;;;;;;;; String-maker

(defun xht--do-make-final-string (converted type-to format-to where-to)
  "Make final string for insertion.
Helper for ‘xht-do’. Needs CONVERTED, TYPE-TO, FORMAT-TO, WHERE-TO."
  (pcase type-to
    (:ht (--> converted
              (pcase format-to
                ((or ?1 ?7) (h-format :h*-pp-str   it))
                ((or ?2 ?8) (h-format :ht-pp-str   it))
                ((or ?3 ?9) (h-format :htbl-pp-str it))
                ((or ?4 ?a) (h-format :h*-cm-str   it))
                ((or ?5 ?b) (h-format :ht-cm-str   it))
                ((or ?6 ?c) (h-format :htbl-cm-str it))
                (_ (error "‘xht--do-make-final-string’: invalid format-to")))
              (pcase format-to
                ((or ?1 ?2 ?3 ?4 ?5 ?6)              it)
                ((or ?7 ?8 ?9 ?a ?b ?c) (format "%S" it)))))

    (_   (--> converted
              (if (and (/= ?f where-to)
                       (memq type-to
                             '(:alist :plist :list :lol)))
                  `',it  ;;<-- restore quote of lists
                it)
              (pcase format-to
                ;; compact
                ((or ?c ?C)
                 (pcase type-to
                   ((or :tsv :ssv :csv :orgtbl 'lines 'kvl)
                    (xht--do-string-cm-ify it))
                   ((or :alist :plist :list :lol :vector)
                    (let ((print-quoted t))
                      (format "%S" it)))
                   (:json (xht--do-json-cm-ify it))
                   (_ it)))

                ;; pretty-printed
                ((or ?p ?P)
                 (pcase type-to
                   ((or :alist :plist :list :lol :vector)
                    (--> it  pp-to-string  s-chomp))
                   (:json (xht--do-json-pp-ify it))
                   (_ it)))
                (_ (error "‘xht--do-make-final-string’: invalid format-to")))

              ;; insert string or insert *as* string?
              (pcase format-to
                ((or ?c ?p) (format "%s" it))
                ((or ?C ?P) (format "%S" it)))))))

;;;;;;;; Reading

(defun xht--do-read-from (thing-from)
  "Try to read string THING-FROM as hash table, list/vector, or string."
  (if-let ((hreadh (ignore-errors (h-read-h thing-from))))
      hreadh
    (let ((reading (--> (read thing-from)
                        (if (listp it)
                            (xht--dequote-list it)
                          it))))
      ;; (if (equal (h-type reading) 'symbol)
      ;;     thing-from
      ;;   reading)
      (pcase (type-of reading)
        ((or 'vector 'cons) reading)
        ('hash-table
         (error "‘xht--do-read-from’: not possible, tested before"))
        (_  thing-from)))))

(defun xht--do-read-file ()
  "Read file."
  (with-temp-buffer
    (insert-file-contents (read-file-name "From which file? "))
    (buffer-string)))

(defun xht--do-read-clipboard ()
  "Read clipboard."
  (if kill-ring
      (substring-no-properties (current-kill 0 'do-not-move))
    ""))

(defun xht--do-read-last-sexp ()
  "We evaluate it and then make it readable again with %S."
  (let ((evald (xht--form-last-sexp-eval)))
    (if (stringp evald)
        evald
      (format "%S" evald))))

(defun xht--do-read-whole-buffer ()
  "Read whole buffer."
  (xht--buff-str-no-prop))

(defun xht--do-read-selected-region ()
  "Read selected region."
  (buffer-substring-no-properties (region-beginning) (region-end)))

(defun xht--do-read-thing-at-point ()
  "Read thing at point."
  (autoload #'org-at-table-p  "org")
  (cond
   ((org-at-table-p)         (xht--do-as-string-orgtbl-at-point))
   ((xht--do-at-hash-table?) (xht--do-as-string-hshtbl-at-point))
   ((nth 3 (syntax-ppss))    (xht--do-as-string-string-at-point))
   ((nth 1 (syntax-ppss))    (xht--do-as-string-lstvec-at-point))
   (t (error "Can't read it"))))

;;;;;;;;; Thing at point

(defun xht--do-type-of-thing-at-point (&optional try-seq)
  "Type of thing at point: org table, hash table, or nil.
If TRY-SEQ is non-nil, also check if I'm inside a list or vector
and return its ‘h-type’ accordingly."
  (autoload #'org-at-table-p  "org")
  (cond
   ((org-at-table-p)     :orgtbl)
   ((xht--do-at-hash-table?) :ht)
   ((nth 3 (syntax-ppss))
    (h-type (xht--do-as-string-string-at-point)))
   (t (when try-seq
        (xht--do-type-of-lstvec-at-point-maybe)))))

;; This one is unused above because of ambiguities: if I run DWIM with type
;; and it says I'm inside a list, do I mean to act on that list or on the
;; last sexp? So the detecting function is below but unused for DWIM.
(defun xht--do-type-of-lstvec-at-point-maybe ()
  "Detect if thing at point is list or vector."
  (-if-let* ((pos   (nth 1 (syntax-ppss)))
             (delim (char-after pos)))
      (pcase delim
        (?\( (h-type (read (xht--do-as-string-lstvec-at-point))))
        (?\[ :vector)
        (_   nil))))

(defun xht--do-as-string-orgtbl-at-point ()
  "String of org table at point."
  (autoload #'org-table-begin "org-table")
  (autoload #'org-table-end   "org-table")
  (buffer-substring-no-properties (org-table-begin) (org-table-end)))

(defun xht--do-as-string-string-at-point ()
  "Outside-quotations string at point."
  (save-excursion
    (goto-char (nth 8 (syntax-ppss))) ;<-- Outside the opening "
    (let ((beg (point)))
      (forward-sexp)
      (read
       (buffer-substring-no-properties beg (point))))))

(defun xht--do-as-string-lstvec-at-point ()
  "String of list or vector at point.
Must be run from the first level of the list or vector."
  (save-excursion
    (goto-char (nth 1 (syntax-ppss))) ;<-- Outside parent ( or [
    (let ((beg (point)))
      (forward-sexp)
      (buffer-substring-no-properties beg (point)))))

;; see also: ‘xht--do-as-string-hshtbl-at-point
;;           xht--do-boundaries-of-thing-at-point
;;           xht--do-boundaries-of-hash-table-at-point

;;;;;;;;;; Hash table

(defun xht--do-as-string-hshtbl-at-point ()
  "String of hash table at point."
  (-let [(beg . end) (xht--do-boundaries-of-hash-table-at-point)]
    (when beg
      (buffer-substring-no-properties beg end))))

(defun xht--do-at-hash-table? ()
  "Is thing at point a hash table?
Return t if in any of the hash table forms:
- #s(hash-table…)
- (ht…)
- (h*…), (h-s*…), (h-t*…), or (h-st*…)

unless the h forms are quoted, as in '(ht…), in which case return nil."
  (save-excursion
    (when (xht--do-goto-hash-table-beginning-maybe)
      t)))

(defun xht--do-goto-hash-table-beginning-maybe ()
  "Try to go to beginning of hash table.
Return point before its starting '(', or nil if not found.

Works in any of the hash table forms:
- #s(hash-table…)
- (ht…)
- (h*…), (h-s*…), (h-t*…), or (h-st*…)

unless the h forms are quoted, as in '(ht…), in which case also
return nil."
  (let (beg)
    (save-excursion
      (when (looking-at "#") (forward-char 1))
      (when (looking-at "s") (forward-char 1))
      (when (looking-at "(") (forward-char 1))
      (catch 'break
        (while (nth 1 (syntax-ppss))
          (goto-char (nth 1 (syntax-ppss)))
          (when (xht--do-at-beginning-of-hash-table?)
            (setq beg (point))
            (throw 'break (point))))))
    (when beg
      (goto-char beg))))

(defun xht--do-at-beginning-of-hash-table? ()
  "Am I just before the first ?( of some hash table representation?"
  (or (and (looking-back "#s" (- (point) 3))
           (looking-at (rx (: "(hash-table"
                              (+ (in " \t\n"))))))
      (and (not (looking-back "'" (- (point) 2)))
           (looking-at (rx (: "("
                              (| "h-t*" "h*" "ht"
                                 "h-s*" "h-st*")
                              (+ (in " \t\n"))))))))

(defun xht--do-boundaries-of-hash-table-at-point ()
  "Cons cell with boundaries of hash table at point."
  (save-excursion
    (when (xht--do-goto-hash-table-beginning-maybe)
      (let ((delta (if (looking-back "#s" -3) -2 0))
            (beg   (point)))
        (forward-sexp)
        (cons (+ beg delta) (point))))))

;;;;;;;;; Boundaries

(defun xht--do-boundaries-of-last-sexp ()
  "Cons cell with boundaries of last sexp."
  (save-excursion
    (backward-sexp)
    (let ((lsexp-beg (point)))
      (forward-sexp)
      ;; boundaries of last sexp:
      (cons lsexp-beg (point)))))

(defun xht--do-boundaries-of-thing-at-point ()
  "Cons cell with boundaries of thing at point."
  (autoload #'org-at-table-p  "org")
  (autoload #'org-table-begin "org-table")
  (autoload #'org-table-end   "org-table")
  (let* (b e)
    (cond
     ((org-at-table-p)
      (setq b (org-table-begin))
      (setq e (org-table-end)))
     ((xht--do-at-hash-table?)
      (-setq (b . e) (xht--do-boundaries-of-hash-table-at-point)))
     ((nth 3 (syntax-ppss)) ;string
      (save-excursion
        (goto-char (nth 8 (syntax-ppss))) ;<-- Outside the opening "
        (setq b (point))
        (forward-sexp)
        (setq e (point))))
     ((nth 1 (syntax-ppss)) ;list or vector
      (save-excursion
        (goto-char (nth 1 (syntax-ppss))) ;<-- Outside parent ( or [
        (forward-sexp)
        (setq e (point))
        (backward-sexp)
        (while (looking-back "'" (- (point) 2))
          (forward-char -1))
        (setq b (point))))
     (t (error "Can't set boundaries")))
    (cons b e)))

(defun xht--do-boundaries-of-selected-region ()
  "Cons cell with boundaries of selected region."
  (cons (region-beginning) (region-end)))

;; see also: ‘xht--do-boundaries-of-hash-table-at-point

;;;;;;;; String formatting (cm or pp)
;;;;;;;;; JSON

(defun xht--do-json-format (pp str)
  "Compactify or pretty-print-ify a JSON string.
If PP is non-nil, pretty-print-ify STR. Otherwise compactify it."
  (let ((json-encoding-pretty-print pp)
        (json-object-type 'alist))
    (json-encode (json-read-from-string str))))

(defun xht--do-json-cm-ify (str)
  "Compactify JSON string STR."
  (xht--do-json-format nil str))

(defun xht--do-json-pp-ify (str)
  "Pretty-print-ify JSON string STR."
  (xht--do-json-format t str))

;;;;;;;;; Other strings

(defun xht--do-string-cm-ify (str)
  "Escape tabs and newlines in string STR."
  ;; Adapted from Magnar Sveen's string-edit.el
  ;; was: se/string-at-point/escape
  (with-temp-buffer
    (insert str)
    (xht--do-string-escape "\\")
    (xht--do-string-escape-ws "n" "\n")
    (xht--do-string-escape-ws "r" "\r")
    (xht--do-string-escape-ws "t" "\t")
    (xht--do-string-escape "\"")
    (goto-char (point-min))    (insert "\"")
    (goto-char (point-max))    (insert "\"")
    (xht--buff-str-no-prop)))

(defun xht--do-string-escape (quote)
  ;; Adapted from Magnar Sveen's string-edit.el
  ;; was: se/escape
  "Escape quotes of type QUOTE in buffer."
  (goto-char (point-min))
  (while (search-forward quote nil t)
    (replace-match "")
    (insert "\\" quote)))

(defun xht--do-string-escape-ws (signifier char)
  ;; Adapted from Magnar Sveen's string-edit.el
  ;; was: se/escape-ws
  "Escape whitespace in buffer given SIGNIFIER and CHAR."
  (goto-char (point-min))
  (while (search-forward char nil t)
    (replace-match "")
    (insert "\\" signifier)))

;;;;;;;; Insertion

(defun xht--insert-str-from-here (str &optional with-spacing)
  "Save excursion, insert STR on next line (or this line if empty).
If WITH-SPACING is non-nil, add one empty line before and after."
  (save-excursion
    (let ((beg (point)))
      (unless (bolp) (forward-line))
      (unless (eolp) (open-line 1))
      (xht--if-with-spacing str with-spacing)
      (indent-region beg (point)))))

(defun xht--non-aggressively-insert-str-from-here (str &optional with-spacing)
  "Insert STR from here with ‘aggressive-indent-mode’ temporarily off.
If WITH-SPACING is non-nil, add newlines before and after."
  (save-excursion
    (unless (bolp) (forward-line))
    (unless (eolp) (open-line 1))
    (if (bound-and-true-p aggressive-indent-mode)
        (prog2
            (aggressive-indent-mode -1)
            (xht--if-with-spacing str with-spacing)
          (aggressive-indent-mode))
      (xht--if-with-spacing str with-spacing))))

(defun xht--if-with-spacing (str &optional with-spacing)
  "Insert string STR, maybe with spacing.
If WITH-SPACING is non-nil, add newlines before and after."
  (if with-spacing
      (insert "\n" str "\n")
    (insert str)))

;;;;;;;; Display

(defun xht--to-display-buffer (name)
  "Go to beginning of elisp buffer named NAME (create as needed)."
  (let ((buffer (get-buffer-create name)))
    (pop-to-buffer buffer)
    (emacs-lisp-mode)
    (goto-char (point-min))))

;;;;;;;; Last sexp

(defun xht--form-last-sexp-eval ()
  "Eval last sexp (the one immediately before point)."
  (let* ((lsf  (xht--form-last-sexp))
         (type (type-of lsf)))
    (pcase type
      ;; only symbols and unquoted lists will be eval'd; not ht or str.
      ((or 'symbol 'cons) (eval lsf))
      (_                  lsf))))

(defun xht--form-last-sexp ()
  "Return last sexp (the one immediately before point)."
  (read (xht--form-last-sexp-str)))

(defun xht--form-last-sexp-str ()
  "Return last sexp (the one immediately before point) as string.
Note that ‘backward-sexp’ includes quotes when the sexp was a list —
but not the #s of hash tables, which we do recover here."
  (save-excursion
    (let ((end (point)))
      (backward-sexp)
      (when (looking-back "#s" (- (point) 4))
        (forward-char -2))
      (s-trim (buffer-substring-no-properties (point) end)))))

;;;;;; Specialized quicker access

(xht--describe
 "These commands pass pre-defined choices to the main dispatcher so you can
more quickly do the things you do more often without having to answer all of
the main dispatcher's questions.")

;;;###autoload
(defun xht-do-convert-dwim ()
  "Convert region, thing at point or last-sexp — try to guess it.
- If you have a region selected, use that.
- Otherwise, if you're inside a string, a vector, some sort of
  list, or #s(hash-table…), use that.
- Otherwise, use last sexp

Then interactively select where to, what to, and format.

See also ‘xht-do’ for other specialized commands listed there."
  (interactive)
  (xht-do (h* :do-what    ?c
              :where-from (xht--do-guess-where-from-option))))

;;;###autoload
(defun xht-do-h*-ify-dwim-to-clipboard ()
  "First h*-ify region, thing at point or last-sexp; send it to clipboard.
Like ‘xht-do-h*-ify-dwim-to-point’, but to clipboard instead of at point."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere
   (xht--do-guess-where-from-option)
   ?c))

;;;###autoload
(defun xht-do-h*-ify-dwim-to-file ()
  "First h*-ify region, thing at point or last-sexp; write to file.
Like ‘xht-do-h*-ify-dwim-to-point’, but to new file instead of at point."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere
   (xht--do-guess-where-from-option)
   ?f))

;;;###autoload
(defun xht-do-h*-ify-dwim-to-replace ()
  "First h*-ify region, thing at point or last-sexp; replace source.
Like ‘xht-do-h*-ify-dwim-to-point’, but replace source instead of
inserting at point."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere
   (xht--do-guess-where-from-option)
   ?r))

;;;###autoload
(defun xht-do-h*-ify-dwim-to-new-buffer ()
  "First h*-ify region, thing at point or last-sexp; insert at new buffer.
Like ‘xht-do-h*-ify-dwim-to-point’, but to new buffer instead of at point."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere
   (xht--do-guess-where-from-option)
   ?b))

;;;###autoload
(defun xht-do-h*-ify-dwim-to-point ()
  "First h*-ify region, thing at point or last-sexp; insert at point.
Equivalent to calling ‘xht-do’ and then answering
with ?c N ?i ?h 1 — where N represents the following do-what-I-mean
autodetection:
- If you have a region selected, ?r
- Otherwise, if you're inside a string, a vector, some sort of
  list, or #s(hash-table…), ?t
- Otherwise, ?x

See also ‘xht-do’ for other specialized commands listed there."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere
   (xht--do-guess-where-from-option)
   ?i))

;;;###autoload
(defun xht-do-h*-ify-selected-region-to-point ()
  "First h*-ify selected region; then insert results at point.
Equivalent to calling ‘xht-do’ and then answering
with ?c ?r ?i ?h 1.

See also ‘xht-do’ for other specialized commands listed there."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere ?r ?i))

;;;###autoload
(defun xht-do-h*-ify-thing-at-point-to-point ()
  "First h*-ify thing at point; then insert results at point.
Equivalent to calling ‘xht-do’ and then answering
with ?c ?t ?i ?h 1.

See also ‘xht-do’ for other specialized commands listed there."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere ?t ?i))

;;;###autoload
(defun xht-do-h*-ify-last-sexp-to-point ()
  "First h*-ify last-sexp; then insert results at point.
Equivalent to calling ‘xht-do’ and then answering
with ?c ?x ?i ?h 1.

See also ‘xht-do’ for other specialized commands listed there."
  (interactive)
  (xht--do-h*-ify-from-somewhere-to-somewhere ?x ?i))

;;;;;;; Helper functions

(defun xht--do-h*-ify-from-somewhere-to-somewhere (from-char to-char)
  "First h*-ify from somewhere; then insert results at point.
Helper for a few dispatcher commands.

FROM-CHAR is the character that identifies where from.
TO-CHAR is the character that identifies where to."
  (xht-do (h* :do-what    ?c
              :where-from from-char
              :where-to   to-char
              :what-to    ?h
              :format-to  ?1
              :file-to    nil)))

(defun xht--do-guess-where-from-option ()
  "Guess char option of where the thing you want to convert is at.
- If you have a region selected, ?r
- Otherwise, if you're inside a string, a vector, some sort of
  list, or #s(hash-table…), ?t
- Otherwise, ?x
Helper for a few dispatcher commands."
  (cond ((use-region-p) ?r)
        ((xht--do-type-of-thing-at-point) ?t)
        (t ?x)))

;;;;; Minor modes

;;;;;; xht-do minor mode

(xht--describe
  "A minor mode for turning on keys for xht-do actions.")

(defvar xht-do-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-x x d") 'xht-do)
    (define-key map (kbd "C-x x c") 'xht-do-convert-dwim)
    (define-key map (kbd "C-x x f") 'xht-do-h*-ify-dwim-to-file)
    ;; x because handier than i and should be frequent:
    (define-key map (kbd "C-x x x") 'xht-do-h*-ify-dwim-to-point)
    (define-key map (kbd "C-x x r") 'xht-do-h*-ify-dwim-to-replace)
    (define-key map (kbd "C-x x C") 'xht-do-h*-ify-dwim-to-clipboard)
    (define-key map (kbd "C-x x b") 'xht-do-h*-ify-dwim-to-new-buffer)
    map))

(defcustom xht-do-mode-lighter nil
  "Mode line lighter for ‘xht-do-mode’.
Either a string to display in the mode line when ‘xht-do-mode
is on, or nil to display nothing (the default)."
  :group 'xht
  :type '(choice (string :tag "Lighter" :value " xdo")
                 (const  :tag "Nothing" nil)))

;;;###autoload
(define-minor-mode xht-do-mode
  "Enable keys for ‘xht-do’ actions.
See also ‘xht-do-mode-lighter’ and ‘global-xht-do-mode’."
  :init-value nil
  :lighter xht-do-mode-lighter
  :keymap  xht-do-mode-map
  :group  'xht)

(defun xht--turn-on-do-mode ()
  "Enable ‘xht-do-mode’ globally.
Not restricted to Emacs Lisp, since you can use it for converting org tables
anywhere ‘orgtbl-mode’ is on; TSVs in ‘csv-mode’; JSONs when in ‘js-mode’;
etc."
  (or (derived-mode-p #'dired-mode)
      (derived-mode-p #'special-mode)
      (xht-do-mode)))

;;;###autoload
(define-globalized-minor-mode global-xht-do-mode
  xht-do-mode  xht--turn-on-do-mode
  :group 'xht)

;;;;;; Font lock minor mode

(xht--describe
  "A minor mode for fontifying XHT's functions and equality operators.")

(defcustom xht-fontlock-add-anaphoric-variables t
  "If non-nil, fontify anaphoric variables 'key', 'field', 'value'."
  :group 'xht
  :type 'boolean)

(defcustom xht-fontlock-add-equality-operators t
  "If non-nil, fontify equality-operators for ‘exemplify-ert’.
These show up in the many examples found in xht's README.org, as
well as in xht's ERT tests in dev/examples.el."
  :group 'xht
  :type 'boolean)

(defcustom xht-fontlock-add-exemplify-ert-macros t
  "If non-nil, fontify the macro ‘exemplify-ert’.
It shows up in xht's ERT tests in dev/examples.el."
  :group 'xht
  :type 'boolean)

(defcustom xht-fontlock-add-ht-callables t
  "If non-nil, fontify public functions and macros from ‘ht’."
  :group 'xht
  :type 'boolean)

(defcustom xht-fontlock-add-xht-callables t
  "If non-nil, fontify public functions and macros from ‘xht’."
  :group 'xht
  :type 'boolean)

(defcustom xht-fontlock-add-xht-helpers nil
  "If non-nil, fontify private functions and macros from ‘xht’."
  :group 'xht
  :type 'boolean)

;; TODO: automate retrieval of these functions/macros/aliases names
(defvar xht--callables-ht
  '(ht
    ht-clear ht-contains-p ht-delete-if ht-each ht-empty-p
    ht-equal-p ht-from-alist ht-from-plist ht-p ht-remove ht-set
    ht-to-alist ht-to-plist ht-update ht-update-with! ht? ht-clear!
    ht-contains? ht-create ht-empty? ht-get ht-get* ht-remove! ht-set! ht-size
    ht-aeach ht-amap ht->alist ht->plist ht-equal? ht-find ht-items ht-keys
    ht-map ht-merge ht-reject ht-reject! ht-select ht-select-keys ht-update!
    ht-values ht<-alist ht<-plist)
  "List of ‘ht’ public callables.")

(defvar xht--callables-xht
  '(h-new
    h-empty-clone h-clr h-clr! h* h-s* h-t* h-st* h-reduce-r
    h--zip-vectors-with h-zip-vectors-with h-zip-vectors h--zip-lists-with
    h-zip-lists-with h-zip-lists h-let h-let-it h-htbl-form h-htbl-cm-str
    h-htbl-pp-str h-ht-form h-ht-cm-str h-ht-pp-str h-h*-form h-h*-cm-str
    h-h*-pp-str h-lambdify h-format h-vmap h--vmap h--vunzip-with
    h-vunzip-with h-vunzip h-vitems h-vkeys h-vvalues h-vrandom h-lmap h--lmap
    h-lkeep h--lkeep h--lunzip-with h-lunzip-with h-lunzip h-litems h-lkeys
    h-lvalues h-lrandom h-hmap h--hmap h-hmap! h--hmap! h-hmap* h--hmap*
    h-hmap*! h--hmap*! h-each h--each h-pop h-pop! h-pop-random h-pop-random!
    h-first h--first h-first! h--first! h-put! h-put h-put*! h-put* h-put-add!
    h-put-add h-put-add*! h-put-add* h-rem! h-rem h-rem*! h-rem* h-sel-keys
    h-sel-keys! h-sel h--sel h-sel! h--sel! h-sel* h--sel* h-sel*! h--sel*!
    h-rej-keys h-rej-keys! h-rej h--rej h-rej! h--rej! h-rej* h--rej* h-rej*!
    h--rej*! h-reverse h-reverse! h-reverse* h-reverse*! h-put-num-with*
    h-put-num-with*! h-put-inc* h-put-inc*! h-put-dec* h-put-dec*! h-has-key?
    h-has-key*? h-has-value? h-has-value*? h-has-pair? h-has-pair*?
    h-has-keys? h-has-values? h-has-pairs? h-mix! h-mix h-mix*! h-mix* h-dif!
    h-dif h-dif*! h-dif* h-cmn! h-cmn h-cmn*! h-cmn* h-kvl= h-pr== h-pr= h==
    h= h_= h~= h-props= h-alist= h-plist= h-json= h-lol= h-orgtbl= h-tsv=
    h-csv= h-ssv= h-type h-type= h-it= h-it~ h-clone* h<-ht h-2d<-1d h-1d<-2d
    h<-vector h->vector h<-list h->list h<-lines h->lines h<-kvl h->kvl
    h<-cons-pair h->cons-pair h<-alist h<-alist* h->alist h->alist* h<-plist
    h<-plist* h->plist h->plist* h<-json* h->json* h<-lol h->lol h<-orgtbl
    h->orgtbl h<-tsv h->tsv h<-ssv h->ssv h->csv h<-it h-kvl? h-alist?
    h-plist? h-lol? h-orgtbl? h-json? h-tsv? h-csv? h-ssv? h-prop h-props
    h-dim h-1d? h-2d? h-nested? h-it-dim h-it-empty? h-it-1d? h-it-2d?
    h-it-nested? h-write! h-write-ht-like! h-write-htbl-cm! h-write-htbl-pp!
    h-write-h*-cm! h-write-ht-cm! h-write-h*-pp! h-write-ht-pp! h-read
    h-read-ht-like h-read-htbl h-read-s h-read-tsv h-read-csv h-read-ssv
    h-read-json h-read-orgtbl h-read-lol h-read-alist h-read-plist h-read-list
    h-read-vector h-read-kvl h-read-lines h-read-cons-pair h-as-symbol
    h-as-string h-as-keyword h-as-number h-2d-new h-2d-header h-2d-idcol
    h-2d-id h-2d-row h-2d-col h-2d-hmap h--2d-hmap h-2d-hmap! h--2d-hmap!
    h-2d-sel-cols h-2d-sel-cols! h-2d-rej-cols h-2d-rej-cols! h-2d-transpose
    h-2d-transpose! h-as-set<-vector h-as-set<-list h-as-set<-lines
    h-as-set<-words h-count<-vector h-count<-list h-count<-lines
    h-count<-words h-count-put h-count-put! h? h-size h-copy h-get h-get*
    h-empty? h-has-key-p h-has-key*-p h-has-keys-p h-has-value-p
    h-has-value*-p h-has-values-p h-has-pair-p h-has-pair*-p h-has-pairs-p
    h-1d-p h-2d-p h-nested-p h-it-1d-p h-it-2d-p h-it-nested-p h-csv-p h-ssv-p
    h-tsv-p h-kvl-p h-lol-p h-json-p h-alist-p h-plist-p h-orgtbl-p h-p
    h-empty-p h-random h-unzip-with h--unzip-with h-unzip h-items h-keys
    h-values h-write-h*! h-write-ht! h-write-htbl! h-read-h h-2d-keys h-1d->2d
    h-2d->1d h<-csv
    ;; Commands
    xht-see-readme xht-see-function-in-readme xht-see-news xht-do-mode
    global-xht-do-mode xht-fontify-mode global-xht-fontify-mode xht-do
    xht-do-convert-dwim xht-do-h*-ify-dwim-to-clipboard
    xht-do-h*-ify-dwim-to-file xht-do-h*-ify-dwim-to-replace
    xht-do-h*-ify-dwim-to-new-buffer xht-do-h*-ify-dwim-to-point
    xht-do-h*-ify-selected-region-to-point
    xht-do-h*-ify-thing-at-point-to-point xht-do-h*-ify-last-sexp-to-point)
  "List of ‘xht’ public callables.")

(defvar xht--helpers-xht
  '(xht--init-size
    xht--init-size-exact xht--init-size-fixed
    xht--let-list-to-sexp xht--let-access-sexp xht--let-remove-dot
    xht--let-deep-dot-search xht--ht-like-format-type xht--put-add
    xht--reverse*! xht--number-with xht--number-with-1+ xht--number-with-1-
    xht--has-x*? xht--mix! xht--mix*! xht--dif! xht--dif*! xht--cmn!
    xht--cmn*! xht--equal? xht--props= xht---pr== xht---pr= xht--== xht--=
    xht--_= xht--~= xht--type= xht--it= xht--it~ xht--equal-many
    xht--thing-to-nat xht<--alist* xht-->alist* xht<--plist* xht-->plist*
    xht--list-to-orgtbl-row xht--list-to-tsv-row xht--2d-col-width
    xht--list-to-ssv-row xht<--*sv xht<--tsv xht<--csv xht<--ssv xht--*sv?
    xht--itify xht--ht-and-empty-or-2d+-or-error xht--count-put-ht!
    xht--count-put-vector! xht--count-put-list! xht--count-put-elem!
    xht--string->symbol xht--keyword->symbol xht--keyword->string
    xht--symbol->string xht--string->keyword xht--symbol->keyword
    xht--string->number xht--symbol->number xht--keyword->number
    xht--number->string xht--number->keyword xht--number->symbol xht--to-type
    xht--htstr-to-htbl xht--str-to-htbl xht--htbl-or-error xht--str-to-alist
    xht--canonicalize-alist xht--alist-or-error xht--str-to-plist
    xht--canonicalize-plist xht--plist-or-error xht--str-to-lol
    xht--canonicalize-lol xht--lol-or-error xht--dequote-list xht--str-to-list
    xht--list-or-error xht--cons-pair-or-error xht--str-to-vector
    xht--vector-or-error xht--str-nw? xht--quote xht--non-aggressively-insert
    xht--file-str-no-prop xht--buff-str-no-prop xht--remove-nested-nils
    xht--misc-examples xht--describe
    ;; Helpers for commands
    xht--display-org-subtree xht--goto-org-heading xht--goto-org-heading-pos
    xht--see-library-readme xht--locate-library-readme xht--libname->libfile
    xht--display-org-subtree xht--turn-on-do-mode xht--do-ht-opts-cleanup
    xht--do-input xht--do-convert-or-as-is xht--do-where-from xht--do-where-to
    xht--do-what-to-0D xht--do-what-to-1D-e xht--do-what-to-1D-i
    xht--do-what-to-2D xht--do-what-to-nested xht--do-format-to-ht-all
    xht--do-format-to-ht-write xht--do-format-to-non-ht-all
    xht--do-format-to-non-ht-write xht--do-make-final-string xht--do-read-from
    xht--do-read-file xht--do-read-clipboard xht--do-read-last-sexp
    xht--do-read-whole-buffer xht--do-read-selected-region
    xht--do-read-thing-at-point xht--do-type-of-thing-at-point
    xht--do-type-of-lstvec-at-point-maybe xht--do-as-string-orgtbl-at-point
    xht--do-as-string-string-at-point xht--do-as-string-lstvec-at-point
    xht--do-as-string-hshtbl-at-point xht--do-at-hash-table?
    xht--do-goto-hash-table-beginning-maybe
    xht--do-at-beginning-of-hash-table?
    xht--do-boundaries-of-hash-table-at-point xht--do-boundaries-of-last-sexp
    xht--do-boundaries-of-thing-at-point xht--do-boundaries-of-selected-region
    xht--do-json-format xht--do-json-cm-ify xht--do-json-pp-ify
    xht--do-string-cm-ify xht--do-string-escape xht--do-string-escape-ws
    xht--insert-str-from-here xht--non-aggressively-insert-str-from-here
    xht--if-with-spacing xht--to-display-buffer xht--form-last-sexp-eval
    xht--form-last-sexp xht--form-last-sexp-str
    xht--do-h*-ify-from-somewhere-to-somewhere xht--do-guess-where-from-option
    xht--fontlock-make-keywords xht--fontlock-rx-anaphoric-variables
    xht--fontlock-rx-exemplify-ert-macros xht--fontlock-make-rx
    xht--turn-on-fontify-mode)
  "List of ‘xht’ helpers.")

(defvaralias 'xht--equality-operators 'xht--ops)
(defvar xht--ops '(!!>
                   => ~>
                   Hp==> Hp=>
                   H==> H=> H_=> H~=>
                   K=> L=> T=>
                   C=> S=> A=>
                   P=> J=> O=>)
  "List of equality operators used by h-defs — as symbols.")

(defun xht--fontlock-make-keywords ()
  "Make keywords for fontlock."
  (-non-nil
   (list (when xht-fontlock-add-anaphoric-variables
           (xht--fontlock-rx-anaphoric-variables))
         (when xht-fontlock-add-exemplify-ert-macros
           (xht--fontlock-rx-exemplify-ert-macros))
         (when xht-fontlock-add-equality-operators
           (xht--fontlock-make-rx  xht--equality-operators))
         (when xht-fontlock-add-ht-callables
           (xht--fontlock-make-rx  xht--callables-ht))
         (when xht-fontlock-add-xht-callables
           (xht--fontlock-make-rx  xht--callables-xht))
         (when xht-fontlock-add-xht-helpers
           (xht--fontlock-make-rx  xht--helpers-xht)))))

(defun xht--fontlock-rx-anaphoric-variables ()
  ;; TODO: Do not fontify these ^ globally;
  ;;       detect and limit to their local anaphoric scope.
  "The rx for anaphoric variables."
  `(,(rx symbol-start (| "key" "field" "value") symbol-end)
    0 font-lock-variable-name-face))

(defun xht--fontlock-rx-exemplify-ert-macros ()
  ;; Macros in dev/examples.el. Based on ‘lisp-mode-symbol-regexp’.
  "The rx for ‘exemplify-ert’ macros."
  `(,(rx ?\( (group (| "exemplify-ert")) symbol-end
         (+ (in "\t "))
         (group (* (| (syntax word) (syntax symbol) (: ?\\ nonl)))))
    (1 font-lock-keyword-face)
    (2 font-lock-function-name-face)))

(defun xht--fontlock-make-rx (symbols-list)
  "Make rx using SYMBOLS-LIST."
  (let* ((strings  (-map #'symbol-name (-uniq symbols-list)))
         (rx-1    `(| ,@strings))
         (rx      `(rx symbol-start ,rx-1 symbol-end)))
    (eval rx)))

(defcustom xht-fontify-mode-lighter nil
  "Mode line lighter for ‘xht-fontify-mode’.
Either a string to display in the mode line when ‘xht-fontify-mode
is on, or nil to display nothing (the default)."
  :group 'xht
  :type '(choice (string :tag "Lighter" :value " xhtf")
                 (const  :tag "Nothing" nil)))

;;;###autoload
(define-minor-mode xht-fontify-mode
  "Toggle fontification of XHT/HT special variables.
This is a buffer-local minor mode intended for Emacs Lisp buffers.
Enabling it causes syntax highlighting of:

- xht's callables (functions, macros, aliases)

- ht's  callables (functions, macros, aliases)

- anaphoric variables such as ‘key’ and ‘value’.

- exemplify-ert macros and equality operators
  (for testing and examples — see ./dev folder)


 They are all turned on when ‘global-xht-fontify-mode’ is on.

 If you wish, you can selectively disable any of them, through the
 customize interface: M-x customize-group RET xht RET. See there
 the variables starting with 'xht-fontlock-add-'.

 See also ‘xht-fontify-mode-lighter’ and ‘global-xht-fontify-mode’."
  :group 'xht :lighter xht-fontify-mode-lighter
  (if xht-fontify-mode
      (font-lock-add-keywords nil (xht--fontlock-make-keywords) t)
    (font-lock-remove-keywords nil (xht--fontlock-make-keywords)))
  (cond ((fboundp 'font-lock-flush) ;; Added in Emacs 25.
         (font-lock-flush))
        ;; font-lock-fontify-buffer’ unconditionally enables
        ;; font-lock-mode’ and is marked ‘interactive-only’ in later
        ;; Emacs versions which have ‘font-lock-flush’, so we guard
        ;; and pacify as needed, respectively.
        (font-lock-mode
         (with-no-warnings
           (font-lock-fontify-buffer)))))

(defun xht--turn-on-fontify-mode ()
  "Enable ‘xht-fontify-mode’ if in an Emacs Lisp buffer."
  (when (derived-mode-p #'emacs-lisp-mode)
    (xht-fontify-mode)))

;;;###autoload
(define-globalized-minor-mode global-xht-fontify-mode
  xht-fontify-mode xht--turn-on-fontify-mode
  :group 'xht)

(defcustom xht-enable-fontlock nil
  "If non-nil, fontify XHT macro calls and special variables."
  :group 'xht
  :set (lambda (sym val)
         (set-default sym val)
         (global-xht-fontify-mode (if val 1 0)))
  :type 'boolean)

;;;;; See README

(xht--describe
  "Commands to open xht's README.org. Optionally, find things in it.")

;;;###autoload
(defun xht-see-readme (&optional heading narrow)
  "Open xht's README.org file.
Search for the file in xht.el's directory.

If found, open it read-only.

If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string. If prefixed
by \"cid:\", search it by CUSTOM_ID.

If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found."
  (interactive)
  (let ((readme xht--readme-org))
    (if (file-exists-p readme)
        (let ((pr (make-progress-reporter
                   (format "Opening %s ... "
                           (abbreviate-file-name readme)))))
          (find-file-read-only readme)
          (when heading
            (xht--goto-org-heading heading narrow))
          (progress-reporter-done pr))
      (message "Couldn't find %s's README.org" xht--name))))

;;;###autoload
(defun xht-see-function-in-readme (&optional function)
  "See FUNCTION in xht's README.org.
When called interactively, select function from minibuffer."
  (interactive)
  (let* ((fun (h-as-string
               (if function
                   (if (memq (h-as-symbol function) xht--callables-xht)
                       function
                     (error "%s isn't a public xht function" function))
                 (completing-read "Choose a function: "
                                  xht--callables-xht))))
         (pr  (make-progress-reporter
               (format "Looking for #'%s ... " fun))))
    ;; regex-quote’ needed to escape asterisks in function names
    (xht-see-readme (format "cid:%s" (regexp-quote fun)))
    (progress-reporter-done pr)))

;;;###autoload
(defun xht-see-news ()
  "See the News in xht's README.org file."
  (interactive)
  (xht-see-readme "News" 'narrow)
  (xht--display-org-subtree))

;;;;;; Helper functions

(defun xht--display-org-subtree ()
  "Selectively display org subtree."
  (let ((cmds '(outline-hide-subtree
                outline-show-children
                outline-next-heading
                outline-show-branches)))
    (and (fboundp (nth 0 cmds))
         (fboundp (nth 1 cmds))
         (fboundp (nth 2 cmds))
         (fboundp (nth 3 cmds))
         (mapc #'funcall cmds))))

(defun xht--goto-org-heading (heading &optional narrow)
  "Navigate to org HEADING and optionally NARROW to it.
If HEADING is prefixed by \"cid:\", search it by CUSTOM_ID."
  (when-let ((pos (xht--find-org-heading-pos heading)))
    (widen)
    (goto-char pos)
    (when (fboundp 'outline-show-subtree)
      (outline-show-subtree))
    (if (and narrow (fboundp 'org-narrow-to-subtree))
        (org-narrow-to-subtree)
      (recenter-top-bottom 0))
    (when (fboundp 'org-flag-drawer)
      (save-excursion
        (forward-line 1)
        (org-flag-drawer t)))))

(defun xht--find-org-heading-pos (heading)
  "Find position of org HEADING in current org file.
If HEADING is prefixed by \"cid:\", search it by CUSTOM_ID.
If found, return position. Otherwise nil."
  (unless (stringp heading)
    (error "Heading must be a string"))
  (let* ((cid? (s-starts-with? "cid:" heading))
         (hdn  (s-chop-prefix  "cid:" heading))
         (frx  (if cid?
                   "^:CUSTOM_ID: +%s *$"
                 "^[*]+ %s"))
         (hrx  (format frx hdn)))
    (save-match-data
      (save-excursion
        (save-restriction
          (widen)
          (goto-char (point-max))
          (re-search-backward hrx nil t 1)
          (if cid?
              (re-search-backward "^[*]+ " nil t 1)
            (point)))))))

;;;;;;; old helpers
;; These helpers are no longer needed, but are kept here anyway:
(defun xht--see-library-readme (libname)
  "Open library LIBNAME's README file.
Load LIBNAME, if not yet loaded, then search for README.org in the
directory from which it has been loaded.

If not found, look for README.md, README.markdown, README.rst,
README.txt, and README. The first one found, if any, is opened
read-only."
  (--> (xht--locate-library-readme libname)
       (if (not it)
           (message "Couldn't find a README file for %s" libname)
         (let ((pr (make-progress-reporter
                    (format "Opening %s ... "
                            (abbreviate-file-name it)))))
           (find-file-read-only it)
           (progress-reporter-done pr)))))

(defun xht--locate-library-readme (libname)
  "Locate library LIBNAME's README file.
Load LIBNAME, if not yet loaded, then search for README.org in the
directory from which it has been loaded.

If not found, look for README.md, README.markdown, README.rst,
README.txt, and README. Return the filename if found, nil
otherwise."
  (--> (xht--libname->libfile libname)
       (expand-file-name "README" (file-name-directory it))
       (let* ((exts '(".org" ".md" ".markdown" ".rst" ".txt" ""))
              (all  (-map (lambda (ext) (format "%s%s" it ext)) exts)))
         (car (-filter #'file-exists-p all)))))

(defun xht--libname->libfile (libname)
  "Given LIBNAME, load library and return filename it loaded from."
  (require (if (symbolp libname)
               libname
             (intern libname)))
  (--> load-history
       (-map #'car it)
       (car (-filter (lambda (lib)
                       (when (string-match
                              (format "/%s.el[cn]*$" libname)
                              lib)
                         lib))
                     it))))

;;;; Aliases
;;;;; For those who prefer to use a single namespace
;;;;;; To ht functions

;; These are aliases to current functions of the ht package that we haven't so
;; far proposed to modify or extend.

(defalias 'h-get          'ht-get)
(defalias 'h-get*         'ht-get*)
(defalias 'h-empty?       'ht-empty?)

;;;;;; To native functions
(defalias 'h?             'hash-table-p)     ;; same as ‘ht?
(defalias 'h-size         'hash-table-count) ;; like ‘ht-size
(defalias 'h-copy         'copy-hash-table)  ;; like ‘ht-copy

;;;;; Preds: users of 'p' for y/n questions will be pleased — amiritep
;;;;;; To h functions
(defalias 'h-has-key-p    'h-has-key?)
(defalias 'h-has-key*-p   'h-has-key*?)
(defalias 'h-has-keys-p   'h-has-keys?)

(defalias 'h-has-value-p  'h-has-value?)
(defalias 'h-has-value*-p 'h-has-value*?)
(defalias 'h-has-values-p 'h-has-values?)

(defalias 'h-has-pair-p   'h-has-pair?)
(defalias 'h-has-pair*-p  'h-has-pair*?)
(defalias 'h-has-pairs-p  'h-has-pairs?)

(defalias 'h-1d-p         'h-1d?)
(defalias 'h-2d-p         'h-2d?)
(defalias 'h-nested-p     'h-nested?)

(defalias 'h-it-1d-p      'h-it-1d?)
(defalias 'h-it-2d-p      'h-it-2d?)
(defalias 'h-it-nested-p  'h-it-nested?)

(defalias 'h-csv-p        'h-csv?)
(defalias 'h-ssv-p        'h-ssv?)
(defalias 'h-tsv-p        'h-tsv?)
(defalias 'h-kvl-p        'h-kvl?)
(defalias 'h-lol-p        'h-lol?)
(defalias 'h-json-p       'h-json?)
(defalias 'h-alist-p      'h-alist?)
(defalias 'h-plist-p      'h-plist?)
(defalias 'h-orgtbl-p     'h-orgtbl?)

;;;;;; To ht functions
(defalias 'h-empty-p      'ht-empty-p)

;;;;;; To native functions

;; Here the Emacs standard recommendation of 'hp' would get us out of h's
;; namespace. It could also displease certain printer manufacturers...

(defalias 'h-p            'hash-table-p)

;;;;; DWIM
;;;;;; because usually lists is what you want
(defalias 'h-random       'h-lrandom)
(defalias 'h-unzip-with   'h-lunzip-with)
(defalias 'h--unzip-with  'h--lunzip-with)
(defalias 'h-unzip        'h-lunzip)
(defalias 'h-items        'h-litems)
(defalias 'h-keys         'h-lkeys)
(defalias 'h-values       'h-lvalues)

;; Note however that ‘h-lmap’ and ‘h-lkeep’ are NOT aliased, and that
;; functions exist that map to hash table: ‘h-hmap’, ‘h-hmap!’, ‘h-hmap*’,
;; h-hmap*!’.

;;;;;; because you probably want it in pretty-printed format
(defalias 'h-write-h*!    'h-write-h*-pp!)
(defalias 'h-write-ht!    'h-write-ht-pp!)
(defalias 'h-write-htbl!  'h-write-htbl-pp!)

;;;;; Just shorter
(defalias 'h-read-h       'h-read-ht-like)

;; Note: no need for a corresponding 'h-write-h!' — just use one of the
;; specific ones for each format: ‘h-write-ht-cm!’, ‘h-write-h*!’,
;; h-write-htbl!’, etc.

;;;;; Consistency
(defalias 'h-2d-keys      'h-lkeys)

;;;;; Directional: make any order work
(defalias 'h-1d->2d      'h-2d<-1d)
(defalias 'h-2d->1d      'h-1d<-2d)

;;;;; Provisional: until other functions have been implemented
;; It'd be good to have a direct conversion not dependent on org.
;; Aliasing to the org-based version until then.
(defalias 'h<-csv 'xht<--csv
  "Provisional aliasing to ‘xht<--csv’ until implementation.")

;;;; Wrapping up

(provide 'xht)

;; Local Variables:
;; coding:                     utf-8
;; indent-tabs-mode:           nil
;; sentence-end-double-space:  nil
;; outline-regexp:             ";;;;* "
;; End:

;;; xht.el ends here