diff --git a/documentation/source/sequence-utilities.rst b/documentation/source/sequence-utilities.rst index 44f48c6..38e954b 100644 --- a/documentation/source/sequence-utilities.rst +++ b/documentation/source/sequence-utilities.rst @@ -9,6 +9,10 @@ Overview The sequence-utilities module implements some useful methods on sequences. +- :func:`zip` +- :func:`zip-all` +- :func:`zip-with` + Reference --------- @@ -363,3 +367,148 @@ far. https://github.com/dylan-lang/collection-extensions/issues/2 :parameter list: An instance of :drm:``. :parameter elt: An instance of :drm:``. :value new-list: An instance of :drm:``. + +zip +^^^ + +.. function:: zip + + The `zip` function combines multiple iterables element-wise into + tuples, stopping at the shortest iterable. + + :signature: + + zip (*sequence1* *sequence2* #key *key1* *key2*) => (*zipped*) + + :parameter sequence1: + + An instance of :drm:``. + + :parameter sequence2: + + An instance of :drm:``. + + :parameter #key key1: + + An instance of :drm:``. Default value: + :drm:`identity`. + + :parameter #key key2: + + An instance of :drm:``. Default value: + :drm:`identity`. + + :value zipped: + + An instance of :drm:``. + + :description: + + The `zip` function is often classified as a *high-order function* and + a *sequence transformation function*. + + The function operates on two sequences (like :drm:`range`, + `array`, :drm:`list`, `deque` or `string`) and + transforms them in a :drm:`` of pairs, each element from + one of the sequences. + + If *key1* or *key2* are not provided, they default to the function + :drm:`identity` returning the object. If provided, this function + transforms the element before collecting it. + + If the sequences provided have different lengths, func:`zip` + automatically truncates the output to the length of the shorter + sequence. + + + :example: + + .. code-block:: dylan + + let a = #(1, 2); + let b = #('a', 'b'); + + let zipped = zip(a, b); + format-out("%=", zipped); // Output: #(#(1, 'a'), #(2, 'b')) + + :example: + + .. code-block:: dylan + + let a = #('a', 'b'); + let b = #(3, 4); + + let zipped = zip(a, b, key2: odd?); + format-out("%=", zipped); // Output: #(#('a', #t), ('b', #f)) + +zip-with +^^^^^^^^ + +.. function:: zip-with + + + :signature: + + zip-with (function collection #rest more-collections) ⇒ new-collection + + :parameter function: + + An instance of :drm:``. + + :parameter collection: + + An instance of :drm:``. + + :parameter more-collections: + + An instance of :drm:``. + + :value new-collection: + + An instance of :drm:``. + + :example: + + .. code-block:: dylan + + let a = #(100, 200, 300); + let b = #(1, 2, 3); + + let zipped = zip-with(\+, a, b); + format-out("%=", zipped); // Output: #(101, 202, 303) + + :seealso: + + :drm:`map` + +zip-all +^^^^^^^ + +.. function:: zip-all + + A :func:`zip` function that can take any number of iterables as + parameters. + + :signature: + + zip-all (sequences) => (zipped) + + :parameter sequences: + + An instance of :drm:``. + + :value zipped: + + An instance of :drm:``. + + :example: + + .. code-block:: dylan + + let a = #(1, 2, 3); + let b = #('a', 'b', 'c'); + let c = #(#t, #f, #t); + + let zipped = zip-all(a, b, c); + + format-out("%=", zipped); // Output: #(#(1, 'a', #t), #(2, 'b', #f), #(3, 'c', #t)) diff --git a/library.dylan b/library.dylan index c42ada9..0eb7f3c 100644 --- a/library.dylan +++ b/library.dylan @@ -38,6 +38,7 @@ module: dylan-user define library collection-extensions use dylan; use common-dylan, import: { byte-vector }; + use collections; export heap, self-organizing-list, vector-search, subseq, sequence-diff; export sde-vector; export collection-utilities; @@ -91,6 +92,7 @@ end module collection-utilities; define module sequence-utilities use dylan; + use collectors; export push!, pop!; export pair?, null?, list?; export xpair, tabulate, list*, take, drop, last-pair; @@ -100,5 +102,6 @@ define module sequence-utilities export concatenate-map, pair-do, choose-map; export partition, assoc, apair, alist-copy, alist-delete; export satisfies, index, find, find-tail, precedes?; + export zip, zip-with, zip-all; end module sequence-utilities; diff --git a/sequence-utils.dylan b/sequence-utils.dylan index 661ab4b..20459f2 100644 --- a/sequence-utils.dylan +++ b/sequence-utils.dylan @@ -588,3 +588,29 @@ define method precedes?(elt-1, elt-2, seq :: , not-found; end block; end method precedes?; + +define function zip + (seq1 :: , seq2 :: , #key key1 = identity, key2 = identity) + => (zipped :: ) + collecting (as ) + for (e1 in seq1, e2 in seq2) + collect(list(key1(e1), key2(e2))) + end + end; +end; + +define constant zip-with + = map; + +define function zip-all + (#rest sequences) => (zipped :: ) + local method recur (seqs, zipped :: ) + if (any?(empty?, seqs)) + reverse(zipped) + else + recur(map(tail, seqs), + add(zipped, map(head, seqs))) + end + end; + if (empty?(sequences)) #() else recur(sequences, #()) end +end; diff --git a/tests/collection-extensions-test-suite.dylan b/tests/collection-extensions-test-suite.dylan index feb7fa5..4371e19 100644 --- a/tests/collection-extensions-test-suite.dylan +++ b/tests/collection-extensions-test-suite.dylan @@ -37,5 +37,6 @@ define suite collection-extensions-test-suite suite sequence-diff-suite; suite collection-utilities-suite; suite sequence-utilities-suite; + suite zip-suite; end suite collection-extensions-test-suite; diff --git a/tests/sequence-utils-suite.dylan b/tests/sequence-utils-suite.dylan index f750346..8871acd 100644 --- a/tests/sequence-utils-suite.dylan +++ b/tests/sequence-utils-suite.dylan @@ -46,3 +46,68 @@ define suite sequence-utilities-suite test push-test; test pop-test; end suite sequence-utilities-suite; + +define test test-zip-all-empty-values + (description: "Testing zip-all with no arguments returns an empty list") + expect-equal(#(), zip-all()) +end test; + +define test test-zip-empty-sequences + (description: "Zip with empty sequences returns an empty list") + expect-equal(#(), zip(#(), #())) +end test; + +define test test-zip-single-element + (description: "Test zip with single element sequences") + expect-equal(#(#(10, 20)), zip(#(10), #(20))) +end test; + +define test test-zip-two-sequences-equal-length + (description: "Test zip with two sequences of equal length") + expect-equal(#(#(1, 'a'), #(2, 'b'), #(3, 'c')), + zip(#(1, 2, 3), #('a', 'b', 'c'))) +end test; + +define test test-zip-with-keys + (description: "Test zip passing key functons") + expect-equal(#(#(#f, #t), #(#t, #f)), + zip(#(1, 2), #(3, 4), + key1: even?, key2: odd?)) +end; + +define test test-zip-two-sequences-unequal-length + (description: "Test zip with two sequences of unequal length") + let expected = #(#(1, 'a'), #(2, 'b')); + expect-equal(expected, zip(#(1, 2), #('a', 'b', 'c'))); + expect-equal(expected, zip(#(1, 2, 3), #('a', 'b'))); +end test; + +define test test-zip-with + (description: "Test zip with a math function") + expect-equal(#(7, 10, 13, 16), + zip-with(method (x, y) 2 * x + y end, + range(from: 1, to: 4), + range(from: 5, to: 8))) +end test; + +define test test-zip-all + (description: "Testing zip with more than two sequences") + expect-equal(#(#(1, 'x', #t), #(2, 'y', #f)), + zip-all(#(1, 2, 3), #('x', 'y', 'z'), #(#t, #f)), + "Testing zip with three sequences of unequal length"); + expect-equal(#(#(1, 'x', #t), #(2, 'y', #f)), + zip-all(#(1, 2), #('x', 'y', 'z'), #(#t, #f)), + "Testing zip with three sequences of unequal length"); +end test; + + +define suite zip-suite + (description: "Test suite for zip function") + test test-zip-all-empty-values; + test test-zip-empty-sequences; + test test-zip-single-element; + test test-zip-two-sequences-equal-length; + test test-zip-two-sequences-unequal-length; + test test-zip-with; + test test-zip-all; +end suite;