test/sequences.lisp @ edf43f3bf670

Add `reductions`
author Steve Losh <steve@stevelosh.com>
date Tue, 20 Feb 2024 08:21:39 -0500
parents 461876acdff5
children 322aefbbcb9f
(in-package :losh.test)

(defparameter *words* nil)

(defun words ()
  (when (null *words*)
    (setf *words* (gathering-vector ()
                    (do-file (line "/usr/share/dict/words")
                      (gather line)))))
  *words*)

(define-test make-sorting-predicate
  (flet ((check (original expected &rest preds)
           (let ((actual (sort (copy-seq original)
                               (apply #'make-sorting-predicate preds))))
             (is (equalp expected actual)))))
    (check '("zz" "yy" "abc")
           '("abc" "yy" "zz")
           #'string<)
    (check '("zz" "yy" "abc")
           '("yy" "zz" "abc")
           (cons #'< #'length)
           #'string<)
    (check '("yy" "zz" "abc")
           '("zz" "yy" "abc")
           (cons #'< #'length)
           #'string>)
    (check '("az" "by" "aby" "zzy")
           '("by" "aby" "zzy" "az")
           (lambda (x y)
             (char< (char x (1- (length x)))
                    (char y (1- (length y)))))
           (cons #'< #'length)
           #'string<)
    (check '("az" "by" "aby" "zzy")
           '("by" "aby" "zzy" "az")
           (cons #'char< (lambda (s) (char s (1- (length s)))))
           (cons #'< #'length)
           #'string<)))


(define-sorting-predicate sort-trivial<
  #'string<)

(define-sorting-predicate sort-short<
  (#'< :key #'length)
  #'string<)

(define-sorting-predicate sort-short>
  (#'< :key #'length)
  #'string>)

(define-sorting-predicate sort-last-char<
  (lambda (x y)
    (char< (char x (1- (length x)))
           (char y (1- (length y)))))
  (#'< :key #'length)
  #'string<)

(define-sorting-predicate sort-fancy<
  (#'char< :key (lambda (s) (char s (1- (length s)))))
  (#'< :key #'length)
  #'string<)

(define-sorting-predicate sort-fancy-quoted<
  ('char< :key (lambda (s) (char s (1- (length s)))))
  ('< :key #'length)
  'string<)

(define-test define-sorting-predicate
  (flet ((check (original expected pred)
           (let ((actual (sort (copy-seq original) pred)))
             (is (equalp expected actual)))))
    (check '("zz" "yy" "abc")
           '("abc" "yy" "zz")
           #'sort-trivial<)
    (check '("zz" "yy" "abc")
           '("yy" "zz" "abc")
           #'sort-short<)
    (check '("yy" "zz" "abc")
           '("zz" "yy" "abc")
           #'sort-short>)
    (check '("az" "by" "aby" "zzy")
           '("by" "aby" "zzy" "az")
           #'sort-last-char<)
    (check '("az" "by" "aby" "zzy")
           '("by" "aby" "zzy" "az")
           #'sort-fancy<)
    (check '("az" "by" "aby" "zzy")
           '("by" "aby" "zzy" "az")
           #'sort-fancy-quoted<)))

(defun sortedp (sequence predicate)
  ;; TODO Should this be a util of its own?
  (etypecase sequence
    (list (loop :for x = (pop sequence)
                :until (null sequence)
                :never (funcall predicate (first sequence) x)))
    (sequence (loop :with l = (length sequence)
                    :for x :from 0 :below l
                    :for y :from 1 :below l
                    :never (funcall predicate (elt sequence y) (elt sequence x))))))

(defun vowelp (char)
  (find (char-downcase char) "aeiou"))

(defun vowels< (a b)
  (< (count-if #'vowelp a) (count-if #'vowelp b)))

(defun random-elts (n sequence &key (result-type 'list))
  "Return `N` random elements from `sequence` (duplicates allowed).

  This wil not be fast if `sequence` is a list.

  "
  (ecase result-type
    (list (loop :repeat n :collect (random-elt sequence)))
    (vector (loop :with result = (make-array n)
                  :for i :from 0 :below n
                  :do (setf (aref result i) (random-elt sequence))
                  :finally (return result)))))


(define-test fuzz-sorting-predicates
  (let ((specs (vector 'string<
                       (cons '< 'length)
                       (cons 'string< 'reverse)
                       'vowels<
                       (cons '< 'sxhash)))
        (words (words)))
    (do-repeat 256
      (let* ((specs (random-elts (random-range 1 (+ 3 (length specs))) specs))
             (predicate (apply #'make-sorting-predicate specs))
             (seq (random-elts (random-range 0 100) words :result-type 'vector)))
        (setf seq (sort seq predicate))
        (is (sortedp seq predicate))))))


(define-test string-join
  (is (string= "" (string-join #\x '())))
  (is (string= "A" (string-join #\x '(a))))
  (is (string= "AxB" (string-join #\x '(a b))))
  (is (string= "AxBxC" (string-join #\x '(a b c))))
  (is (string= "A, B, C" (string-join ", " #(a b c))))
  (is (string= "foo" (string-join #\space '("foo"))))
  (is (string= "f o o" (string-join #\space "foo"))))

(define-test fuzz-string-join
  (let ((words (words)))
    (do-repeat 500
      (let* ((n (random-range 0 10))
             (ws (random-elts n words))
             (sep (random-elt #(#\, "" "," ", ")))
             (result (string-join sep ws)))
        (if (zerop n)
          (is (string= "" result))
          (is (= (+ (reduce #'+ ws :key #'length)
                    (* (1- n) (length (string sep))))
                 (length result))))))))

(define-test reductions/basic
  (is (equal '() (reductions #'+ '())))
  (is (equal '(1) (reductions #'+ '(1))))
  (is (equal '(1 3) (reductions #'+ '(1 2))))
  (is (equal '(1 3 6) (reductions #'+ '(1 2 3))))
  (is (equal '(100 101 103 106)
             (reductions #'+ '(1 2 3) :initial-value 100)))
  (is (equal '(nil (-3) (-2 -3) (-1 -2 -3))
             (reductions #'cons '(1 2 3)
                         :initial-value nil
                         :key #'-
                         :from-end t))))

(define-test reductions/initial-value
  (is (equal '(23) (reductions #'+ '() :initial-value 23)))
  (is (equal '(23 123) (reductions #'+ '(100) :initial-value 23)))
  (is (equal '(23 123 1123) (reductions #'+ '(100 1000) :initial-value 23))))

(define-test reductions/key
  ;; Key should be called on the contents.
  (is (equal '(-1 -3 -6)
             (reductions #'+ '(1 2 3)
                         :key #'-)))
  ;; Key should NOT be called on the initial value, if given.
  (is (equal '(100 101 103 106)
             (reductions #'+ '((1) (2) (3))
                         :initial-value 100
                         :key #'car))))

(define-test reductions/start-end
  (is (equal '(0 1 3 6 10 15) (reductions #'+ '(0 1 2 3 4 5) :start 0 :end nil)))
  (is (equal '(  1 3 6 10 15) (reductions #'+ '(0 1 2 3 4 5) :start 1 :end nil)))
  (is (equal '(0 1 3 6 10   ) (reductions #'+ '(0 1 2 3 4 5) :start 0 :end 5)))
  (is (equal '(    2 5  9   ) (reductions #'+ '(0 1 2 3 4 5) :start 2 :end 5)))
  (is (equal '(    2        ) (reductions #'+ '(0 1 2 3 4 5) :start 2 :end 3)))
  (is (equal '(             ) (reductions #'+ '(0 1 2 3 4 5) :start 2 :end 2)))
  (is (equal '(             ) (reductions #'+ '(0 1 2 3 4 5) :start 6 :end nil)))
  (is (equal '(    2 5  9   ) (reductions #'+ (mapcar #'list '(0 1 2 3 4 5))
                                          :start 2 :end 5 :key #'car))))

(define-test reductions/from-end
  (flet ((cat (a b) (concatenate 'string a b)))
    (is (equalp '("E" "DE" "CDE" "BCDE" "ABCDE")
                (reductions #'cat '(a b c d e) :from-end t
                            :key #'string)))
    (is (equalp '("" "E" "DE" "CDE" "BCDE" "ABCDE")
                (reductions #'cat '(a b c d e) :from-end t
                            :key #'string :initial-value "")))
    (is (equalp '("" "C" "BC")
                (reductions #'cat '(a b c d e) :from-end t
                            :key #'string :initial-value ""
                            :start 1 :end 3)))
    (is (equalp '("C" "BC")
                (reductions #'cat '(a b c d e) :from-end t
                            :key #'string
                            :start 1 :end 3)))
    (is (equalp '()
                (reductions #'cat '(a b c d e) :from-end t
                            :key #'string
                            :start 1 :end 1)))
    (is (equalp '("")
                (reductions #'cat '(a b c d e) :from-end t
                            :key #'string :initial-value ""
                            :start 1 :end 1)))))

(define-test reductions/non-list
  (is (equal '(1 3 6) (reductions #'+ (vector 1 2 3))))
  (is (equal '(99 100) (reductions #'+ (vector 1 2 3) :start 0 :end 1 :initial-value 99)))
  (is (equal '(99) (reductions #'+ (vector 1 2 3) :start 0 :end 0 :initial-value 99))))