diff --git a/packages/collection/__tests__/collection.test.ts b/packages/collection/__tests__/collection.test.ts index fc069376b..94be55336 100644 --- a/packages/collection/__tests__/collection.test.ts +++ b/packages/collection/__tests__/collection.test.ts @@ -908,3 +908,73 @@ describe('random thisArg tests', () => { }, array); }); }); + +describe('findLast() tests', () => { + const coll = createTestCollection(); + test('it returns last matched element', () => { + expect(coll.findLast((value) => value % 2 === 1)).toStrictEqual(3); + }); + + test('throws if fn is not a function', () => { + // @ts-expect-error: Invalid function + expectInvalidFunctionError(() => createCollection().findLast()); + // @ts-expect-error: Invalid function + expectInvalidFunctionError(() => createCollection().findLast(123), 123); + }); + + test('binds the thisArg', () => { + coll.findLast(function findLast() { + expect(this).toBeNull(); + return true; + }, null); + }); +}); + +describe('findLastKey() tests', () => { + const coll = createTestCollection(); + test('it returns last matched element', () => { + expect(coll.findLastKey((value) => value % 2 === 1)).toStrictEqual('c'); + }); + + test('throws if fn is not a function', () => { + // @ts-expect-error: Invalid function + expectInvalidFunctionError(() => createCollection().findLastKey()); + // @ts-expect-error: Invalid function + expectInvalidFunctionError(() => createCollection().findLastKey(123), 123); + }); + + test('binds the thisArg', () => { + coll.findLastKey(function findLastKey() { + expect(this).toBeNull(); + return true; + }, null); + }); +}); + +describe('reduceRight() tests', () => { + const coll = createTestCollection(); + + test('throws if fn is not a function', () => { + // @ts-expect-error: Invalid function + expectInvalidFunctionError(() => coll.reduceRight()); + // @ts-expect-error: Invalid function + expectInvalidFunctionError(() => coll.reduceRight(123), 123); + }); + + test('reduce collection into a single value with initial value', () => { + const sum = coll.reduceRight((a, x) => a + x, 0); + expect(sum).toStrictEqual(6); + }); + + test('reduce collection into a single value without initial value', () => { + const sum = coll.reduceRight((a, x) => a + x); + expect(sum).toStrictEqual(6); + }); + + test('reduce empty collection without initial value', () => { + const coll = createCollection(); + expect(() => coll.reduceRight((a: number, x) => a + x)).toThrowError( + new TypeError('Reduce of empty collection with no initial value'), + ); + }); +}); diff --git a/packages/collection/src/collection.ts b/packages/collection/src/collection.ts index 5644085ca..4f54dc2f4 100644 --- a/packages/collection/src/collection.ts +++ b/packages/collection/src/collection.ts @@ -274,6 +274,64 @@ export class Collection extends Map { return undefined; } + /** + * Searches for a last item where the given function returns a truthy value. This behaves like + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast | Array.findLast()}. + * + * @param fn - The function to test with (should return a boolean) + * @param thisArg - Value to use as `this` when executing the function + */ + public findLast(fn: (value: V, key: K, collection: this) => value is V2): V2 | undefined; + public findLast(fn: (value: V, key: K, collection: this) => unknown): V | undefined; + public findLast( + fn: (this: This, value: V, key: K, collection: this) => value is V2, + thisArg: This, + ): V2 | undefined; + public findLast(fn: (this: This, value: V, key: K, collection: this) => unknown, thisArg: This): V | undefined; + public findLast(fn: (value: V, key: K, collection: this) => unknown, thisArg?: unknown): V | undefined { + if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`); + if (thisArg !== undefined) fn = fn.bind(thisArg); + const entries = [...this.entries()]; + for (let index = entries.length - 1; index >= 0; index--) { + const val = entries[index]![1]; + const key = entries[index]![0]; + if (fn(val, key, this)) return val; + } + + return undefined; + } + + /** + * Searches for the key of a last item where the given function returns a truthy value. This behaves like + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex | Array.findLastIndex()}, + * but returns the key rather than the positional index. + * + * @param fn - The function to test with (should return a boolean) + * @param thisArg - Value to use as `this` when executing the function + */ + public findLastKey(fn: (value: V, key: K, collection: this) => key is K2): K2 | undefined; + public findLastKey(fn: (value: V, key: K, collection: this) => unknown): K | undefined; + public findLastKey( + fn: (this: This, value: V, key: K, collection: this) => key is K2, + thisArg: This, + ): K2 | undefined; + public findLastKey( + fn: (this: This, value: V, key: K, collection: this) => unknown, + thisArg: This, + ): K | undefined; + public findLastKey(fn: (value: V, key: K, collection: this) => unknown, thisArg?: unknown): K | undefined { + if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`); + if (thisArg !== undefined) fn = fn.bind(thisArg); + const entries = [...this.entries()]; + for (let index = entries.length - 1; index >= 0; index--) { + const key = entries[index]![0]; + const val = entries[index]![1]; + if (fn(val, key, this)) return key; + } + + return undefined; + } + /** * Removes items that satisfy the provided filter function. * @@ -533,6 +591,37 @@ export class Collection extends Map { return accumulator; } + /** + * Applies a function to produce a single value. Identical in behavior to + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight | Array.reduceRight()}. + * + * @param fn - Function used to reduce, taking four arguments; `accumulator`, `value`, `key`, and `collection` + * @param initialValue - Starting value for the accumulator + */ + public reduceRight(fn: (accumulator: T, value: V, key: K, collection: this) => T, initialValue?: T): T { + if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`); + const entries = [...this.entries()]; + let accumulator!: T; + + let index: number; + if (initialValue === undefined) { + if (entries.length === 0) throw new TypeError('Reduce of empty collection with no initial value'); + accumulator = entries[entries.length - 1]![1] as unknown as T; + index = entries.length - 1; + } else { + accumulator = initialValue; + index = entries.length; + } + + while (--index >= 0) { + const key = entries[index]![0]; + const val = entries[index]![1]; + accumulator = fn(accumulator, val, key, this); + } + + return accumulator; + } + /** * Identical to * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach | Map.forEach()},