diff options
Diffstat (limited to 'users/wpcarro/emacs/pkgs/list')
-rw-r--r-- | users/wpcarro/emacs/pkgs/list/README.md | 19 | ||||
-rw-r--r-- | users/wpcarro/emacs/pkgs/list/default.nix | 26 | ||||
-rw-r--r-- | users/wpcarro/emacs/pkgs/list/list.el | 219 | ||||
-rw-r--r-- | users/wpcarro/emacs/pkgs/list/tests.el | 107 |
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))))) |