diff --git a/collections/deno.json b/collections/deno.json index b2517dff7..1dd139cdb 100644 --- a/collections/deno.json +++ b/collections/deno.json @@ -51,6 +51,7 @@ "./unstable-drop-last-while": "./unstable_drop_last_while.ts", "./unstable-intersect": "./unstable_intersect.ts", "./unstable-sample": "./unstable_sample.ts", + "./unstable-sliding-windows": "./unstable_sliding_windows.ts", "./unstable-sort-by": "./unstable_sort_by.ts", "./unstable-take-last-while": "./unstable_take_last_while.ts", "./unstable-take-while": "./unstable_take_while.ts", diff --git a/collections/unstable_sliding_windows.ts b/collections/unstable_sliding_windows.ts new file mode 100644 index 000000000..26d788965 --- /dev/null +++ b/collections/unstable_sliding_windows.ts @@ -0,0 +1,101 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. + +/** Options for {@linkcode slidingWindows}. */ +export interface SlidingWindowsOptions { + /** + * If step is set, each window will start that many elements after the last + * window's start. + * + * @default {1} + */ + step?: number; + /** + * If partial is set, windows will be generated for the last elements of the + * collection, resulting in some undefined values if size is greater than 1. + * + * @default {false} + */ + partial?: boolean; +} + +/** + * Generates sliding views of the given iterable of the given size and returns an + * array containing all of them. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * If step is set, each window will start that many elements after the last + * window's start. (Default: 1) + * + * If partial is set, windows will be generated for the last elements of the + * collection, resulting in some undefined values if size is greater than 1. + * + * @typeParam T The type of the array elements. + * + * @param iterable The iterable to generate sliding windows from. + * @param size The size of the sliding windows. + * @param options The options for generating sliding windows. + * + * @returns An array containing all sliding windows of the given size. + * + * @example Usage + * ```ts + * import { slidingWindows } from "@std/collections/unstable-sliding-windows"; + * import { assertEquals } from "@std/assert"; + * const numbers = [1, 2, 3, 4, 5]; + * + * const windows = slidingWindows(numbers, 3); + * assertEquals(windows, [ + * [1, 2, 3], + * [2, 3, 4], + * [3, 4, 5], + * ]); + * + * const windowsWithStep = slidingWindows(numbers, 3, { step: 2 }); + * assertEquals(windowsWithStep, [ + * [1, 2, 3], + * [3, 4, 5], + * ]); + * + * const windowsWithPartial = slidingWindows(numbers, 3, { partial: true }); + * assertEquals(windowsWithPartial, [ + * [1, 2, 3], + * [2, 3, 4], + * [3, 4, 5], + * [4, 5], + * [5], + * ]); + * ``` + */ +export function slidingWindows( + iterable: Iterable, + size: number, + options: SlidingWindowsOptions = {}, +): T[][] { + const { step = 1, partial = false } = options; + if (!Number.isInteger(size) || size <= 0) { + throw new RangeError( + `Cannot create sliding windows: size must be a positive integer, current value is ${size}`, + ); + } + if (!Number.isInteger(step) || step <= 0) { + throw new RangeError( + `Cannot create sliding windows: step must be a positive integer, current value is ${step}`, + ); + } + const array = Array.isArray(iterable) ? iterable : Array.from(iterable); + const len = array.length; + const result: T[][] = []; + for (let i = 0; i <= len; i += step) { + let last = i + size; + if (last > len) { + last = len; + } + const window: T[] = array.slice(i, last); + if ((partial && window.length) || window.length === size) { + result.push(window); + } + } + return result; +} diff --git a/collections/unstable_sliding_windows_test.ts b/collections/unstable_sliding_windows_test.ts new file mode 100644 index 000000000..3ad75a3c0 --- /dev/null +++ b/collections/unstable_sliding_windows_test.ts @@ -0,0 +1,335 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals, assertThrows } from "@std/assert"; +import { slidingWindows } from "./unstable_sliding_windows.ts"; + +function slidingWindowsTest( + input: Parameters, + expected: T[][], + message?: string, +) { + const actual = slidingWindows(...input); + assertEquals(actual, expected, message); +} + +function slidingWindowsThrowsTest( + input: [ + collection: T[], + size: number, + config?: { step?: number; partial?: boolean }, + ], + ErrorClass: ErrorConstructor, + msgIncludes?: string, + msg?: string | undefined, +) { + assertThrows( + () => { + slidingWindows(...input); + }, + ErrorClass, + msgIncludes, + msg, + ); +} + +Deno.test({ + name: "slidingWindows() handles no mutation", + fn() { + const numbers = [1, 2, 3, 4, 5]; + slidingWindows(numbers, 3); + assertEquals(numbers, [1, 2, 3, 4, 5]); + }, +}); + +Deno.test({ + name: "slidingWindows() handles empty input", + fn() { + slidingWindowsTest([[], 3], []); + slidingWindowsTest([[], 3, {}], []); + slidingWindowsTest([[], 3, { step: 2 }], []); + slidingWindowsTest([[], 3, { partial: true }], []); + slidingWindowsTest([[], 3, { step: 2, partial: true }], []); + }, +}); + +Deno.test({ + name: "slidingWindows() handles default option", + fn() { + slidingWindowsTest([[1, 2, 3, 4, 5], 5], [[1, 2, 3, 4, 5]]); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 3], + [ + [1, 2, 3], + [2, 3, 4], + [3, 4, 5], + ], + ); + slidingWindowsTest([[1, 2, 3, 4, 5], 1], [[1], [2], [3], [4], [5]]); + }, +}); + +Deno.test({ + name: "slidingWindows() handles step option", + fn() { + slidingWindowsTest([[1, 2, 3, 4, 5], 5, { step: 2 }], [[1, 2, 3, 4, 5]]); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 3, { step: 2 }], + [ + [1, 2, 3], + [3, 4, 5], + ], + ); + slidingWindowsTest([[1, 2, 3, 4, 5], 1, { step: 2 }], [[1], [3], [5]]); + }, +}); + +Deno.test({ + name: "slidingWindows() handles partial option", + fn() { + slidingWindowsTest( + [[1, 2, 3, 4, 5], 5, { partial: true }], + [[1, 2, 3, 4, 5], [2, 3, 4, 5], [3, 4, 5], [4, 5], [5]], + ); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 3, { partial: true }], + [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5], [5]], + ); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 1, { partial: true }], + [[1], [2], [3], [4], [5]], + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles step and partial option", + fn() { + slidingWindowsTest( + [[1, 2, 3, 4, 5], 5, { step: 2, partial: true }], + [[1, 2, 3, 4, 5], [3, 4, 5], [5]], + ); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 3, { step: 2, partial: true }], + [[1, 2, 3], [3, 4, 5], [5]], + ); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 1, { step: 2, partial: true }], + [[1], [3], [5]], + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles invalid size or step: other than number", + fn() { + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], NaN], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is NaN", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: NaN }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is NaN", + ); + slidingWindowsThrowsTest( + // @ts-ignore: for test + [[1, 2, 3, 4, 5], "invalid"], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is invalid", + ); + slidingWindowsThrowsTest( + // @ts-ignore: for test + [[1, 2, 3, 4, 5], 3, { step: "invalid" }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is invalid", + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles invalid size or step: not integer number", + fn() { + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 0.5], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is 0.5", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: 0.5 }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is 0.5", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 1.5], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is 1.5", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: 1.5 }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is 1.5", + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles invalid size or step: not positive number", + fn() { + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 0], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is 0", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: 0 }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is 0", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], -1], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is -1", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: -1 }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is -1", + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles invalid size or step: infinity", + fn() { + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], Number.NEGATIVE_INFINITY], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is -Infinity", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: Number.NEGATIVE_INFINITY }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is -Infinity", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], Number.POSITIVE_INFINITY], + RangeError, + "Cannot create sliding windows: size must be a positive integer, current value is Infinity", + ); + slidingWindowsThrowsTest( + [[1, 2, 3, 4, 5], 3, { step: Number.POSITIVE_INFINITY }], + RangeError, + "Cannot create sliding windows: step must be a positive integer, current value is Infinity", + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles large size", + fn() { + slidingWindowsTest([[1, 2, 3, 4, 5], 100], []); + slidingWindowsTest([[1, 2, 3, 4, 5], 100, { step: 2 }], []); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 100, { step: 2, partial: true }], + [[1, 2, 3, 4, 5], [3, 4, 5], [5]], + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles large step", + fn() { + slidingWindowsTest([[1, 2, 3, 4, 5], 3, { step: 100 }], [[1, 2, 3]]); + slidingWindowsTest( + [[1, 2, 3, 4, 5], 3, { step: 100, partial: true }], + [[1, 2, 3]], + ); + }, +}); + +Deno.test({ + name: "slidingWindows() handles empty Array", + fn() { + slidingWindowsTest([Array(5), 5], [ + Array(5), + ]); + slidingWindowsTest([Array(5), 3], [ + Array(3), + Array(3), + Array(3), + ]); + slidingWindowsTest([Array(5), 1], [ + Array(1), + Array(1), + Array(1), + Array(1), + Array(1), + ]); + }, +}); + +Deno.test("slidingWindows() handles a generator", () => { + function* gen() { + yield 1; + yield 2; + yield 3; + yield 4; + yield 5; + } + function* emptyGen() {} + slidingWindowsTest([gen(), 5], [[1, 2, 3, 4, 5]]); + slidingWindowsTest([gen(), 3], [[1, 2, 3], [2, 3, 4], [3, 4, 5]]); + slidingWindowsTest([gen(), 1], [[1], [2], [3], [4], [5]]); + slidingWindowsTest([gen(), 3, { partial: true }], [ + [1, 2, 3], + [2, 3, 4], + [3, 4, 5], + [4, 5], + [5], + ]); + slidingWindowsTest([gen(), 3, { step: 2 }], [[1, 2, 3], [3, 4, 5]]); + slidingWindowsTest([gen(), 1, { step: 2, partial: true }], [[1], [3], [5]]); + + slidingWindowsTest([emptyGen(), 3], []); +}); + +Deno.test("slidingWindows() handles a string", () => { + const str = "12345"; + slidingWindowsTest([str, 5], [["1", "2", "3", "4", "5"]]); + slidingWindowsTest([str, 3], [["1", "2", "3"], ["2", "3", "4"], [ + "3", + "4", + "5", + ]]); + slidingWindowsTest([str, 1], [["1"], ["2"], ["3"], ["4"], ["5"]]); +}); + +Deno.test("slidingWindows() handles a Set", () => { + const set = new Set([1, 2, 3, 4, 5]); + slidingWindowsTest([set, 5], [[1, 2, 3, 4, 5]]); + slidingWindowsTest([set, 3], [[1, 2, 3], [2, 3, 4], [3, 4, 5]]); + slidingWindowsTest([set, 1], [[1], [2], [3], [4], [5]]); +}); + +Deno.test("slidingWindows() handles a Map", () => { + const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ["d", 4], + ["e", 5], + ]); + slidingWindowsTest([map, 3], [ + [["a", 1], ["b", 2], ["c", 3]], + [["b", 2], ["c", 3], ["d", 4]], + [["c", 3], ["d", 4], ["e", 5]], + ]); + slidingWindowsTest([map, 1], [ + [["a", 1]], + [["b", 2]], + [["c", 3]], + [["d", 4]], + [["e", 5]], + ]); +});