Skip to content

Commit

Permalink
fix(reduce): align with native reduce behavior + perf improvements (#341
Browse files Browse the repository at this point in the history
)
  • Loading branch information
aleclarson authored Jan 26, 2025
1 parent 48f29b9 commit 1bc47b7
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 50 deletions.
36 changes: 26 additions & 10 deletions src/async/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,33 @@
*/
export async function reduce<T, K>(
array: readonly T[],
asyncReducer: (acc: K, item: T, index: number) => Promise<K>,
initValue?: K,
reducer: (acc: K, item: T, index: number) => Promise<K>,
initialValue: K,
): Promise<K>
export async function reduce<T, K>(
array: readonly T[],
reducer: (acc: K, item: T, index: number) => Promise<K>,
): Promise<K>
export async function reduce<T, K>(
array: readonly T[],
reducer: (acc: K, item: T, index: number) => Promise<K>,
initialValue?: K,
): Promise<K> {
const initProvided = initValue !== undefined
if (!initProvided && array?.length < 1) {
throw new Error('Cannot reduce empty array with no init value')
if (!array) {
array = []
}
const indices = array.keys()
let acc = initialValue
// biome-ignore lint/style/noArguments:
if (acc === undefined && arguments.length < 3) {
if (!array.length) {
throw new TypeError('Reduce of empty array with no initial value')
}
acc = array[0] as any
indices.next()
}
const iter = initProvided ? array : array.slice(1)
let value: any = initProvided ? initValue : array[0]
for (const [i, item] of iter.entries()) {
value = await asyncReducer(value, item, i)
for (const index of indices) {
acc = await reducer(acc!, array[index], index)
}
return value
return acc!
}
51 changes: 11 additions & 40 deletions tests/async/reduce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,15 @@ import * as _ from 'radashi'
const cast = <T = any[]>(value: any): T => value

describe('asyncReduce', () => {
const numbers = [0, 1, 2, 3, 4]
const reducer = async (a: number, b: number): Promise<number> => {
return new Promise(res => res(a + b))
}
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
})
test('returns result of reducer', async () => {
const numbers = [
0,
1,
2,
3,
4, // => 10
]
const asyncSum = async (a: number, b: number): Promise<number> => {
return new Promise(res => res(a + b))
}
const result = await _.reduce<number, number>(numbers, asyncSum, 0)
const result = await _.reduce<number, number>(numbers, reducer, 0)
expect(result).toBe(10)
})
test('passes correct indexes', async () => {
Expand All @@ -35,41 +29,18 @@ describe('asyncReduce', () => {
const result = await _.reduce<string, number[]>(array, asyncSumIndex, [])
expect(result).toEqual([0, 1, 2, 3, 4])
})
})

describe('reduce/asyncReduceV2', () => {
const numbers = [0, 1, 2, 3, 4]
const reducer = async (a: number, b: number): Promise<number> => {
return new Promise(res => res(a + b))
}

beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
})
test('calls asyncReduce', async () => {
const result = await _.reduce<number, number>(numbers, reducer, 0)
expect(result).toBe(10)
})
test('uses first item in array when no init provided', async () => {
const result = await _.reduce(numbers, reducer)
expect(result).toBe(10)
})
test('throws on no init value and an empty array', async () => {
try {
await _.reduce([], reducer)
} catch (err) {
expect(err).not.toBeNull()
return
}
expect.fail('Expected error to be thrown')
await expect(async () => _.reduce([], reducer)).rejects.toThrowError(
'Reduce of empty array with no initial value',
)
})
test('throws on no init value and a null array input', async () => {
try {
await _.reduce(cast(null), reducer)
} catch (err) {
expect(err).not.toBeNull()
return
}
expect.fail('Expected error to be thrown')
await expect(async () =>
_.reduce(cast(null), reducer),
).rejects.toThrowError('Reduce of empty array with no initial value')
})
})

0 comments on commit 1bc47b7

Please sign in to comment.