UNPKG

@gmana/utils

Version:

Utility functions for React and TypeScript projects.

613 lines (504 loc) 17 kB
# @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