From 84dd50995936b390f26b744611a07ca3f4356810 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sun, 1 Dec 2024 20:12:06 +0500 Subject: [PATCH] feat: add `cartesianProduct` function (#241) Co-authored-by: Marlon Passos <1marlonpassos@gmail.com> Co-authored-by: Alec Larson <1925840+aleclarson@users.noreply.github.com> --- benchmarks/array/cartesianProduct.bench.ts | 19 +++++++++ docs/array/cartesianProduct.mdx | 35 ++++++++++++++++ src/array/cartesianProduct.ts | 47 ++++++++++++++++++++++ src/mod.ts | 1 + tests/array/cartesianProduct.test-d.ts | 31 ++++++++++++++ tests/array/cartesianProduct.test.ts | 47 ++++++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 benchmarks/array/cartesianProduct.bench.ts create mode 100644 docs/array/cartesianProduct.mdx create mode 100644 src/array/cartesianProduct.ts create mode 100644 tests/array/cartesianProduct.test-d.ts create mode 100644 tests/array/cartesianProduct.test.ts diff --git a/benchmarks/array/cartesianProduct.bench.ts b/benchmarks/array/cartesianProduct.bench.ts new file mode 100644 index 00000000..37b36ec5 --- /dev/null +++ b/benchmarks/array/cartesianProduct.bench.ts @@ -0,0 +1,19 @@ +import * as _ from 'radashi' + +describe('cartesianProduct', () => { + bench('with an empty array (n=1)', () => { + _.cartesianProduct([]) + }) + + bench('with a non-empty array (n=1)', () => { + _.cartesianProduct(['a', 'b', 'c']) + }) + + bench('with two small arrays (n=2)', () => { + _.cartesianProduct(['red', 'blue'], ['fast', 'slow']) + }) + + bench('with three small arrays (n=3)', () => { + _.cartesianProduct(['red', 'blue'], ['fast', 'slow'], ['big', 'small']) + }) +}) diff --git a/docs/array/cartesianProduct.mdx b/docs/array/cartesianProduct.mdx new file mode 100644 index 00000000..75250464 --- /dev/null +++ b/docs/array/cartesianProduct.mdx @@ -0,0 +1,35 @@ +--- +title: cartesianProduct +description: Perform a Cartesian product of arrays +--- + +### Usage + +Create an [n-ary Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product#n-ary_Cartesian_product) from the given arrays. +The inputs are arrays, and the output is an array of arrays representing all +possible combinations where the first element is from the first array, the second element +is from the second array, and so on. + +```ts +import * as _ from 'radashi' + +const colors = ['red', 'blue'] +const numbers = [1, 2, 3] +const booleans = [true, false] + +_.cartesianProduct(colors, numbers, booleans) +// => [ +// ['red', 1, true], +// ['red', 1, false], +// ['red', 2, true], +// ['red', 2, false], +// ['red', 3, true], +// ['red', 3, false], +// ['blue', 1, true], +// ['blue', 1, false], +// ['blue', 2, true], +// ['blue', 2, false], +// ['blue', 3, true], +// ['blue', 3, false], +// ] +``` diff --git a/src/array/cartesianProduct.ts b/src/array/cartesianProduct.ts new file mode 100644 index 00000000..4a3b8bb8 --- /dev/null +++ b/src/array/cartesianProduct.ts @@ -0,0 +1,47 @@ +type ReadonlyArray2D = readonly (readonly T[])[] + +/** + * Create an [n-ary Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product#n-ary_Cartesian_product) + * from the given arrays. + * + * @see https://radashi.js.org/reference/array/cartesianProduct + * @example + * ```ts + * cartesianProduct([ + * ['red', 'blue'], + * ['big', 'small'], + * ['fast', 'slow'], + * ]) + * // [ + * // ['red', 'big', 'fast'], + * // ['red', 'big', 'slow'], + * // ['red', 'small', 'fast'], + * // ['red', 'small', 'slow'], + * // ['blue', 'big', 'fast'], + * // ['blue', 'big', 'slow'], + * // ['blue', 'small', 'fast'], + * // ['blue', 'small', 'slow'] + * // ] + * ``` + */ +export function cartesianProduct>( + ...arrays: [...T] +): Array<{ [K in keyof T]: T[K][number] }> + +export function cartesianProduct>( + ...arrays: T +): T[][] { + let out: T[][] = [[]] + for (const array of arrays) { + const result = [] + for (const currentArray of out) { + for (const item of array) { + const currentArrayCopy = currentArray.slice() + currentArrayCopy.push(item) + result.push(currentArrayCopy) + } + } + out = result + } + return out +} diff --git a/src/mod.ts b/src/mod.ts index ad247bf0..58366019 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -1,5 +1,6 @@ export * from './array/alphabetical.ts' export * from './array/boil.ts' +export * from './array/cartesianProduct.ts' export * from './array/castArray.ts' export * from './array/castArrayIfExists.ts' export * from './array/cluster.ts' diff --git a/tests/array/cartesianProduct.test-d.ts b/tests/array/cartesianProduct.test-d.ts new file mode 100644 index 00000000..b31e1d20 --- /dev/null +++ b/tests/array/cartesianProduct.test-d.ts @@ -0,0 +1,31 @@ +import * as _ from 'radashi' + +describe('cartesianProduct return type', () => { + test('with an empty array', () => { + const result = _.cartesianProduct([]) + expectTypeOf(result).toEqualTypeOf<[never][]>() + }) + test('with two arrays', () => { + const result = _.cartesianProduct(['red', 'blue'], [1, 2]) + const [[v1, v2]] = result + expectTypeOf(result).toEqualTypeOf<['red' | 'blue', 1 | 2][]>() + expectTypeOf(v1).toEqualTypeOf<'red' | 'blue'>() + expectTypeOf(v2).toEqualTypeOf<1 | 2>() + }) + test('with three arrays', () => { + const result = _.cartesianProduct(['red', 'blue'], [1, 2], [true, false]) + const [[v1, v2, v3]] = result + expectTypeOf(result).toEqualTypeOf<['red' | 'blue', 1 | 2, boolean][]>() + expectTypeOf(v1).toEqualTypeOf<'red' | 'blue'>() + expectTypeOf(v2).toEqualTypeOf<1 | 2>() + expectTypeOf(v3).toEqualTypeOf() + }) + test('with readonly arrays', () => { + const colors = ['red', 'blue'] as const + const sizes = [1, 2] as const + const result = _.cartesianProduct(colors, sizes) + expectTypeOf(result).toEqualTypeOf< + [(typeof colors)[number], (typeof sizes)[number]][] + >() + }) +}) diff --git a/tests/array/cartesianProduct.test.ts b/tests/array/cartesianProduct.test.ts new file mode 100644 index 00000000..10c0d655 --- /dev/null +++ b/tests/array/cartesianProduct.test.ts @@ -0,0 +1,47 @@ +import * as _ from 'radashi' + +describe('cartesianProduct', () => { + test('returns an array containing an empty array when given an empty input (n=0)', () => { + expect(_.cartesianProduct()).toEqual([[]]) + }) + test('returns an empty array when given an empty array (n=1)', () => { + expect(_.cartesianProduct([])).toEqual([]) + }) + test('returns an empty array when given multiple empty arrays (n>1)', () => { + expect(_.cartesianProduct([], [], [])).toEqual([]) + }) + test('returns an empty array when one of the arrays in the input is empty (n>1)', () => { + expect(_.cartesianProduct(['1', '2', '3'], [])).toEqual([]) + }) + test('returns an array of singletons when given a single array (n=1)', () => { + expect(_.cartesianProduct(['1', '2', '3'])).toEqual([['1'], ['2'], ['3']]) + }) + test('performs a correct Cartesian cartesianProduct for two arrays (n=2)', () => { + expect(_.cartesianProduct(['red', 'blue'], [1, 2, 3])).toEqual([ + ['red', 1], + ['red', 2], + ['red', 3], + ['blue', 1], + ['blue', 2], + ['blue', 3], + ]) + }) + test('performs a correct Cartesian cartesianProduct for more than two arrays (n>2)', () => { + expect( + _.cartesianProduct(['red', 'blue'], [1, 2, 3], [true, false]), + ).toEqual([ + ['red', 1, true], + ['red', 1, false], + ['red', 2, true], + ['red', 2, false], + ['red', 3, true], + ['red', 3, false], + ['blue', 1, true], + ['blue', 1, false], + ['blue', 2, true], + ['blue', 2, false], + ['blue', 3, true], + ['blue', 3, false], + ]) + }) +})