about summary refs log tree commit diff
path: root/users/wpcarro/emacs/pkgs/list
diff options
context:
space:
mode:
Diffstat (limited to 'users/wpcarro/emacs/pkgs/list')
-rw-r--r--users/wpcarro/emacs/pkgs/list/README.md19
-rw-r--r--users/wpcarro/emacs/pkgs/list/default.nix26
-rw-r--r--users/wpcarro/emacs/pkgs/list/list.el219
-rw-r--r--users/wpcarro/emacs/pkgs/list/tests.el107
4 files changed, 371 insertions, 0 deletions
diff --git a/users/wpcarro/emacs/pkgs/list/README.md b/users/wpcarro/emacs/pkgs/list/README.md
new file mode 100644
index 0000000000..7afa8494fb
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/README.md
@@ -0,0 +1,19 @@
+# list.el
+
+Functions for working with lists in Elisp.
+
+## Wish List
+
+Here are some additional functions that I'd like to support.
+
+-  **TODO**: delete_at/2
+-  **TODO**: flatten/1
+-  **TODO**: flatten/2
+-  **TODO**: foldl/3
+-  **TODO**: foldr/3
+-  **TODO**: insert_at/3
+-  **TODO**: pop_at/3
+-  **TODO**: replace_at/3
+-  **TODO**: starts_with?/2
+-  **TODO**: update_at/3
+-  **TODO**: zip/1
diff --git a/users/wpcarro/emacs/pkgs/list/default.nix b/users/wpcarro/emacs/pkgs/list/default.nix
new file mode 100644
index 0000000000..1be0b901eb
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/default.nix
@@ -0,0 +1,26 @@
+{ pkgs, depot, ... }:
+
+let
+  list = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "list";
+        version = "1.0.0";
+        src = ./list.el;
+        packageRequires =
+          (with depot.users.wpcarro.emacs.pkgs; [
+            maybe
+            set
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ list ]);
+in
+list.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/list/list.el b/users/wpcarro/emacs/pkgs/list/list.el
new file mode 100644
index 0000000000..18be5f0a71
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/list.el
@@ -0,0 +1,219 @@
+;;; list.el --- Functions for working with lists -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24"))
+
+;;; Commentary:
+;; Since I prefer having the `list-' namespace, I wrote this module to wrap many
+;; of the functions that are defined in the the global namespace in Elisp.  I
+;; sometimes forget the names of these functions, so it's nice for them to be
+;; organized like this.
+;;
+;; Motivation:
+;; Here are some examples of function names where I prefer more modern
+;; alternatives:
+;; - `car': Return the first element (i.e. "head") of a linked list
+;; - `cdr': Return the tail of a linked list
+
+;; As are most APIs for standard libraries that I write, this is influenced by
+;; Elixir's standard library.
+;;
+;; Similar libraries:
+;; - dash.el: Excellent and widely adopted library for working with lists.
+;; - list-utils.el: Utility library that covers things that dash.el may not
+;;   cover.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'maybe)
+(require 'set)
+(require 'set)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list-new ()
+  "Return a new, empty list."
+  '())
+
+(defun list-concat (&rest lists)
+  "Joins `LISTS' into on list."
+  (apply #'append lists))
+
+(defun list-duplicate (n x)
+  "Duplicates the given element, X, N times in a list."
+  (list-map (lambda (_) x) (number-sequence 1 n)))
+
+(defun list-join (joint xs)
+  "Join a list of strings, XS, with JOINT."
+  (if (list-empty? xs)
+      ""
+    (list-reduce (list-first xs)
+                 (lambda (x acc)
+                   (format "%s%s%s" acc joint x))
+                 (list-tail xs))))
+
+(defun list-length (xs)
+  "Return the number of elements in `XS'."
+  (length xs))
+
+(defun list-get (i xs)
+  "Return the value in `XS' at `I', or nil."
+  (nth i xs))
+
+(defun list-first (xs &optional default)
+  "Alias for `list-head' for `XS'."
+  (if (list-empty? xs)
+      default
+    (car xs)))
+
+(defun list-last (xs &optional default)
+  "Returns the last element in XS or DEFAULT if empty."
+  (if (list-empty? xs)
+      default
+    (nth (- (length xs) 1) xs)))
+
+(defun list-tail (xs)
+  "Return the tail of `XS'."
+  (cdr xs))
+
+(defun list-reverse (xs)
+  "Reverses `XS'."
+  (reverse xs))
+
+(defun list-cons (x xs)
+  "Add `X' to the head of `XS'."
+  (cons x xs))
+
+(defun list-delete (x xs)
+  "Deletes the given element, X, from XS.
+Returns a new list without X. If X occurs more than once, only the first
+  occurrence is removed."
+  (let ((deleted? nil))
+    (list-reject (lambda (y)
+                   (if deleted? nil
+                     (when (equal x y)
+                       (setq deleted? t) t)))
+                 xs)))
+
+(defun list-filter (p xs)
+  "Return a subset of XS where predicate P returned t."
+  (list--assert-instance xs)
+  (seq-filter p xs))
+
+(defun list-map (f xs)
+  "Call `F' on each element of `XS'."
+  (list--assert-instance xs)
+  (seq-map f xs))
+
+(defun list-reduce (acc f xs)
+  "Return over `XS' calling `F' on an element in `XS'and `ACC'."
+  (list--assert-instance xs)
+  (seq-reduce (lambda (acc x) (funcall f x acc)) xs acc))
+
+(defun list-map-indexed (f xs)
+  "Call `F' on each element of `XS' along with its index."
+  (list-reverse
+   (cdr
+    (list-reduce '(0 . nil)
+                 (lambda (x acc)
+                   (let ((i (car acc))
+                         (result (cdr acc)))
+                     `(,(+ 1 i) . ,(cons (funcall f x i) result))))
+                 xs))))
+
+(defun list-reject (p xs)
+  "Return a subset of XS where predicate of P return nil."
+  (list-filter (lambda (x) (not (funcall p x))) xs))
+
+(defun list-find (p xs)
+  "Return the first x in XS that passes P or nil."
+  (list--assert-instance xs)
+  (seq-find p xs))
+
+(defun list-dedupe-adjacent (xs)
+  "Return XS without adjacent duplicates."
+  (list-reverse
+   (list-reduce (list (list-first xs))
+                (lambda (x acc)
+                  (if (equal x (list-first acc))
+                      acc
+                    (list-cons x acc)))
+                xs)))
+
+(defun list-chunk (n xs)
+  "Chunk XS into lists of size N."
+  (if (> n (length xs))
+      (list xs)
+    (let* ((xs (list-reduce '(:curr () :result ())
+                            (lambda (x acc)
+                              (let ((curr (plist-get acc :curr))
+                                    (result (plist-get acc :result)))
+                                (if (= (- n 1) (length curr))
+                                    `(:curr () :result ,(list-cons (list-reverse (list-cons x curr)) result))
+                                  `(:curr ,(list-cons x curr) :result
+                                          ,result)))) xs))
+           (curr (plist-get xs :curr))
+           (result (plist-get xs :result)))
+      (list-reverse (if curr (list-cons curr result) result)))))
+
+(defun list-wrap (xs)
+  "Wraps XS in a list if it is not a list already."
+  (if (list-instance? xs)
+      xs
+    (list xs)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list-instance? (xs)
+  "Return t if `XS' is a list.
+Be leery of using this with things like alists.  Many data structures in Elisp
+  are implemented using linked lists."
+  (listp xs))
+
+(defun list-empty? (xs)
+  "Return t if XS are empty."
+  (= 0 (list-length xs)))
+
+(defun list-all? (p xs)
+  "Return t if all `XS' pass the predicate, `P'."
+  (if (list-empty? xs)
+      t
+    (and (maybe-some? (funcall p (car xs)))
+         (list-all? p (cdr xs)))))
+
+(defun list-any? (p xs)
+  "Return t if any `XS' pass the predicate, `P'."
+  (if (list-empty? xs)
+      nil
+    (or (maybe-some? (funcall p (car xs)))
+        (list-any? p (cdr xs)))))
+
+(defun list-contains? (x xs)
+  "Return t if X is in XS using `equal'."
+  (list--assert-instance xs)
+  (maybe-some? (seq-contains-p xs x)))
+
+(defun list-xs-distinct-by? (f xs)
+  "Return t if all elements in XS are distinct after applying F to each."
+  (= (length xs)
+     (set-count (set-from-list (list-map f xs)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helpers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list--assert-instance (xs)
+  (unless (list-instance? xs)
+    (error (format "Assertion failed: argument is not a list: %s" xs))))
+
+(provide 'list)
+;;; list.el ends here
diff --git a/users/wpcarro/emacs/pkgs/list/tests.el b/users/wpcarro/emacs/pkgs/list/tests.el
new file mode 100644
index 0000000000..4b45796883
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/tests.el
@@ -0,0 +1,107 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'list)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(setq xs '(1 2 3 4 5))
+
+(ert-deftest list-length ()
+  (should (= 0 (list-length '())))
+  (should (= 5 (list-length xs))))
+
+(ert-deftest list-reduce ()
+  (should (= 16 (list-reduce 1 (lambda (x acc) (+ x acc)) xs))))
+
+(ert-deftest list-map ()
+  (should
+   (equal '(2 4 6 8 10)
+          (list-map (lambda (x) (* x 2)) xs))))
+
+(ert-deftest list-xs-distinct-by? ()
+  (should
+   (equal t (list-xs-distinct-by?
+             (lambda (x) (plist-get x :kbd))
+             '((:kbd "C-a" :name "foo")
+               (:kbd "C-b" :name "foo"))))))
+
+(ert-deftest list-dedupe-adjacent ()
+  (should (equal '(1 2 3 4 3 5)
+                 (list-dedupe-adjacent '(1 1 1 2 2 3 4 4 3 5 5)))))
+
+(ert-deftest list-contains? ()
+  ;; Assert returns t or nil
+  (should (equal t (list-contains? 1 xs)))
+  (should (equal nil (list-contains? 100 xs))))
+
+(ert-deftest list-join ()
+  (should (equal "foo-bar-baz"
+                 (list-join "-" '("foo" "bar" "baz")))))
+
+(ert-deftest list-chunk ()
+  (should (equal '((1 2 3 4 5 6))
+                 (list-chunk 7 '(1 2 3 4 5 6))))
+  (should (equal '((1) (2) (3) (4) (5) (6))
+                 (list-chunk 1 '(1 2 3 4 5 6))))
+  (should (equal '((1 2 3) (4 5 6))
+                 (list-chunk 3 '(1 2 3 4 5 6))))
+  (should (equal '((1 2) (3 4) (5 6))
+                 (list-chunk 2 '(1 2 3 4 5 6)))))
+
+(ert-deftest list-find ()
+  (should (equal 2 (list-find (lambda (x) (= 2 x)) '(1 2 3 4)))))
+
+(ert-deftest list-all? ()
+  (should (equal t (list-all? (lambda (x) (= 2 x)) nil)))
+  (should (null (list-all? (lambda (x) (= 2 x)) '(1 2 3))))
+  (should (equal t (list-all? (lambda (x) (= 2 x)) '(2 2 2 2)))))
+
+(ert-deftest list-any? ()
+  (should (null (list-any? (lambda (x) (= 2 x)) nil)))
+  (should (equal t (list-any? (lambda (x) (= 2 x)) '(1 2 3))))
+  (should (null (list-any? (lambda (x) (= 4 x)) '(1 2 3)))))
+
+(ert-deftest list-duplicate ()
+  (should (equal '() (list-duplicate 0 "hello")))
+  (should (equal '("hi") (list-duplicate 1 "hi")))
+  (should (equal '("bye" "bye") (list-duplicate 2 "bye")))
+  (should (equal '((1 2) (1 2) (1 2)) (list-duplicate 3 '(1 2)))))
+
+(ert-deftest list-first ()
+  (should (null (list-first '())))
+  (should (equal 1 (list-first '() 1)))
+  (should (equal 1 (list-first '(1))))
+  (should (equal 1 (list-first '(1) 2)))
+  (should (equal 1 (list-first '(1 2 3)))))
+
+(ert-deftest list-last ()
+  (should (null (list-last '())))
+  (should (equal 1 (list-last '() 1)))
+  (should (equal 1 (list-last '(1))))
+  (should (equal 1 (list-last '(1) 2)))
+  (should (equal 3 (list-last '(1 2 3)))))
+
+(ert-deftest list-wrap ()
+  (should (equal '("hello") (list-wrap "hello")))
+  (should (equal '(1 2 3) (list-wrap '(1 2 3))))
+  (should (equal '() (list-wrap nil))))
+
+(ert-deftest list-delete ()
+  (should (equal '(b c) (list-delete 'a '(a b c))))
+  (should (equal '(a b c) (list-delete 'd '(a b c))))
+  (should (equal '(a b c) (list-delete 'b '(a b b c))))
+  (should (equal '() (list-delete 'b '()))))
+
+(ert-deftest list-concat ()
+  (should (equal '(1 2 3 4 5) (list-concat '(1) '(2 3) '(4 5))))
+  (should (equal '(1 2 3) (list-concat '() '(1 2 3)))))
+
+;; TODO(wpcarro): Supoprt this.
+;; (ert-deftest list-zip ()
+;;   (should (equal '((1 3 5) (2 4 6)) (list-zip '(1 2) '(3 4) '(5 6))))
+;;   (should (equal '((1 3 5)) (list-zip '(1 2) '(3) '(5 6)))))