npm package: @gmana/utils
Description: Utility functions for React and TypeScript projects.
Author: Sun Sreng
Latest version: 1.6.0

# @gmana/utils

A lightweight, dependency-free collection of utility functions for TypeScript and React projects.

## Installation

```bash
npm install @gmana/utils
# or
pnpm add @gmana/utils
# or
bun add @gmana/utils
```

## API Reference

### Array

#### `chunk<T>(array: T[], size: number): T[][]`
Splits an array into chunks of the specified size.
```ts
chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]
```

#### `compact<T>(array: (T | Falsy)[]): T[]`
Removes all falsy values (`null`, `undefined`, `false`, `0`, `""`) from an array.
```ts
compact([1, null, 2, undefined, 3]) // [1, 2, 3]
```

#### `countBy<T, K>(array: T[], keyFn: (item: T) => K): Record<K, number>`
Counts occurrences of each key returned by `keyFn`.
```ts
countBy(['a', 'b', 'a'], x => x) // { a: 2, b: 1 }
```

#### `difference<T>(array1: T[], array2: T[]): T[]`
Returns elements in `array1` that are not in `array2`.
```ts
difference([1, 2, 3], [2]) // [1, 3]
```

#### `drop<T>(array: T[], n: number): T[]`
Removes the first `n` elements from an array.
```ts
drop([1, 2, 3, 4], 2) // [3, 4]
```

#### `dropRight<T>(array: T[], n: number): T[]`
Removes the last `n` elements from an array.
```ts
dropRight([1, 2, 3, 4], 2) // [1, 2]
```

#### `flattenArray<T>(array: (T | T[])[]): T[]`
Flattens one level of nesting.
```ts
flattenArray([1, [2, 3], [4]]) // [1, 2, 3, 4]
```

#### `flattenDeepArray<T>(array): T[]`
Deeply flattens all levels of nesting.
```ts
flattenDeepArray([1, [2, [3, [4]]]]) // [1, 2, 3, 4]
```

#### `groupBy<T, K>(array: T[], key: (item: T) => K): Record<K, T[]>`
Groups array elements by the key returned by `key`.
```ts
groupBy([{ type: 'a' }, { type: 'b' }, { type: 'a' }], x => x.type)
// { a: [{ type: 'a' }, { type: 'a' }], b: [{ type: 'b' }] }
```

#### `groupConsecutive<T, K>(array: T[], keyFn: (item: T) => K): T[][]`
Groups consecutive elements that share the same key into sub-arrays.
```ts
groupConsecutive([1, 1, 2, 2, 1], x => x)
// [[1, 1], [2, 2], [1]]
```

#### `intersection<T>(array1: T[], array2: T[]): T[]`
Returns elements common to both arrays.
```ts
intersection([1, 2, 3], [2, 3, 4]) // [2, 3]
```

#### `maxBy<T>(array: T[], selector: (item: T) => number): T | undefined`
Returns the element with the highest value per `selector`.
```ts
maxBy([{ n: 1 }, { n: 3 }, { n: 2 }], x => x.n) // { n: 3 }
```

#### `meanBy<T>(array: T[], selector: (item: T) => number): number`
Returns the average of values returned by `selector`.
```ts
meanBy([{ n: 1 }, { n: 2 }, { n: 3 }], x => x.n) // 2
```

#### `minBy<T>(array: T[], selector: (item: T) => number): T | undefined`
Returns the element with the lowest value per `selector`.
```ts
minBy([{ n: 1 }, { n: 3 }, { n: 2 }], x => x.n) // { n: 1 }
```

#### `partition<T>(array: T[], predicate: (item: T, index: number) => boolean): [T[], T[]]`
Splits an array into two groups: elements that satisfy the predicate and those that don't.
```ts
partition([1, 2, 3, 4], x => x % 2 === 0) // [[2, 4], [1, 3]]
```

#### `sample<T>(array: T[], n?: number): T[]`
Returns `n` random elements from the array (default `1`).
```ts
sample([1, 2, 3, 4, 5], 2) // e.g. [3, 5]
```

#### `shuffle<T>(array: T[]): T[]`
Returns a new array with elements shuffled using the Fisher-Yates algorithm.
```ts
shuffle([1, 2, 3, 4]) // e.g. [3, 1, 4, 2]
```

#### `sortBy<T>(array: T[], ...selectors: ((item: T) => unknown)[]): T[]`
Sorts by one or more selector functions.
```ts
sortBy(
  [{ last: 'B', first: 'Z' }, { last: 'A', first: 'A' }, { last: 'A', first: 'B' }],
  u => u.last,
  u => u.first
)
// [{ last: 'A', first: 'A' }, { last: 'A', first: 'B' }, { last: 'B', first: 'Z' }]
```

#### `sumBy<T>(array: T[], selector: (item: T) => number): number`
Sums the values returned by `selector`.
```ts
sumBy([{ price: 10 }, { price: 20 }, { price: 5 }], x => x.price) // 35
```

#### `symmetricDifference<T>(array1: T[], array2: T[]): T[]`
Returns elements present in either array but not both.
```ts
symmetricDifference([1, 2, 3], [2, 3, 4]) // [1, 4]
```

#### `take<T>(array: T[], n: number): T[]`
Takes the first `n` elements.
```ts
take([1, 2, 3, 4], 2) // [1, 2]
```

#### `takeRight<T>(array: T[], n: number): T[]`
Takes the last `n` elements.
```ts
takeRight([1, 2, 3, 4], 2) // [3, 4]
```

#### `unique<T>(array: T[]): T[]`
Removes duplicate values.
```ts
unique([1, 2, 2, 3, 3]) // [1, 2, 3]
```

#### `uniqueBy<T, K>(array: T[], keyFn: (item: T) => K): T[]`
Removes duplicates based on the key returned by `keyFn`.
```ts
uniqueBy([{ id: 1, v: 'a' }, { id: 1, v: 'b' }, { id: 2, v: 'c' }], x => x.id)
// [{ id: 1, v: 'a' }, { id: 2, v: 'c' }]
```

#### `zip<T>(...arrays: T): Array<[...]>`
Zips multiple arrays together into an array of tuples.
```ts
zip([1, 2, 3], ['a', 'b', 'c']) // [[1, 'a'], [2, 'b'], [3, 'c']]
```

---

### Object

#### `compactObject<T>(input: T, options?: CompactOptions): Partial<T>`
Removes empty/falsy values from an object.

```ts
interface CompactOptions {
  compactArrays?: boolean
  removeEmptyArrays?: boolean
  isEmpty?: (value: unknown) => boolean
}
```

```ts
compactObject({ a: 1, b: null, c: '' })                      // { a: 1 }
compactObject({ a: [1, null, 2] }, { compactArrays: true })  // { a: [1, 2] }
```

#### `pick<T, K extends keyof T>(names: K[], obj: T): Pick<T, K>`
Picks specified keys from an object. Also available in curried form.
```ts
pick(['a', 'b'], { a: 1, b: 2, c: 3 }) // { a: 1, b: 2 }
pick(['a', 'b'])({ a: 1, b: 2, c: 3 }) // { a: 1, b: 2 }
```

#### `flatten(obj: Record<string, unknown>, separator?: string): Record<string, unknown>`
Flattens a nested object into a single level using the given separator (default `"."`).
```ts
flatten({ a: { b: { c: 1 } } })          // { 'a.b.c': 1 }
flatten({ a: { b: 1 } }, '_')            // { 'a_b': 1 }
```

---

### String

#### `getInitialLetter(fullName?: string | null, fallback?: string): string`
Extracts 1–2 letter initials from a name.
```ts
getInitialLetter('Sun Sreng')       // 'SS'
getInitialLetter('Sun')             // 'S'
getInitialLetter(null, 'N/A')       // 'N/A'
```

#### `makeTitle(base: string, site: string, params: TemplateParams): string`
Builds a page title from a base, site name, and optional template.
```ts
makeTitle('Jobs in Tech', 'Acme', {})
// 'Jobs in Tech | Acme'

makeTitle('Jobs in Tech', 'Acme', { template: '%s - Powered by Acme' })
// 'Jobs in Tech - Powered by Acme'

makeTitle('Jobs in Tech', 'Acme', { template: (title, site) => `${title} :: ${site}` })
// 'Jobs in Tech :: Acme'

makeTitle('Acme OG Preview', 'Acme', { disableSuffix: true })
// 'Acme OG Preview'
```

#### `toCase(input: string, type: CaseType): string`
Converts a string to the specified case. Supported types: `lowercase`, `uppercase`, `sentence`, `title`, `snake`, `kebab`, `dot`, `constant`, `pascal`, `camel`.
```ts
toCase('hello world', 'pascal')    // 'HelloWorld'
toCase('hello world', 'camel')     // 'helloWorld'
toCase('helloWorld', 'snake')      // 'hello_world'
toCase('helloWorld', 'kebab')      // 'hello-world'
toCase('helloWorld', 'dot')        // 'hello.world'
toCase('helloWorld', 'constant')   // 'HELLO_WORLD'
toCase('helloWorld', 'title')      // 'Hello World'
toCase('helloWorld', 'sentence')   // 'Hello world'
```

#### `extendCases(custom: Record<string, CaseDefinition>): void`
Registers custom case transformers for use with `toCase`.
```ts
extendCases({
  'pipe': {
    steps: [tokenize, normalizeLower],
    format: (ctx) => ctx.words.join('|'),
  },
})
toCase('hello world', 'pipe') // 'hello|world'
```

#### `truncateText(text?: string, options?: TruncateTextOptions): string | undefined`
Truncates text with an ellipsis.
```ts
interface TruncateTextOptions {
  maxLength?: number
  ellipsis?: string
  preserveWords?: boolean
  returnUndefinedIfEmpty?: boolean
}
```
```ts
truncateText('Hello world', { maxLength: 7 })                          // 'Hell...'
truncateText('Hello world', { maxLength: 7, preserveWords: true })     // 'Hello...'
truncateText('Hello world', { maxLength: 7, ellipsis: '…' })           // 'Hell…'
truncateText('', { returnUndefinedIfEmpty: true })                     // undefined
```

---

### Number

#### `formatBytes(bytes: number, options?: FormatBytesOptions): string`
Formats a byte count to a human-readable string.
```ts
formatBytes(1536)                           // '1.50 KB'
formatBytes(1048576)                        // '1.00 MB'
formatBytes(1500, { precision: 0 })         // '1 KB'
formatBytes(1500, { base: 1000 })           // '1.50 KB'
```

#### `toBytes(input: ByteInput, options?: Partial<ByteConvertOptions>): number`
Converts a byte string to a raw number.
```ts
toBytes('2.5gb')                            // 2684354560
toBytes('1kb')                              // 1024
toBytes('1kb', { base: 1000 })             // 1000
```

#### `createByteConverter(defaultOptions?): (input, overrides?) => number`
Creates a pre-configured byte converter function.
```ts
const convert = createByteConverter({ base: 1000 })
convert('1kb')   // 1000
convert('1mb')   // 1000000
```

#### `formatNumber(value?: number | null, decimalPlaces?: number): string`
Formats a number with suffix notation (`K`, `M`, `B`, `T`).
```ts
formatNumber(1500)           // '1.5K'
formatNumber(2000000)        // '2M'
formatNumber(1234567, 2)     // '1.23M'
formatNumber(-1500)          // '-1.5K'
formatNumber(0)              // '0'
```

#### `formatCurrency(options): string`
Formats a number as a localized currency string.
```ts
formatCurrency({ amount: 1234.5, currencyCode: 'USD' })                      // '$1,234.50'
formatCurrency({ amount: 1234.5, currencyCode: 'EUR', locale: 'de-DE' })     // '1.234,50 €'
formatCurrency({ amount: 1000, currencyCode: 'KHR' })                        // '៛1,000'
```

#### `numberToWord(n: number): string`
Converts a number to English words.
```ts
numberToWord(42)          // 'Forty Two'
numberToWord(1000)        // 'One Thousand'
numberToWord(1234567.99)  // 'One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven Point Eight Nine'
```

#### `numberToWordKm(value: number): string`
Converts a number to Khmer words.
```ts
numberToWordKm(5)           // 'ប្រាំ'
numberToWordKm(1000)        // 'មួយពាន់'
numberToWordKm(1234567.89)  // 'មួយលាន ពីរសែន បីម៉ឺន បួនពាន់ ប្រាំរយ ហុកសិបប្រាំពីរ ក្បៀស ប្រាំបី ប្រាំបួន'
```

#### `toASCII(s: string): string`
Converts Khmer numerals to ASCII digits.
```ts
toASCII('០១២៣') // '0123'
```

#### `toKhmer(s: string): string`
Converts ASCII digits to Khmer numerals.
```ts
toKhmer('0123') // '០១២៣'
```

---

### Date / Time

#### `formatTime(seconds: number, options?: FormatTimeOptions): string`
Formats a duration in seconds to a human-readable string.
```ts
interface FormatTimeOptions {
  format?: 'digital' | 'long' | 'short' | 'compact'
  alwaysShowHours?: boolean
  roundingMode?: 'floor' | 'ceil' | 'round'
  padMinutes?: boolean
  separator?: string
}
```
```ts
formatTime(3661, { format: 'digital' })  // '1:01:01'
formatTime(3661, { format: 'long' })     // '1 hour 1 minute 1 second'
formatTime(3661, { format: 'short' })    // '1h 1m 1s'
formatTime(3661, { format: 'compact' })  // '1h'
```

#### `parseTime(timeString: string, separator?: string): number`
Parses a formatted time string back to seconds.
```ts
parseTime('1:01:01')   // 3661
parseTime('0:30')      // 1800
```

#### `toIso(value?: Date | string): string | undefined`
Converts a `Date` or date string to ISO 8601 format.
```ts
toIso(new Date('2024-01-15'))   // '2024-01-15T00:00:00.000Z'
toIso('2024-01-15')             // '2024-01-15T00:00:00.000Z'
toIso()                         // undefined
```

---

### URL / Path

#### `absoluteUrl(path?, options?): string`
Creates an absolute URL from a path, with optional query params and fragment.
```ts
absoluteUrl('/users', { query: { page: '2' }, baseUrl: 'https://example.com' })
// 'https://example.com/users?page=2'

absoluteUrl('/about', { fragment: 'team', baseUrl: 'https://example.com' })
// 'https://example.com/about#team'
```

#### `joinPaths(segments: (string | null | undefined)[]): string`
Joins URL path segments, handling leading/trailing slashes.
```ts
joinPaths(['api', 'users', '123'])      // 'api/users/123'
joinPaths(['/api/', '/users/', null])   // 'api/users'
```

#### `isUrl(url: string | URL): boolean`
Validates a URL, requiring `http` or `https` scheme.
```ts
isUrl('https://example.com')   // true
isUrl('ftp://example.com')     // false
isUrl('not-a-url')             // false
```

#### `isValidUrl(url: string): boolean`
Validates a URL string (any scheme).
```ts
isValidUrl('https://example.com')   // true
isValidUrl('ftp://example.com')     // true
isValidUrl('not-a-url')             // false
```

---

### Type Checking

#### `isArray(value: unknown): boolean`
```ts
isArray([1, 2, 3])   // true
isArray('hello')     // false
```

#### `isBoolean(value: unknown): boolean`
```ts
isBoolean(true)    // true
isBoolean(1)       // false
```

#### `isEmpty(value: unknown): boolean`
Returns `true` for `null`, `undefined`, `{}`, `[]`, and `""`.
```ts
isEmpty(null)   // true
isEmpty([])     // true
isEmpty({})     // true
isEmpty(0)      // false
```

#### `isFunction(value: unknown): boolean`
```ts
isFunction(() => {})   // true
isFunction('fn')       // false
```

#### `isNumber(value: unknown): boolean`
Excludes `NaN`.
```ts
isNumber(42)     // true
isNumber(NaN)    // false
```

#### `isObject(value: unknown): boolean`
Returns `true` for non-null objects (excludes arrays).
```ts
isObject({ a: 1 })   // true
isObject(null)       // false
```

#### `isString(value: unknown): boolean`
```ts
isString('hello')   // true
isString(42)        // false
```

#### `isSymbol(value: unknown): boolean`
```ts
isSymbol(Symbol('s'))   // true
isSymbol('s')           // false
```

#### `isUndef(value: unknown): boolean`
```ts
isUndef(undefined)   // true
isUndef(null)        // false
```

#### `isValidJsonString(str: string): boolean`
```ts
isValidJsonString('{"a":1}')   // true
isValidJsonString('{bad}')     // false
```

#### `isValidComponentName(name: string, options?): ComponentValidationResult`
Validates a kebab-case component name.
```ts
isValidComponentName('my-button')    // { valid: true, errors: [] }
isValidComponentName('MyButton')     // { valid: false, errors: ['...'] }
```

#### `isDev`
`true` when `NODE_ENV` is `"development"` or `"test"`.
```ts
if (isDev) console.log('debug info')
```

#### `isNavigator`
`true` when the `navigator` global is available (browser environment).
```ts
if (isNavigator) console.log(navigator.userAgent)
```

---

### CSS / Styling

#### `clsx(...args: ClassValue[]): string`
Conditionally joins class names.
```ts
clsx('foo', { bar: true, baz: false })         // 'foo bar'
clsx('a', undefined, null, 'b')                // 'a b'
clsx(['x', { y: true }])                       // 'x y'
```

#### `cn(...inputs: ClassValue[]): string`
Combines `clsx` with `tailwind-merge` to merge Tailwind classes without conflicts.
```ts
cn('px-2 py-1', 'px-4')                        // 'py-1 px-4'
cn('text-red-500', { 'text-blue-500': true })   // 'text-blue-500'
```

---

### Token / Auth

#### `getTokenExpClaim(token: string): number | null`
Extracts the `exp` claim (Unix timestamp) from a JWT without verifying the signature.
```ts
const exp = getTokenExpClaim('eyJ...')   // e.g. 1713000000
```

#### `isTokenExpired(token: string, offsetSeconds?: number): boolean`
Returns `true` if the JWT is expired, with an optional clock skew offset.
```ts
isTokenExpired('eyJ...')          // false (if still valid)
isTokenExpired('eyJ...', 300)     // true if expiring within 5 minutes
```

---

### OS Detection

#### `getOS(userAgent: string): { type: OS, label: string }`
Detects the operating system from a user-agent string.

```ts
type OS = 'windows' | 'macos' | 'linux' | 'android' | 'ios' | 'unknown'
```

```ts
getOS(navigator.userAgent)
// { type: 'macos', label: 'macOS' }

getOS('Mozilla/5.0 (Windows NT 10.0; Win64; x64)...')
// { type: 'windows', label: 'Windows' }
```

---

### vCard

#### `VCardGenerator.generate(contact: VCardContact): string`
Generates a vCard 4.0 format string.
```ts
import { VCardGenerator } from '@gmana/utils'

const vcard = VCardGenerator.generate({
  firstName: 'Sun',
  lastName: 'Sreng',
  email: 'sun.sreng123@gmail.com',
  organization: 'Gmana',
  phone: '+1-555-0100',
})
// 'BEGIN:VCARD\r\nVERSION:4.0\r\n...'
```

#### `VCardGenerator.createDownloadBlob(contact: VCardContact): Blob`
Creates a downloadable `.vcf` blob for use with a download link.
```ts
const blob = VCardGenerator.createDownloadBlob({
  firstName: 'Sun',
  lastName: 'Sreng',
  email: 'sun.sreng123@gmail.com',
})

const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'contact.vcf'
a.click()
```

---

## License

MIT