clean-jsdoc-theme TypeDoc APIv0.0.0
docs-site/typedoc-src/utils/array.tstypescript
/**
 * Array utility functions
 *
 * @remarks
 * Provides type-safe array manipulation utilities.
 *
 * @packageDocumentation
 */

/**
 * Groups array elements by a key selector
 *
 * @typeParam T - Array element type
 * @typeParam K - Key type (must be string | number | symbol)
 * @param array - The input array
 * @param keySelector - Function to extract the grouping key
 * @returns Object with grouped elements
 *
 * @example
 * ```typescript
 * const users = [
 *   { name: 'Alice', age: 25 },
 *   { name: 'Bob', age: 30 },
 *   { name: 'Charlie', age: 25 }
 * ]
 *
 * groupBy(users, u => u.age)
 * // { 25: [{ name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 }], 30: [{ name: 'Bob', age: 30 }] }
 * ```
 *
 * @public
 */
export function groupBy<T, K extends string | number | symbol>(
  array: T[],
  keySelector: (item: T) => K
): Record<K, T[]> {
  return array.reduce(
    (result, item) => {
      const key = keySelector(item)
      if (!result[key]) {
        result[key] = []
      }
      result[key].push(item)
      return result
    },
    {} as Record<K, T[]>
  )
}

/**
 * Removes duplicate elements from an array
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @param keySelector - Optional function to extract comparison key
 * @returns Array with unique elements
 *
 * @example
 * ```typescript
 * unique([1, 2, 2, 3, 3, 3]) // [1, 2, 3]
 *
 * unique(
 *   [{ id: 1, name: 'a' }, { id: 1, name: 'b' }, { id: 2, name: 'c' }],
 *   item => item.id
 * )
 * // [{ id: 1, name: 'a' }, { id: 2, name: 'c' }]
 * ```
 *
 * @public
 */
export function unique<T>(array: T[], keySelector?: (item: T) => unknown): T[] {
  if (!keySelector) {
    return [...new Set(array)]
  }

  const seen = new Set<unknown>()
  return array.filter((item) => {
    const key = keySelector(item)
    if (seen.has(key)) {
      return false
    }
    seen.add(key)
    return true
  })
}

/**
 * Chunks an array into smaller arrays of a specified size
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @param size - Size of each chunk
 * @returns Array of chunks
 *
 * @example
 * ```typescript
 * chunk([1, 2, 3, 4, 5], 2)
 * // [[1, 2], [3, 4], [5]]
 * ```
 *
 * @throws {@link Error} If size is less than 1
 *
 * @public
 */
export function chunk<T>(array: T[], size: number): T[][] {
  if (size < 1) {
    throw new Error('Chunk size must be at least 1')
  }

  const result: T[][] = []
  for (let i = 0; i < array.length; i += size) {
    result.push(array.slice(i, i + size))
  }
  return result
}

/**
 * Flattens a nested array to a specified depth
 *
 * @typeParam T - Base element type
 * @param array - The nested array
 * @param depth - Maximum depth to flatten
 * @returns Flattened array
 *
 * @example
 * ```typescript
 * flatten([1, [2, [3, [4]]]], 1) // [1, 2, [3, [4]]]
 * flatten([1, [2, [3, [4]]]], 2) // [1, 2, 3, [4]]
 * flatten([1, [2, [3, [4]]]], Infinity) // [1, 2, 3, 4]
 * ```
 *
 * @public
 */
export function flatten<T>(array: unknown[], depth = 1): T[] {
  if (depth < 1) {
    return array.slice() as T[]
  }

  return array.reduce<T[]>((result, item) => {
    if (Array.isArray(item) && depth > 0) {
      result.push(...flatten<T>(item, depth - 1))
    } else {
      result.push(item as T)
    }
    return result
  }, [])
}

/**
 * Finds the intersection of two arrays
 *
 * @typeParam T - Array element type
 * @param a - First array
 * @param b - Second array
 * @returns Array of elements present in both arrays
 *
 * @example
 * ```typescript
 * intersection([1, 2, 3], [2, 3, 4]) // [2, 3]
 * ```
 *
 * @public
 */
export function intersection<T>(a: T[], b: T[]): T[] {
  const setB = new Set(b)
  return a.filter((item) => setB.has(item))
}

/**
 * Finds the difference between two arrays
 *
 * @typeParam T - Array element type
 * @param a - First array
 * @param b - Second array
 * @returns Elements in `a` but not in `b`
 *
 * @example
 * ```typescript
 * difference([1, 2, 3], [2, 3, 4]) // [1]
 * ```
 *
 * @public
 */
export function difference<T>(a: T[], b: T[]): T[] {
  const setB = new Set(b)
  return a.filter((item) => !setB.has(item))
}

/**
 * Finds the union of two arrays
 *
 * @typeParam T - Array element type
 * @param a - First array
 * @param b - Second array
 * @returns Array with unique elements from both arrays
 *
 * @example
 * ```typescript
 * union([1, 2, 3], [2, 3, 4]) // [1, 2, 3, 4]
 * ```
 *
 * @public
 */
export function union<T>(a: T[], b: T[]): T[] {
  return [...new Set([...a, ...b])]
}

/**
 * Shuffles an array using Fisher-Yates algorithm
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @returns New shuffled array (does not modify original)
 *
 * @example
 * ```typescript
 * shuffle([1, 2, 3, 4, 5]) // [3, 1, 5, 2, 4] (random order)
 * ```
 *
 * @public
 */
export function shuffle<T>(array: T[]): T[] {
  const result = [...array]
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[result[i], result[j]] = [result[j], result[i]]
  }
  return result
}

/**
 * Picks a random element from an array
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @returns Random element or undefined if array is empty
 *
 * @example
 * ```typescript
 * sample([1, 2, 3, 4, 5]) // 3 (random)
 * sample([]) // undefined
 * ```
 *
 * @public
 */
export function sample<T>(array: T[]): T | undefined {
  if (array.length === 0) return undefined
  return array[Math.floor(Math.random() * array.length)]
}

/**
 * Picks multiple random elements from an array
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @param count - Number of elements to pick
 * @returns Array of random elements
 *
 * @example
 * ```typescript
 * sampleSize([1, 2, 3, 4, 5], 3) // [2, 4, 1] (random)
 * ```
 *
 * @public
 */
export function sampleSize<T>(array: T[], count: number): T[] {
  const shuffled = shuffle(array)
  return shuffled.slice(0, Math.min(count, array.length))
}

/**
 * Sorts an array by multiple criteria
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @param comparators - Array of comparator functions
 * @returns New sorted array
 *
 * @example
 * ```typescript
 * const users = [
 *   { name: 'Bob', age: 30 },
 *   { name: 'Alice', age: 25 },
 *   { name: 'Alice', age: 30 }
 * ]
 *
 * sortBy(users, [
 *   (a, b) => a.name.localeCompare(b.name),
 *   (a, b) => a.age - b.age
 * ])
 * // [{ name: 'Alice', age: 25 }, { name: 'Alice', age: 30 }, { name: 'Bob', age: 30 }]
 * ```
 *
 * @public
 */
export function sortBy<T>(array: T[], comparators: ((a: T, b: T) => number)[]): T[] {
  return [...array].sort((a, b) => {
    for (const comparator of comparators) {
      const result = comparator(a, b)
      if (result !== 0) return result
    }
    return 0
  })
}

/**
 * Creates an array of numbers from start to end
 *
 * @param start - Start value (inclusive)
 * @param end - End value (exclusive)
 * @param step - Step value
 * @returns Array of numbers
 *
 * @example
 * ```typescript
 * range(0, 5) // [0, 1, 2, 3, 4]
 * range(1, 10, 2) // [1, 3, 5, 7, 9]
 * range(5, 0, -1) // [5, 4, 3, 2, 1]
 * ```
 *
 * @public
 */
export function range(start: number, end: number, step = 1): number[] {
  if (step === 0) {
    throw new Error('Step cannot be zero')
  }

  const result: number[] = []
  if (step > 0) {
    for (let i = start; i < end; i += step) {
      result.push(i)
    }
  } else {
    for (let i = start; i > end; i += step) {
      result.push(i)
    }
  }
  return result
}

/**
 * Zips multiple arrays together
 *
 * @typeParam T - Tuple type of arrays
 * @param arrays - Arrays to zip
 * @returns Array of tuples
 *
 * @example
 * ```typescript
 * zip([1, 2, 3], ['a', 'b', 'c']) // [[1, 'a'], [2, 'b'], [3, 'c']]
 * zip([1, 2], ['a', 'b', 'c']) // [[1, 'a'], [2, 'b']]
 * ```
 *
 * @public
 */
export function zip<T extends unknown[][]>(...arrays: T): { [K in keyof T]: T[K] extends (infer U)[] ? U : never }[] {
  if (arrays.length === 0) return []

  const minLength = Math.min(...arrays.map((arr) => arr.length))
  const result: unknown[][] = []

  for (let i = 0; i < minLength; i++) {
    result.push(arrays.map((arr) => arr[i]))
  }

  return result as { [K in keyof T]: T[K] extends (infer U)[] ? U : never }[]
}

/**
 * Partitions an array into two arrays based on a predicate
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @param predicate - Function to test elements
 * @returns Tuple of [matching, non-matching] elements
 *
 * @example
 * ```typescript
 * partition([1, 2, 3, 4, 5], n => n % 2 === 0)
 * // [[2, 4], [1, 3, 5]]
 * ```
 *
 * @public
 */
export function partition<T>(array: T[], predicate: (item: T) => boolean): [T[], T[]] {
  const truthy: T[] = []
  const falsy: T[] = []

  for (const item of array) {
    if (predicate(item)) {
      truthy.push(item)
    } else {
      falsy.push(item)
    }
  }

  return [truthy, falsy]
}

/**
 * Gets the first element of an array
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @returns First element or undefined
 *
 * @example
 * ```typescript
 * first([1, 2, 3]) // 1
 * first([]) // undefined
 * ```
 *
 * @public
 */
export function first<T>(array: T[]): T | undefined {
  return array[0]
}

/**
 * Gets the last element of an array
 *
 * @typeParam T - Array element type
 * @param array - The input array
 * @returns Last element or undefined
 *
 * @example
 * ```typescript
 * last([1, 2, 3]) // 3
 * last([]) // undefined
 * ```
 *
 * @public
 */
export function last<T>(array: T[]): T | undefined {
  return array[array.length - 1]
}

/**
 * Creates a lookup object from an array
 *
 * @typeParam T - Array element type
 * @typeParam K - Key type
 * @param array - The input array
 * @param keySelector - Function to extract the key
 * @returns Object mapping keys to elements
 *
 * @example
 * ```typescript
 * const users = [
 *   { id: '1', name: 'Alice' },
 *   { id: '2', name: 'Bob' }
 * ]
 *
 * keyBy(users, u => u.id)
 * // { '1': { id: '1', name: 'Alice' }, '2': { id: '2', name: 'Bob' } }
 * ```
 *
 * @public
 */
export function keyBy<T, K extends string | number | symbol>(
  array: T[],
  keySelector: (item: T) => K
): Record<K, T> {
  return array.reduce(
    (result, item) => {
      result[keySelector(item)] = item
      return result
    },
    {} as Record<K, T>
  )
}