464 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			464 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| Copyright 2024 New Vector Ltd.
 | |
| Copyright 2021 The Matrix.org Foundation C.I.C.
 | |
| 
 | |
| SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 | |
| Please see LICENSE files in the repository root for full details.
 | |
| */
 | |
| 
 | |
| import {
 | |
|     arrayDiff,
 | |
|     arrayFastClone,
 | |
|     arrayFastResample,
 | |
|     arrayHasDiff,
 | |
|     arrayHasOrderChange,
 | |
|     arrayUnion,
 | |
|     arrayRescale,
 | |
|     arraySeed,
 | |
|     arraySmoothingResample,
 | |
|     arrayTrimFill,
 | |
|     arrayIntersection,
 | |
|     ArrayUtil,
 | |
|     GroupedArray,
 | |
|     concat,
 | |
|     asyncEvery,
 | |
|     asyncSome,
 | |
| } from "../../src/utils/arrays";
 | |
| 
 | |
| type TestParams = { input: number[]; output: number[] };
 | |
| type TestCase = [string, TestParams];
 | |
| 
 | |
| function expectSample(input: number[], expected: number[], smooth = false) {
 | |
|     const result = (smooth ? arraySmoothingResample : arrayFastResample)(input, expected.length);
 | |
|     expect(result).toBeDefined();
 | |
|     expect(result).toHaveLength(expected.length);
 | |
|     expect(result).toEqual(expected);
 | |
| }
 | |
| 
 | |
| describe("arrays", () => {
 | |
|     describe("arrayFastResample", () => {
 | |
|         const downsampleCases: TestCase[] = [
 | |
|             ["Odd -> Even", { input: [1, 2, 3, 4, 5], output: [1, 4] }],
 | |
|             ["Odd -> Odd", { input: [1, 2, 3, 4, 5], output: [1, 3, 5] }],
 | |
|             ["Even -> Odd", { input: [1, 2, 3, 4], output: [1, 2, 3] }],
 | |
|             ["Even -> Even", { input: [1, 2, 3, 4], output: [1, 3] }],
 | |
|         ];
 | |
|         it.each(downsampleCases)("downsamples correctly from %s", (_d, { input, output }) =>
 | |
|             expectSample(input, output),
 | |
|         );
 | |
| 
 | |
|         const upsampleCases: TestCase[] = [
 | |
|             ["Odd -> Even", { input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3] }],
 | |
|             ["Odd -> Odd", { input: [1, 2, 3], output: [1, 1, 2, 2, 3] }],
 | |
|             ["Even -> Odd", { input: [1, 2], output: [1, 1, 1, 2, 2] }],
 | |
|             ["Even -> Even", { input: [1, 2], output: [1, 1, 1, 2, 2, 2] }],
 | |
|         ];
 | |
|         it.each(upsampleCases)("upsamples correctly from %s", (_d, { input, output }) => expectSample(input, output));
 | |
| 
 | |
|         const maintainSampleCases: TestCase[] = [
 | |
|             ["Odd", { input: [1, 2, 3], output: [1, 2, 3] }], // Odd
 | |
|             ["Even", { input: [1, 2], output: [1, 2] }], // Even
 | |
|         ];
 | |
| 
 | |
|         it.each(maintainSampleCases)("maintains samples for %s", (_d, { input, output }) =>
 | |
|             expectSample(input, output),
 | |
|         );
 | |
|     });
 | |
| 
 | |
|     describe("arraySmoothingResample", () => {
 | |
|         // Dev note: these aren't great samples, but they demonstrate the bare minimum. Ideally
 | |
|         // we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
 | |
|         // but that's not really feasible to manually verify accuracy.
 | |
|         const downsampleCases: TestCase[] = [
 | |
|             ["Odd -> Even", { input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3] }],
 | |
|             ["Odd -> Odd", { input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3] }],
 | |
|             ["Even -> Odd", { input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3] }],
 | |
|             ["Even -> Even", { input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3] }],
 | |
|         ];
 | |
| 
 | |
|         it.each(downsampleCases)("downsamples correctly from %s", (_d, { input, output }) =>
 | |
|             expectSample(input, output, true),
 | |
|         );
 | |
| 
 | |
|         const upsampleCases: TestCase[] = [
 | |
|             ["Odd -> Even", { input: [2, 0, 2], output: [2, 2, 0, 0, 2, 2] }],
 | |
|             ["Odd -> Odd", { input: [2, 0, 2], output: [2, 2, 0, 0, 2] }],
 | |
|             ["Even -> Odd", { input: [2, 0], output: [2, 2, 2, 0, 0] }],
 | |
|             ["Even -> Even", { input: [2, 0], output: [2, 2, 2, 0, 0, 0] }],
 | |
|         ];
 | |
|         it.each(upsampleCases)("upsamples correctly from %s", (_d, { input, output }) =>
 | |
|             expectSample(input, output, true),
 | |
|         );
 | |
| 
 | |
|         const maintainCases: TestCase[] = [
 | |
|             ["Odd", { input: [2, 0, 2], output: [2, 0, 2] }],
 | |
|             ["Even", { input: [2, 0], output: [2, 0] }],
 | |
|         ];
 | |
|         it.each(maintainCases)("maintains samples for %s", (_d, { input, output }) => expectSample(input, output));
 | |
|     });
 | |
| 
 | |
|     describe("arrayRescale", () => {
 | |
|         it("should rescale", () => {
 | |
|             const input = [8, 9, 1, 0, 2, 7, 10];
 | |
|             const output = [80, 90, 10, 0, 20, 70, 100];
 | |
|             const result = arrayRescale(input, 0, 100);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(output.length);
 | |
|             expect(result).toEqual(output);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayTrimFill", () => {
 | |
|         it("should shrink arrays", () => {
 | |
|             const input = [1, 2, 3];
 | |
|             const output = [1, 2];
 | |
|             const seed = [4, 5, 6];
 | |
|             const result = arrayTrimFill(input, output.length, seed);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(output.length);
 | |
|             expect(result).toEqual(output);
 | |
|         });
 | |
| 
 | |
|         it("should expand arrays", () => {
 | |
|             const input = [1, 2, 3];
 | |
|             const output = [1, 2, 3, 4, 5];
 | |
|             const seed = [4, 5, 6];
 | |
|             const result = arrayTrimFill(input, output.length, seed);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(output.length);
 | |
|             expect(result).toEqual(output);
 | |
|         });
 | |
| 
 | |
|         it("should keep arrays the same", () => {
 | |
|             const input = [1, 2, 3];
 | |
|             const output = [1, 2, 3];
 | |
|             const seed = [4, 5, 6];
 | |
|             const result = arrayTrimFill(input, output.length, seed);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(output.length);
 | |
|             expect(result).toEqual(output);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arraySeed", () => {
 | |
|         it("should create an array of given length", () => {
 | |
|             const val = 1;
 | |
|             const output = [val, val, val];
 | |
|             const result = arraySeed(val, output.length);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(output.length);
 | |
|             expect(result).toEqual(output);
 | |
|         });
 | |
|         it("should maintain pointers", () => {
 | |
|             const val = {}; // this works because `{} !== {}`, which is what toEqual checks
 | |
|             const output = [val, val, val];
 | |
|             const result = arraySeed(val, output.length);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(output.length);
 | |
|             expect(result).toEqual(output);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayFastClone", () => {
 | |
|         it("should break pointer reference on source array", () => {
 | |
|             const val = {}; // we'll test to make sure the values maintain pointers too
 | |
|             const input = [val, val, val];
 | |
|             const result = arrayFastClone(input);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(input.length);
 | |
|             expect(result).toEqual(input); // we want the array contents to match...
 | |
|             expect(result).not.toBe(input); // ... but be a different reference
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayHasOrderChange", () => {
 | |
|         it("should flag true on B ordering difference", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [3, 2, 1];
 | |
|             const result = arrayHasOrderChange(a, b);
 | |
|             expect(result).toBe(true);
 | |
|         });
 | |
| 
 | |
|         it("should flag false on no ordering difference", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 3];
 | |
|             const result = arrayHasOrderChange(a, b);
 | |
|             expect(result).toBe(false);
 | |
|         });
 | |
| 
 | |
|         it("should flag true on A length > B length", () => {
 | |
|             const a = [1, 2, 3, 4];
 | |
|             const b = [1, 2, 3];
 | |
|             const result = arrayHasOrderChange(a, b);
 | |
|             expect(result).toBe(true);
 | |
|         });
 | |
| 
 | |
|         it("should flag true on A length < B length", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 3, 4];
 | |
|             const result = arrayHasOrderChange(a, b);
 | |
|             expect(result).toBe(true);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayHasDiff", () => {
 | |
|         it("should flag true on A length > B length", () => {
 | |
|             const a = [1, 2, 3, 4];
 | |
|             const b = [1, 2, 3];
 | |
|             const result = arrayHasDiff(a, b);
 | |
|             expect(result).toBe(true);
 | |
|         });
 | |
| 
 | |
|         it("should flag true on A length < B length", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 3, 4];
 | |
|             const result = arrayHasDiff(a, b);
 | |
|             expect(result).toBe(true);
 | |
|         });
 | |
| 
 | |
|         it("should flag true on element differences", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [4, 5, 6];
 | |
|             const result = arrayHasDiff(a, b);
 | |
|             expect(result).toBe(true);
 | |
|         });
 | |
| 
 | |
|         it("should flag false if same but order different", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [3, 1, 2];
 | |
|             const result = arrayHasDiff(a, b);
 | |
|             expect(result).toBe(false);
 | |
|         });
 | |
| 
 | |
|         it("should flag false if same", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 3];
 | |
|             const result = arrayHasDiff(a, b);
 | |
|             expect(result).toBe(false);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayDiff", () => {
 | |
|         it("should see added from A->B", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 3, 4];
 | |
|             const result = arrayDiff(a, b);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result.added).toBeDefined();
 | |
|             expect(result.removed).toBeDefined();
 | |
|             expect(result.added).toHaveLength(1);
 | |
|             expect(result.removed).toHaveLength(0);
 | |
|             expect(result.added).toEqual([4]);
 | |
|         });
 | |
| 
 | |
|         it("should see removed from A->B", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2];
 | |
|             const result = arrayDiff(a, b);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result.added).toBeDefined();
 | |
|             expect(result.removed).toBeDefined();
 | |
|             expect(result.added).toHaveLength(0);
 | |
|             expect(result.removed).toHaveLength(1);
 | |
|             expect(result.removed).toEqual([3]);
 | |
|         });
 | |
| 
 | |
|         it("should see added and removed in the same set", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 4]; // note diff
 | |
|             const result = arrayDiff(a, b);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result.added).toBeDefined();
 | |
|             expect(result.removed).toBeDefined();
 | |
|             expect(result.added).toHaveLength(1);
 | |
|             expect(result.removed).toHaveLength(1);
 | |
|             expect(result.added).toEqual([4]);
 | |
|             expect(result.removed).toEqual([3]);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayIntersection", () => {
 | |
|         it("should return the intersection", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 4]; // note diff
 | |
|             const result = arrayIntersection(a, b);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(2);
 | |
|             expect(result).toEqual([1, 2]);
 | |
|         });
 | |
| 
 | |
|         it("should return an empty array on no matches", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [4, 5, 6];
 | |
|             const result = arrayIntersection(a, b);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(0);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("arrayUnion", () => {
 | |
|         it("should union 3 arrays with deduplication", () => {
 | |
|             const a = [1, 2, 3];
 | |
|             const b = [1, 2, 4, 5]; // note missing 3
 | |
|             const c = [6, 7, 8, 9];
 | |
|             const result = arrayUnion(a, b, c);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(9);
 | |
|             expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | |
|         });
 | |
| 
 | |
|         it("should deduplicate a single array", () => {
 | |
|             // dev note: this is technically an edge case, but it is described behaviour if the
 | |
|             // function is only provided one array (it'll merge the array against itself)
 | |
|             const a = [1, 1, 2, 2, 3, 3];
 | |
|             const result = arrayUnion(a);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result).toHaveLength(3);
 | |
|             expect(result).toEqual([1, 2, 3]);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("ArrayUtil", () => {
 | |
|         it("should maintain the pointer to the given array", () => {
 | |
|             const input = [1, 2, 3];
 | |
|             const result = new ArrayUtil(input);
 | |
|             expect(result.value).toBe(input);
 | |
|         });
 | |
| 
 | |
|         it("should group appropriately", () => {
 | |
|             const input = [
 | |
|                 ["a", 1],
 | |
|                 ["b", 2],
 | |
|                 ["c", 3],
 | |
|                 ["a", 4],
 | |
|                 ["a", 5],
 | |
|                 ["b", 6],
 | |
|             ];
 | |
|             const output = {
 | |
|                 a: [
 | |
|                     ["a", 1],
 | |
|                     ["a", 4],
 | |
|                     ["a", 5],
 | |
|                 ],
 | |
|                 b: [
 | |
|                     ["b", 2],
 | |
|                     ["b", 6],
 | |
|                 ],
 | |
|                 c: [["c", 3]],
 | |
|             };
 | |
|             const result = new ArrayUtil(input).groupBy((p) => p[0]);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result.value).toBeDefined();
 | |
| 
 | |
|             const asObject = Object.fromEntries(result.value.entries());
 | |
|             expect(asObject).toMatchObject(output);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("GroupedArray", () => {
 | |
|         it("should maintain the pointer to the given map", () => {
 | |
|             const input = new Map([
 | |
|                 ["a", [1, 2, 3]],
 | |
|                 ["b", [7, 8, 9]],
 | |
|                 ["c", [4, 5, 6]],
 | |
|             ]);
 | |
|             const result = new GroupedArray(input);
 | |
|             expect(result.value).toBe(input);
 | |
|         });
 | |
| 
 | |
|         it("should ordering by the provided key order", () => {
 | |
|             const input = new Map([
 | |
|                 ["a", [1, 2, 3]],
 | |
|                 ["b", [7, 8, 9]], // note counting diff
 | |
|                 ["c", [4, 5, 6]],
 | |
|             ]);
 | |
|             const output = [4, 5, 6, 1, 2, 3, 7, 8, 9];
 | |
|             const keyOrder = ["c", "a", "b"]; // note weird order to cause the `output` to be strange
 | |
|             const result = new GroupedArray(input).orderBy(keyOrder);
 | |
|             expect(result).toBeDefined();
 | |
|             expect(result.value).toBeDefined();
 | |
|             expect(result.value).toEqual(output);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("concat", () => {
 | |
|         const emptyArray = () => new Uint8Array(0);
 | |
|         const array1 = () => new Uint8Array([1, 2, 3]);
 | |
|         const array2 = () => new Uint8Array([4, 5, 6]);
 | |
|         const array3 = () => new Uint8Array([7, 8, 9]);
 | |
| 
 | |
|         it("should work for empty arrays", () => {
 | |
|             expect(concat(emptyArray(), emptyArray())).toEqual(emptyArray());
 | |
|         });
 | |
| 
 | |
|         it("should concat an empty and non-empty array", () => {
 | |
|             expect(concat(emptyArray(), array1())).toEqual(array1());
 | |
|         });
 | |
| 
 | |
|         it("should concat an non-empty and empty array", () => {
 | |
|             expect(concat(array1(), emptyArray())).toEqual(array1());
 | |
|         });
 | |
| 
 | |
|         it("should concat two arrays", () => {
 | |
|             expect(concat(array1(), array2())).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6]));
 | |
|         });
 | |
| 
 | |
|         it("should concat three arrays", () => {
 | |
|             expect(concat(array1(), array2(), array3())).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("asyncEvery", () => {
 | |
|         it("when called with an empty array, it should return true", async () => {
 | |
|             expect(await asyncEvery([], jest.fn().mockResolvedValue(true))).toBe(true);
 | |
|         });
 | |
| 
 | |
|         it("when called with some items and the predicate resolves to true for all of them, it should return true", async () => {
 | |
|             const predicate = jest.fn().mockResolvedValue(true);
 | |
|             expect(await asyncEvery([1, 2, 3], predicate)).toBe(true);
 | |
|             expect(predicate).toHaveBeenCalledTimes(3);
 | |
|             expect(predicate).toHaveBeenCalledWith(1);
 | |
|             expect(predicate).toHaveBeenCalledWith(2);
 | |
|             expect(predicate).toHaveBeenCalledWith(3);
 | |
|         });
 | |
| 
 | |
|         it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
 | |
|             const predicate = jest.fn().mockResolvedValue(false);
 | |
|             expect(await asyncEvery([1, 2, 3], predicate)).toBe(false);
 | |
|             expect(predicate).toHaveBeenCalledTimes(1);
 | |
|             expect(predicate).toHaveBeenCalledWith(1);
 | |
|         });
 | |
| 
 | |
|         it("when called with some items and the predicate resolves to false for one of them, it should return false", async () => {
 | |
|             const predicate = jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false);
 | |
|             expect(await asyncEvery([1, 2, 3], predicate)).toBe(false);
 | |
|             expect(predicate).toHaveBeenCalledTimes(2);
 | |
|             expect(predicate).toHaveBeenCalledWith(1);
 | |
|             expect(predicate).toHaveBeenCalledWith(2);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("asyncSome", () => {
 | |
|         it("when called with an empty array, it should return false", async () => {
 | |
|             expect(await asyncSome([], jest.fn().mockResolvedValue(true))).toBe(false);
 | |
|         });
 | |
| 
 | |
|         it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
 | |
|             const predicate = jest.fn().mockResolvedValue(false);
 | |
|             expect(await asyncSome([1, 2, 3], predicate)).toBe(false);
 | |
|             expect(predicate).toHaveBeenCalledTimes(3);
 | |
|             expect(predicate).toHaveBeenCalledWith(1);
 | |
|             expect(predicate).toHaveBeenCalledWith(2);
 | |
|             expect(predicate).toHaveBeenCalledWith(3);
 | |
|         });
 | |
| 
 | |
|         it("when called with some items and the predicate resolves to true, it should short-circuit and return true", async () => {
 | |
|             const predicate = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
 | |
|             expect(await asyncSome([1, 2, 3], predicate)).toBe(true);
 | |
|             expect(predicate).toHaveBeenCalledTimes(2);
 | |
|             expect(predicate).toHaveBeenCalledWith(1);
 | |
|             expect(predicate).toHaveBeenCalledWith(2);
 | |
|         });
 | |
|     });
 | |
| });
 |