aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lisp/emacs-lisp/map.el270
-rw-r--r--test/automated/map-test.el324
2 files changed, 594 insertions, 0 deletions
diff --git a/lisp/emacs-lisp/map.el b/lisp/emacs-lisp/map.el
new file mode 100644
index 00000000000..fec06343f7c
--- /dev/null
+++ b/lisp/emacs-lisp/map.el
@@ -0,0 +1,270 @@
1;;; map.el --- Map manipulation functions -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2015 Free Software Foundation, Inc.
4
5;; Author: Nicolas Petton <nicolas@petton.fr>
6;; Keywords: convenience, map, hash-table, alist, array
7;; Version: 1.0
8;; Package: map
9
10;; Maintainer: emacs-devel@gnu.org
11
12;; This file is part of GNU Emacs.
13
14;; GNU Emacs is free software: you can redistribute it and/or modify
15;; it under the terms of the GNU General Public License as published by
16;; the Free Software Foundation, either version 3 of the License, or
17;; (at your option) any later version.
18
19;; GNU Emacs is distributed in the hope that it will be useful,
20;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22;; GNU General Public License for more details.
23
24;; You should have received a copy of the GNU General Public License
25;; along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27;;; Commentary:
28
29;; map.el provides map-manipulation functions that work on alists,
30;; hash-table and arrays. All functions are prefixed with "map-".
31;;
32;; Functions taking a predicate or iterating over a map using a
33;; function take the function as their first argument. All other
34;; functions take the map as their first argument.
35
36;; TODO:
37;; - Add support for char-tables
38;; - Maybe add support for gv?
39;; - See if we can integrate text-properties
40;; - A macro similar to let-alist but working on any type of map could
41;; be really useful
42
43;;; Code:
44
45(require 'seq)
46
47(defun map-elt (map key &optional default)
48 "Perform a lookup in MAP of KEY and return its associated value.
49If KEY is not found, return DEFAULT which defaults to nil.
50
51If MAP is a list, `assoc' is used to lookup KEY."
52 (map--dispatch map
53 :list (or (cdr (assoc key map)) default)
54 :hash-table (gethash key map default)
55 :array (or (ignore-errors (elt map key)) default)))
56
57(defmacro map-put (map key value)
58 "In MAP, associate KEY with VALUE and return MAP.
59If KEY is already present in MAP, replace its value with VALUE."
60 (declare (debug t))
61 `(progn
62 (map--dispatch (m ,map m)
63 :list (setq ,map (cons (cons ,key ,value) m))
64 :hash-table (puthash ,key ,value m)
65 :array (aset m ,key ,value))))
66
67(defmacro map-delete (map key)
68 "In MAP, delete the key KEY if present and return MAP.
69If MAP is an array, store nil at the index KEY."
70 (declare (debug t))
71 `(progn
72 (map--dispatch (m ,map m)
73 :list (setq ,map (map--delete-alist m ,key))
74 :hash-table (remhash ,key m)
75 :array (aset m ,key nil))))
76
77(defun map-nested-elt (map keys &optional default)
78 "Travserse MAP using KEYS and return the looked up value or DEFAULT if nil.
79Map can be a nested map composed of alists, hash-tables and arrays."
80 (or (seq-reduce (lambda (acc key)
81 (when (map-p acc)
82 (map-elt acc key)))
83 keys
84 map)
85 default))
86
87(defun map-keys (map)
88 "Return the list of keys in MAP."
89 (map-apply (lambda (key value) key) map))
90
91(defun map-values (map)
92 "Return the list of values in MAP."
93 (map-apply (lambda (key value) value) map))
94
95(defun map-pairs (map)
96 "Return the elements of MAP as key/value association lists."
97 (map-apply (lambda (key value)
98 (cons key value))
99 map))
100
101(defun map-length (map)
102 "Return the length of MAP."
103 (length (map-keys map)))
104
105(defun map-copy (map)
106 "Return a copy of MAP."
107 (map--dispatch map
108 :list (seq-copy map)
109 :hash-table (copy-hash-table map)
110 :array (seq-copy map)))
111
112(defun map-apply (function map)
113 "Return the result of applying FUNCTION to each element of MAP.
114FUNCTION is called with two arguments, the key and the value."
115 (funcall (map--dispatch map
116 :list #'map--apply-alist
117 :hash-table #'map--apply-hash-table
118 :array #'map--apply-array)
119 function
120 map))
121
122(defun map-keys-apply (function map)
123 "Return the result of applying FUNCTION to each key of MAP."
124 (map-apply (lambda (key val)
125 (funcall function key))
126 map))
127
128(defun map-values-apply (function map)
129 "Return the result of applying FUNCTION to each value of MAP."
130 (map-apply (lambda (key val)
131 (funcall function val))
132 map))
133
134(defun map-filter (pred map)
135 "Return an alist of the key/val pairs of which (PRED key val) is non-nil in MAP."
136 (delq nil (map-apply (lambda (key val)
137 (if (funcall pred key val)
138 (cons key val)
139 nil))
140 map)))
141
142(defun map-remove (pred map)
143 "Return an alist of the key/val pairs of which (PRED key val) is nil in MAP."
144 (map-filter (lambda (key val) (not (funcall pred key val)))
145 map))
146
147(defun map-p (map)
148 "Return non-nil if MAP is a map (list, hash-table or array)."
149 (or (listp map)
150 (hash-table-p map)
151 (arrayp map)))
152
153(defun map-empty-p (map)
154 "Return non-nil is MAP is empty.
155MAP can be a list, hash-table or array."
156 (null (map-keys map)))
157
158(defun map-contains-key-p (map key &optional testfn)
159 "Return non-nil if MAP contain the key KEY, nil otherwise.
160Equality is defined by TESTFN if non-nil or by `equal' if nil.
161MAP can be a list, hash-table or array."
162 (seq-contains-p (map-keys map) key testfn))
163
164(defun map-some-p (pred map)
165 "Return any key/value pair for which (PRED key val) is non-nil is MAP."
166 (catch 'map--break
167 (map-apply (lambda (key value)
168 (when (funcall pred key value)
169 (throw 'map--break (cons key value))))
170 map)
171 nil))
172
173(defun map-every-p (pred map)
174 "Return non-nil if (PRED key val) is non-nil for all elements of the map MAP."
175 (catch 'map--break
176 (map-apply (lambda (key value)
177 (or (funcall pred key value)
178 (throw 'map--break nil)))
179 map)
180 t))
181
182(defun map-merge (type &rest maps)
183 "Merge into a map of type TYPE all the key/value pairs in the maps MAPS."
184 (let (result)
185 (while maps
186 (map-apply (lambda (key value)
187 (map-put result key value))
188 (pop maps)))
189 (map-into result type)))
190
191(defun map-into (map type)
192 "Convert the map MAP into a map of type TYPE.
193TYPE can be one of the following symbols: list or hash-table."
194 (pcase type
195 (`list (map-pairs map))
196 (`hash-table (map--into-hash-table map))))
197
198(defmacro map--dispatch (spec &rest args)
199 "Evaluate one of the provided forms depending on the type of MAP.
200
201SPEC can be a map or a list of the form (VAR MAP [RESULT]).
202ARGS should have the form [TYPE FORM]...
203
204The following keyword types are meaningful: `:list',
205`:hash-table' and `array'.
206
207An error is thrown if MAP is neither a list, hash-table or array.
208
209Return RESULT if non-nil or the result of evaluation of the
210form.
211
212\(fn (VAR MAP [RESULT]) &rest ARGS)"
213 (declare (debug t) (indent 1))
214 (unless (listp spec)
215 (setq spec `(,spec ,spec)))
216 (let ((map-var (car spec))
217 (result-var (make-symbol "result")))
218 `(let ((,map-var ,(cadr spec))
219 ,result-var)
220 (setq ,result-var
221 (cond ((listp ,map-var) ,(plist-get args :list))
222 ((hash-table-p ,map-var) ,(plist-get args :hash-table))
223 ((arrayp ,map-var) ,(plist-get args :array))
224 (t (error "Unsupported map: %s" ,map-var))))
225 ,@(when (cddr spec)
226 `((setq ,result-var ,@(cddr spec))))
227 ,result-var)))
228
229(defun map--apply-alist (function map)
230 "Private function used to apply FUNCTION over MAP, MAP being an alist."
231 (seq-map (lambda (pair)
232 (funcall function
233 (car pair)
234 (cdr pair)))
235 map))
236
237(defun map--apply-hash-table (function map)
238 "Private function used to apply FUNCTION over MAP, MAP being a hash-table."
239 (let (result)
240 (maphash (lambda (key value)
241 (push (funcall function key value) result))
242 map)
243 (nreverse result)))
244
245(defun map--apply-array (function map)
246 "Private function used to apply FUNCTION over MAP, MAP being an array."
247 (let ((index 0))
248 (seq-map (lambda (elt)
249 (prog1
250 (funcall function index elt)
251 (setq index (1+ index))))
252 map)))
253
254(defun map--delete-alist (map key)
255 "Return MAP with KEY removed."
256 (seq-remove (lambda (pair)
257 (equal key (car pair)))
258 map))
259
260(defun map--into-hash-table (map)
261 "Convert MAP into a hash-table."
262 (let ((ht (make-hash-table :size (map-length map)
263 :test 'equal)))
264 (map-apply (lambda (key value)
265 (map-put ht key value))
266 map)
267 ht))
268
269(provide 'map)
270;;; map.el ends here
diff --git a/test/automated/map-test.el b/test/automated/map-test.el
new file mode 100644
index 00000000000..8a12be84aa1
--- /dev/null
+++ b/test/automated/map-test.el
@@ -0,0 +1,324 @@
1;;; map-tests.el --- Tests for map.el
2
3;; Copyright (C) 2015 Free Software Foundation, Inc.
4
5;; Author: Nicolas Petton <nicolas@petton.fr>
6;; Maintainer: emacs-devel@gnu.org
7
8;; This file is part of GNU Emacs.
9
10;; GNU Emacs is free software: you can redistribute it and/or modify
11;; it under the terms of the GNU General Public License as published by
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23;;; Commentary:
24
25;; Tests for map.el
26
27;;; Code:
28
29(require 'ert)
30(require 'map)
31
32(defmacro with-maps-do (alist-name vec-name ht-name &rest body)
33 (declare (indent 3))
34 `(let ((,alist-name '((a . 2)
35 (b . 3)
36 (c . 4)))
37 (,vec-name (make-vector 3 nil))
38 (,ht-name (make-hash-table)))
39 (aset ,vec-name 0 'a)
40 (aset ,vec-name 1 'b)
41 (aset ,vec-name 2 'c)
42 (puthash 'a 2 ,ht-name)
43 (puthash 'b 3 ,ht-name)
44 (puthash 'c 4 ,ht-name)
45 (progn
46 ,@body)))
47
48(ert-deftest test-map-elt ()
49 (with-maps-do alist vec ht
50 (assert (= 2 (map-elt alist 'a)))
51 (assert (= 3 (map-elt alist 'b)))
52 (assert (= 4 (map-elt alist 'c)))
53 (assert (null (map-elt alist 'd)))
54 (assert (= 2 (map-elt ht 'a)))
55 (assert (= 3 (map-elt ht 'b)))
56 (assert (= 4 (map-elt ht 'c)))
57 (assert (null (map-elt ht 'd)))
58 (assert (eq 'a (map-elt vec 0)))
59 (assert (eq 'b (map-elt vec 1)))
60 (assert (eq 'c (map-elt vec 2)))
61 (assert (null (map-elt vec 3)))))
62
63(ert-deftest test-map-elt-default ()
64 (with-maps-do alist vec ht
65 (assert (= 5 (map-elt alist 'd 5)))
66 (assert (= 5 (map-elt vec 4 5)))
67 (assert (= 5 (map-elt ht 'd 5)))))
68
69(ert-deftest test-map-put ()
70 (with-maps-do alist vec ht
71 (map-put alist 'd 4)
72 (assert (= (map-elt alist 'd) 4))
73 (map-put alist 'd 5)
74 (assert (= (map-elt alist 'd) 5))
75 (map-put ht 'd 4)
76 (assert (= (map-elt ht 'd) 4))
77 (map-put ht 'd 5)
78 (assert (= (map-elt ht 'd) 5))
79 (map-put vec 0 'd)
80 (assert (eq (map-elt vec 0) 'd))
81 (should-error (map-put vec 4 'd))))
82
83(ert-deftest test-map-put-literal ()
84 (assert (= (map-elt (map-put [1 2 3] 1 4) 1)
85 4))
86 (assert (= (map-elt (map-put (make-hash-table) 'a 2) 'a)
87 2))
88 (should-error (map-put '((a . 1)) 'b 2))
89 (should-error (map-put '() 'a 1)))
90
91(ert-deftest test-map-put-return-value ()
92 (let ((ht (make-hash-table)))
93 (assert (eq (map-put ht 'a 'hello) ht))))
94
95(ert-deftest test-map-delete ()
96 (with-maps-do alist vec ht
97 (map-delete alist 'a)
98 (assert (null (map-elt alist 'a)))
99 (map-delete ht 'a)
100 (assert (null (map-elt ht 'a)))
101 (map-delete vec 2)
102 (assert (null (map-elt vec 2)))))
103
104(ert-deftest test-map-delete-return-value ()
105 (let ((ht (make-hash-table)))
106 (assert (eq (map-delete ht 'a) ht))))
107
108(ert-deftest test-map-nested-elt ()
109 (let ((vec [a b [c d [e f]]]))
110 (assert (eq (map-nested-elt vec '(2 2 0)) 'e)))
111 (let ((alist '((a . 1)
112 (b . ((c . 2)
113 (d . 3)
114 (e . ((f . 4)
115 (g . 5))))))))
116 (assert (eq (map-nested-elt alist '(b e f))
117 4)))
118 (let ((ht (make-hash-table)))
119 (map-put ht 'a 1)
120 (map-put ht 'b (make-hash-table))
121 (map-put (map-elt ht 'b) 'c 2)
122 (assert (eq (map-nested-elt ht '(b c))
123 2))))
124
125(ert-deftest test-map-nested-elt-default ()
126 (let ((vec [a b [c d]]))
127 (assert (null (map-nested-elt vec '(2 3))))
128 (assert (null (map-nested-elt vec '(2 1 1))))
129 (assert (= 4 (map-nested-elt vec '(2 1 1) 4)))))
130
131(ert-deftest test-map-p ()
132 (assert (map-p nil))
133 (assert (map-p '((a . b) (c . d))))
134 (assert (map-p '(a b c d)))
135 (assert (map-p []))
136 (assert (map-p [1 2 3]))
137 (assert (map-p (make-hash-table)))
138 (assert (map-p "hello"))
139 (with-maps-do alist vec ht
140 (assert (map-p alist))
141 (assert (map-p vec))
142 (assert (map-p ht))
143 (assert (not (map-p 1)))
144 (assert (not (map-p 'hello)))))
145
146(ert-deftest test-map-keys ()
147 (with-maps-do alist vec ht
148 (assert (equal (map-keys alist) '(a b c)))
149 (assert (equal (map-keys vec) '(0 1 2)))
150 (assert (equal (map-keys ht) '(a b c)))))
151
152(ert-deftest test-map-values ()
153 (with-maps-do alist vec ht
154 (assert (equal (map-values alist) '(2 3 4)))
155 (assert (equal (map-values vec) '(a b c)))
156 (assert (equal (map-values ht) '(2 3 4)))))
157
158(ert-deftest test-map-pairs ()
159 (with-maps-do alist vec ht
160 (assert (equal (map-pairs alist) alist))
161 (assert (equal (map-pairs vec) '((0 . a)
162 (1 . b)
163 (2 . c))))
164 (assert (equal (map-pairs ht) alist))))
165
166(ert-deftest test-map-length ()
167 (let ((ht (make-hash-table)))
168 (puthash 'a 1 ht)
169 (puthash 'b 2 ht)
170 (puthash 'c 3 ht)
171 (puthash 'd 4 ht)
172 (assert (= 0 (map-length nil)))
173 (assert (= 0 (map-length [])))
174 (assert (= 0 (map-length (make-hash-table))))
175 (assert (= 5 (map-length [0 1 2 3 4])))
176 (assert (= 2 (map-length '((a . 1) (b . 2)))))
177 (assert (= 4 (map-length ht)))))
178
179(ert-deftest test-map-copy ()
180 (with-maps-do alist vec ht
181 (dolist (map (list alist vec ht))
182 (let ((copy (map-copy map)))
183 (assert (equal (map-keys map) (map-keys copy)))
184 (assert (equal (map-values map) (map-values copy)))
185 (assert (not (eq map copy)))))))
186
187(ert-deftest test-map-apply ()
188 (with-maps-do alist vec ht
189 (dolist (map (list alist ht))
190 (assert (equal (map-apply (lambda (k v) (cons (symbol-name k) v))
191 map)
192 '(("a" . 2) ("b" . 3) ("c" . 4)))))
193 (assert (equal (map-apply (lambda (k v) (cons (1+ k) v))
194 vec)
195 '((1 . a)
196 (2 . b)
197 (3 . c))))))
198
199(ert-deftest test-map-keys-apply ()
200 (with-maps-do alist vec ht
201 (dolist (map (list alist ht))
202 (assert (equal (map-keys-apply (lambda (k) (symbol-name k))
203 map)
204 '("a" "b" "c"))))
205 (assert (equal (map-keys-apply (lambda (k) (1+ k))
206 vec)
207 '(1 2 3)))))
208
209(ert-deftest test-map-values-apply ()
210 (with-maps-do alist vec ht
211 (dolist (map (list alist ht))
212 (assert (equal (map-values-apply (lambda (v) (1+ v))
213 map)
214 '(3 4 5))))
215 (assert (equal (map-values-apply (lambda (v) (symbol-name v))
216 vec)
217 '("a" "b" "c")))))
218
219(ert-deftest test-map-filter ()
220 (with-maps-do alist vec ht
221 (dolist (map (list alist ht))
222 (assert (equal (map-keys (map-filter (lambda (k v)
223 (<= 3 v))
224 map))
225 '(b c)))
226 (assert (null (map-filter (lambda (k v)
227 (eq 'd k))
228 map))))
229 (assert (null (map-filter (lambda (k v)
230 (eq 3 v))
231 [1 2 4 5])))
232 (assert (equal (map-filter (lambda (k v)
233 (eq 3 k))
234 [1 2 4 5])
235 '((3 . 5))))))
236
237(ert-deftest test-map-remove ()
238 (with-maps-do alist vec ht
239 (dolist (map (list alist ht))
240 (assert (equal (map-keys (map-remove (lambda (k v)
241 (<= 3 v))
242 map))
243 '(a)))
244 (assert (equal (map-keys (map-remove (lambda (k v)
245 (eq 'd k))
246 map))
247 (map-keys map))))
248 (assert (equal (map-remove (lambda (k v)
249 (eq 3 v))
250 [1 2 4 5])
251 '((0 . 1)
252 (1 . 2)
253 (2 . 4)
254 (3 . 5))))
255 (assert (null (map-remove (lambda (k v)
256 (>= k 0))
257 [1 2 4 5])))))
258
259(ert-deftest test-map-empty-p ()
260 (assert (map-empty-p nil))
261 (assert (not (map-empty-p '((a . b) (c . d)))))
262 (assert (map-empty-p []))
263 (assert (not (map-empty-p [1 2 3])))
264 (assert (map-empty-p (make-hash-table)))
265 (assert (not (map-empty-p "hello")))
266 (assert (map-empty-p "")))
267
268(ert-deftest test-map-contains-key-p ()
269 (assert (map-contains-key-p '((a . 1) (b . 2)) 'a))
270 (assert (not (map-contains-key-p '((a . 1) (b . 2)) 'c)))
271 (assert (map-contains-key-p '(("a" . 1)) "a"))
272 (assert (not (map-contains-key-p '(("a" . 1)) "a" #'eq)))
273 (assert (map-contains-key-p [a b c] 2))
274 (assert (not (map-contains-key-p [a b c] 3))))
275
276(ert-deftest test-map-some-p ()
277 (with-maps-do alist vec ht
278 (dolist (map (list alist ht))
279 (assert (equal (map-some-p (lambda (k v)
280 (eq 'a k))
281 map)
282 (cons 'a 2)))
283 (assert (not (map-some-p (lambda (k v)
284 (eq 'd k))
285 map))))
286 (assert (equal (map-some-p (lambda (k v)
287 (> k 1))
288 vec)
289 (cons 2 'c)))
290 (assert (not (map-some-p (lambda (k v)
291 (> k 3))
292 vec)))))
293
294(ert-deftest test-map-every-p ()
295 (with-maps-do alist vec ht
296 (dolist (map (list alist ht vec))
297 (assert (map-every-p (lambda (k v)
298 k)
299 map))
300 (assert (not (map-every-p (lambda (k v)
301 nil)
302 map))))
303 (assert (map-every-p (lambda (k v)
304 (>= k 0))
305 vec))
306 (assert (not (map-every-p (lambda (k v)
307 (> k 3))
308 vec)))))
309
310(ert-deftest test-map-into ()
311 (with-maps-do alist vec ht
312 (assert (hash-table-p (map-into alist 'hash-table)))
313 (assert (equal (map-into (map-into alist 'hash-table) 'list)
314 alist))
315 (assert (listp (map-into ht 'list)))
316 (assert (equal (map-keys (map-into (map-into ht 'list) 'hash-table))
317 (map-keys ht)))
318 (assert (equal (map-values (map-into (map-into ht 'list) 'hash-table))
319 (map-values ht)))
320 (assert (null (map-into nil 'list)))
321 (assert (map-empty-p (map-into nil 'hash-table)))))
322
323(provide 'map-tests)
324;;; map-tests.el ends here