Skip to content

Commit

Permalink
feat: support findLast and reduceRight methods (#9573)
Browse files Browse the repository at this point in the history
* feat(collection): findLast and reverseRight

* chore(collection): performance and tests change

Co-authored-by: kyra <[email protected]>

* remove unnecessary code

* Update packages/collection/src/collection.ts

Co-authored-by: Aura Román <[email protected]>

* Apply suggestions from code review

Co-authored-by: Jiralite <[email protected]>

---------

Co-authored-by: kyra <[email protected]>
Co-authored-by: Jiralite <[email protected]>
  • Loading branch information
3 people authored Nov 5, 2023
1 parent c051ed9 commit ac64508
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 0 deletions.
70 changes: 70 additions & 0 deletions packages/collection/__tests__/collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>((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'),
);
});
});
89 changes: 89 additions & 0 deletions packages/collection/src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,64 @@ export class Collection<K, V> extends Map<K, V> {
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<V2 extends V>(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<This, V2 extends V>(
fn: (this: This, value: V, key: K, collection: this) => value is V2,
thisArg: This,
): V2 | undefined;
public findLast<This>(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<K2 extends K>(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<This, K2 extends K>(
fn: (this: This, value: V, key: K, collection: this) => key is K2,
thisArg: This,
): K2 | undefined;
public findLastKey<This>(
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.
*
Expand Down Expand Up @@ -533,6 +591,37 @@ export class Collection<K, V> extends Map<K, V> {
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<T>(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()},
Expand Down

0 comments on commit ac64508

Please sign in to comment.