Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support iterables in places #90

Closed
wants to merge 10 commits into from
25 changes: 19 additions & 6 deletions src/array/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { reduceIterable, type CastIterableItem } from 'radashi'

/**
* Splits a single list into many lists of the desired size.
*
Expand All @@ -8,10 +10,21 @@
* // [[1, 2], [3, 4], [5, 6]]
* ```
*/
export function cluster<T>(array: readonly T[], size = 2): T[][] {
const clusters: T[][] = []
for (let i = 0; i < array.length; i += size) {
clusters.push(array.slice(i, i + size))
}
return clusters
export function cluster<T extends object>(
iterable: T,
size = 2,
): CastIterableItem<T>[][] {
const clusters = [[]] as any[][]
let [cluster] = clusters
return reduceIterable(
iterable,
(clusters, item) => {
if (cluster.length === size) {
clusters.push((cluster = []))
}
cluster.push(item)
return clusters
},
clusters,
)
}
25 changes: 14 additions & 11 deletions src/array/counting.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { reduceIterable, type CastIterableItem } from 'radashi'

/**
* Counts the occurrences of each unique value returned by the `identity`
* function when applied to each item in the array.
Expand All @@ -9,19 +11,20 @@
* // { even: 2, odd: 2 }
* ```
*/
export function counting<T, TId extends string | number | symbol>(
array: readonly T[],
identity: (item: T) => TId,
): Record<TId, number> {
if (!array) {
return {} as Record<TId, number>
export function counting<T extends object, K extends keyof any>(
iterable: T,
identity: (item: CastIterableItem<T>) => K,
): Record<K, number> {
if (!iterable) {
return {} as Record<K, number>
}
return array.reduce(
(acc, item) => {
return reduceIterable(
iterable,
(counts, item) => {
const id = identity(item)
acc[id] = (acc[id] ?? 0) + 1
return acc
counts[id] = (counts[id] ?? 0) + 1
return counts
},
{} as Record<TId, number>,
{} as Record<K, number>,
)
}
19 changes: 10 additions & 9 deletions src/array/group.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { reduceIterable, type CastIterableItem } from 'radashi'

/**
* Sorts an `array` of items into groups. The return value is a map
* where the keys are the group IDs the given `getGroupId` function
Expand All @@ -10,19 +12,18 @@
* // { even: [2], odd: [1, 3, 4] }
* ```
*/
export function group<T, Key extends string | number | symbol>(
array: readonly T[],
getGroupId: (item: T) => Key,
): { [K in Key]?: T[] } {
return array.reduce(
export function group<T extends object, Key extends string | number | symbol>(
iterable: T,
getGroupId: (item: CastIterableItem<T>) => Key,
): { [K in Key]?: CastIterableItem<T>[] } {
return reduceIterable(
iterable,
(acc, item) => {
const groupId = getGroupId(item)
if (!acc[groupId]) {
acc[groupId] = []
}
acc[groupId] ??= []
acc[groupId].push(item)
return acc
},
{} as Record<Key, T[]>,
{} as Record<Key, CastIterableItem<T>[]>,
)
}
53 changes: 32 additions & 21 deletions src/array/select.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { reduceIterable, type CastIterableItem } from 'radashi'

/**
* Select performs a filter and a mapper inside of a reduce, only
* iterating the list one time. If condition is omitted, will
Expand All @@ -14,32 +16,41 @@
* // => [9, 16]
* ```
*/
export function select<T, U>(
array: readonly T[],
mapper: (item: T, index: number) => U,
condition: (item: T, index: number) => boolean,
export function select<T extends object, U>(
iterable: T,
mapper: (item: CastIterableItem<T>, index: number) => U | null | undefined,
): U[]

export function select<T, U>(
array: readonly T[],
mapper: (item: T, index: number) => U | null | undefined,
export function select<T extends object, U>(
iterable: T,
mapper: (item: CastIterableItem<T>, index: number) => U,
condition?: (item: CastIterableItem<T>, index: number) => boolean,
): U[]

export function select<T, U>(
array: readonly T[],
mapper: (item: T, index: number) => U,
condition?: (item: T, index: number) => boolean,
export function select<T extends object, U>(
iterable: T,
mapper: (item: CastIterableItem<T>, index: number) => U,
condition?: (item: CastIterableItem<T>, index: number) => boolean,
): U[] {
if (!array) {
if (!iterable) {
return []
}
let mapped: U
return array.reduce((acc, item, index) => {
if (condition) {
condition(item, index) && acc.push(mapper(item, index))
} else if ((mapped = mapper(item, index)) != null) {
acc.push(mapped)
}
return acc
}, [] as U[])
return reduceIterable(
iterable,
condition
? (acc, item, index) => {
if (condition(item, index)) {
acc.push(mapper(item, index))
}
return acc
}
: (acc, item, index) => {
const mapped = mapper(item, index)
if (mapped != null) {
acc.push(mapped)
}
return acc
},
[] as U[],
)
}
29 changes: 19 additions & 10 deletions src/array/selectFirst.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { searchIterable, type CastIterableItem } from 'radashi'

/**
* Select performs a find + map operation, short-circuiting on the first
* element that satisfies the prescribed condition. If condition is omitted,
Expand All @@ -14,18 +16,25 @@
* // => 9
* ```
*/
export function selectFirst<T, U>(
array: readonly T[],
mapper: (item: T, index: number) => U,
condition?: (item: T, index: number) => boolean,
export function selectFirst<T extends object, U>(
iterable: T,
mapper: (item: CastIterableItem<T>, index: number) => U,
condition?: (item: CastIterableItem<T>, index: number) => boolean,
): U | undefined {
if (!array) {
if (!iterable) {
return undefined
}
let foundIndex = -1
const found = array.find((item, index) => {
foundIndex = index
return condition ? condition(item, index) : mapper(item, index) != null
let mapped: U | undefined
searchIterable(iterable, (item, index) => {
if (!condition) {
mapped = mapper(item, index)
if (mapped != null) {
return true
}
} else if (condition(item, index)) {
mapped = mapper(item, index)
return true
}
})
return found === undefined ? undefined : mapper(found, foundIndex)
return mapped ?? undefined
}
19 changes: 19 additions & 0 deletions src/iterable/castIterable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type CastIterableItem<Input extends object> = Input extends Iterable<
infer Item
>
? Item
: [string, unknown]

export type CastIterable<Input> = Input extends object
? Input extends Iterable<any>
? Input
: Iterable<CastIterableItem<Input>>
: never

export function castIterable<Input extends object>(
input: Input,
): CastIterable<Input> {
return (
(input as any)[Symbol.iterator] ? input : Object.entries(input)
) as CastIterable<Input>
}
41 changes: 41 additions & 0 deletions src/iterable/reduceIterable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { castIterable, isArray, type CastIterableItem } from 'radashi'

export function reduceIterable<TIterable extends object, TResult>(
iterable: TIterable,
reducer: (
acc: TResult,
value: CastIterableItem<TIterable>,
index: number,
) => TResult,
initial: TResult,
): TResult

export function reduceIterable<TIterable extends object, TResult>(
iterable: TIterable,
reducer: (
acc: TResult | undefined,
value: CastIterableItem<TIterable>,
index: number,
) => TResult,
initial?: TResult | undefined,
): TResult | undefined

export function reduceIterable<TIterable extends object, TResult>(
iterable: TIterable,
reducer: (
acc: TResult,
value: CastIterableItem<TIterable>,
index: number,
) => TResult,
initial?: TResult | undefined,
): TResult | undefined {
if (!isArray(iterable)) {
let acc = initial as TResult
let index = 0
for (const item of castIterable(iterable)) {
acc = reducer(acc, item, index++)
}
return acc
}
return (iterable as any[]).reduce(reducer, initial)
}
26 changes: 26 additions & 0 deletions src/iterable/searchIterable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { castIterable, isArray, type CastIterableItem } from 'radashi'

export function searchIterable<T extends object>(
iterable: T,
match: (item: CastIterableItem<T>, index: number) => boolean | undefined,
): CastIterableItem<T> | undefined {
let item: CastIterableItem<T>
if (isArray(iterable)) {
// Note: We can't use Array.prototype.find here, because it
// doesn't respect the sparseness of an array.
;(iterable as any[]).some((it, i) => {
if (match(it, i)) {
item = it
return true
}
})
} else {
let index = 0
for (item of castIterable(iterable)) {
if (match(item, index++)) {
break
}
}
}
return item!
}
4 changes: 4 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export * from './function/castComparator.ts'
export * from './function/castMapping.ts'
export * from './function/noop.ts'

export * from './iterable/castIterable.ts'
export * from './iterable/reduceIterable.ts'
export * from './iterable/searchIterable.ts'

export * from './number/clamp.ts'
export * from './number/inRange.ts'
export * from './number/lerp.ts'
Expand Down
Loading