@gmana/utils
Version:
Utility functions for React and TypeScript projects.
613 lines (504 loc) • 17 kB
Markdown
# @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