> = {
6 | [P in keyof T & string as Lowercase]: T[P]
7 | }
8 |
9 | type UppercasedKeys> = {
10 | [P in keyof T & string as Uppercase]: T[P]
11 | }
12 |
13 | /**
14 | * Removes (shakes out) undefined entries from an
15 | * object. Optional second argument shakes out values
16 | * by custom evaluation.
17 | */
18 | export const shake = (
19 | obj: T,
20 | filter: (value: any) => boolean = x => x === undefined
21 | ): Omit => {
22 | if (!obj) return {} as T
23 | const keys = Object.keys(obj) as (keyof T)[]
24 | return keys.reduce((acc, key) => {
25 | if (filter(obj[key])) {
26 | return acc
27 | } else {
28 | acc[key] = obj[key]
29 | return acc
30 | }
31 | }, {} as T)
32 | }
33 |
34 | /**
35 | * Map over all the keys of an object to return
36 | * a new object
37 | */
38 | export const mapKeys = <
39 | TValue,
40 | TKey extends string | number | symbol,
41 | TNewKey extends string | number | symbol
42 | >(
43 | obj: Record,
44 | mapFunc: (key: TKey, value: TValue) => TNewKey
45 | ): Record => {
46 | const keys = Object.keys(obj) as TKey[]
47 | return keys.reduce((acc, key) => {
48 | acc[mapFunc(key as TKey, obj[key])] = obj[key]
49 | return acc
50 | }, {} as Record)
51 | }
52 |
53 | /**
54 | * Map over all the keys to create a new object
55 | */
56 | export const mapValues = <
57 | TValue,
58 | TKey extends string | number | symbol,
59 | TNewValue
60 | >(
61 | obj: Record,
62 | mapFunc: (value: TValue, key: TKey) => TNewValue
63 | ): Record => {
64 | const keys = Object.keys(obj) as TKey[]
65 | return keys.reduce((acc, key) => {
66 | acc[key] = mapFunc(obj[key], key)
67 | return acc
68 | }, {} as Record)
69 | }
70 |
71 | /**
72 | * Map over all the keys to create a new object
73 | */
74 | export const mapEntries = <
75 | TKey extends string | number | symbol,
76 | TValue,
77 | TNewKey extends string | number | symbol,
78 | TNewValue
79 | >(
80 | obj: Record,
81 | toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue]
82 | ): Record => {
83 | if (!obj) return {} as Record
84 | return Object.entries(obj).reduce((acc, [key, value]) => {
85 | const [newKey, newValue] = toEntry(key as TKey, value as TValue)
86 | acc[newKey] = newValue
87 | return acc
88 | }, {} as Record)
89 | }
90 |
91 | /**
92 | * Returns an object with { [keys]: value }
93 | * inverted as { [value]: key }
94 | */
95 | export const invert = <
96 | TKey extends string | number | symbol,
97 | TValue extends string | number | symbol
98 | >(
99 | obj: Record
100 | ): Record => {
101 | if (!obj) return {} as Record
102 | const keys = Object.keys(obj) as TKey[]
103 | return keys.reduce((acc, key) => {
104 | acc[obj[key]] = key
105 | return acc
106 | }, {} as Record)
107 | }
108 |
109 | /**
110 | * Convert all keys in an object to lower case
111 | */
112 | export const lowerize = >(obj: T) =>
113 | mapKeys(obj, k => k.toLowerCase()) as LowercasedKeys
114 |
115 | /**
116 | * Convert all keys in an object to upper case
117 | */
118 | export const upperize = >(obj: T) =>
119 | mapKeys(obj, k => k.toUpperCase()) as UppercasedKeys
120 |
121 | /**
122 | * Creates a shallow copy of the given obejct/value.
123 | * @param {*} obj value to clone
124 | * @returns {*} shallow clone of the given value
125 | */
126 | export const clone = (obj: T): T => {
127 | // Primitive values do not need cloning.
128 | if (isPrimitive(obj)) {
129 | return obj
130 | }
131 |
132 | // Binding a function to an empty object creates a
133 | // copy function.
134 | if (typeof obj === 'function') {
135 | return obj.bind({})
136 | }
137 |
138 | // Access the constructor and create a new object.
139 | // This method can create an array as well.
140 | const newObj = new ((obj as object).constructor as { new (): T })()
141 |
142 | // Assign the props.
143 | Object.getOwnPropertyNames(obj).forEach(prop => {
144 | // Bypass type checking since the primitive cases
145 | // are already checked in the beginning
146 | ;(newObj as any)[prop] = (obj as any)[prop]
147 | })
148 |
149 | return newObj
150 | }
151 |
152 | /**
153 | * Convert an object to a list, mapping each entry
154 | * into a list item
155 | */
156 | export const listify = (
157 | obj: Record,
158 | toItem: (key: TKey, value: TValue) => KResult
159 | ) => {
160 | if (!obj) return []
161 | const entries = Object.entries(obj)
162 | if (entries.length === 0) return []
163 | return entries.reduce((acc, entry) => {
164 | acc.push(toItem(entry[0] as TKey, entry[1] as TValue))
165 | return acc
166 | }, [] as KResult[])
167 | }
168 |
169 | /**
170 | * Pick a list of properties from an object
171 | * into a new object
172 | */
173 | export const pick = (
174 | obj: T,
175 | keys: TKeys[]
176 | ): Pick => {
177 | if (!obj) return {} as Pick
178 | return keys.reduce((acc, key) => {
179 | if (Object.prototype.hasOwnProperty.call(obj, key)) acc[key] = obj[key]
180 | return acc
181 | }, {} as Pick)
182 | }
183 |
184 | /**
185 | * Omit a list of properties from an object
186 | * returning a new object with the properties
187 | * that remain
188 | */
189 | export const omit = (
190 | obj: T,
191 | keys: TKeys[]
192 | ): Omit => {
193 | if (!obj) return {} as Omit
194 | if (!keys || keys.length === 0) return obj as Omit
195 | return keys.reduce(
196 | (acc, key) => {
197 | // Gross, I know, it's mutating the object, but we
198 | // are allowing it in this very limited scope due
199 | // to the performance implications of an omit func.
200 | // Not a pattern or practice to use elsewhere.
201 | delete acc[key]
202 | return acc
203 | },
204 | { ...obj }
205 | )
206 | }
207 |
208 | /**
209 | * Dynamically get a nested value from an array or
210 | * object with a string.
211 | *
212 | * @example get(person, 'friends[0].name')
213 | */
214 | export const get = (
215 | value: any,
216 | path: string,
217 | defaultValue?: TDefault
218 | ): TDefault => {
219 | const segments = path.split(/[\.\[\]]/g)
220 | let current: any = value
221 | for (const key of segments) {
222 | if (current === null) return defaultValue as TDefault
223 | if (current === undefined) return defaultValue as TDefault
224 | const dequoted = key.replace(/['"]/g, '')
225 | if (dequoted.trim() === '') continue
226 | current = current[dequoted]
227 | }
228 | if (current === undefined) return defaultValue as TDefault
229 | return current
230 | }
231 |
232 | /**
233 | * Opposite of get, dynamically set a nested value into
234 | * an object using a key path. Does not modify the given
235 | * initial object.
236 | *
237 | * @example
238 | * set({}, 'name', 'ra') // => { name: 'ra' }
239 | * set({}, 'cards[0].value', 2) // => { cards: [{ value: 2 }] }
240 | */
241 | export const set = (
242 | initial: T,
243 | path: string,
244 | value: K
245 | ): T => {
246 | if (!initial) return {} as T
247 | if (!path || value === undefined) return initial
248 | const segments = path.split(/[\.\[\]]/g).filter(x => !!x.trim())
249 | const _set = (node: any) => {
250 | if (segments.length > 1) {
251 | const key = segments.shift() as string
252 | const nextIsNum = toInt(segments[0], null) === null ? false : true
253 | node[key] = node[key] === undefined ? (nextIsNum ? [] : {}) : node[key]
254 | _set(node[key])
255 | } else {
256 | node[segments[0]] = value
257 | }
258 | }
259 | // NOTE: One day, when structuredClone has more
260 | // compatability use it to clone the value
261 | // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
262 | const cloned = clone(initial)
263 | _set(cloned)
264 | return cloned
265 | }
266 |
267 | /**
268 | * Merges two objects together recursivly into a new
269 | * object applying values from right to left.
270 | * Recursion only applies to child object properties.
271 | */
272 | export const assign = >(
273 | initial: X,
274 | override: X
275 | ): X => {
276 | if (!initial || !override) return initial ?? override ?? {}
277 |
278 | return Object.entries({ ...initial, ...override }).reduce(
279 | (acc, [key, value]) => {
280 | return {
281 | ...acc,
282 | [key]: (() => {
283 | if (isObject(initial[key])) return assign(initial[key], value)
284 | // if (isArray(value)) return value.map(x => assign)
285 | return value
286 | })()
287 | }
288 | },
289 | {} as X
290 | )
291 | }
292 |
293 | /**
294 | * Get a string list of all key names that exist in
295 | * an object (deep).
296 | *
297 | * @example
298 | * keys({ name: 'ra' }) // ['name']
299 | * keys({ name: 'ra', children: [{ name: 'hathor' }] }) // ['name', 'children.0.name']
300 | */
301 | export const keys = (value: TValue): string[] => {
302 | if (!value) return []
303 | const getKeys = (nested: any, paths: string[]): string[] => {
304 | if (isObject(nested)) {
305 | return Object.entries(nested).flatMap(([k, v]) =>
306 | getKeys(v, [...paths, k])
307 | )
308 | }
309 | if (isArray(nested)) {
310 | return nested.flatMap((item, i) => getKeys(item, [...paths, `${i}`]))
311 | }
312 | return [paths.join('.')]
313 | }
314 | return getKeys(value, [])
315 | }
316 |
317 | /**
318 | * Flattens a deep object to a single demension, converting
319 | * the keys to dot notation.
320 | *
321 | * @example
322 | * crush({ name: 'ra', children: [{ name: 'hathor' }] })
323 | * // { name: 'ra', 'children.0.name': 'hathor' }
324 | */
325 | export const crush = (value: TValue): object => {
326 | if (!value) return {}
327 | return objectify(
328 | keys(value),
329 | k => k,
330 | k => get(value, k)
331 | )
332 | }
333 |
334 | /**
335 | * The opposite of crush, given an object that was
336 | * crushed into key paths and values will return
337 | * the original object reconstructed.
338 | *
339 | * @example
340 | * construct({ name: 'ra', 'children.0.name': 'hathor' })
341 | * // { name: 'ra', children: [{ name: 'hathor' }] }
342 | */
343 | export const construct = (obj: TObject): object => {
344 | if (!obj) return {}
345 | return Object.keys(obj).reduce((acc, path) => {
346 | return set(acc, path, (obj as any)[path])
347 | }, {})
348 | }
349 |
--------------------------------------------------------------------------------
/src/random.ts:
--------------------------------------------------------------------------------
1 | import { iterate } from './array'
2 |
3 | /**
4 | * Generates a random number between min and max
5 | */
6 | export const random = (min: number, max: number) => {
7 | return Math.floor(Math.random() * (max - min + 1) + min)
8 | }
9 |
10 | /**
11 | * Draw a random item from a list. Returns
12 | * null if the list is empty
13 | */
14 | export const draw = (array: readonly T[]): T | null => {
15 | const max = array.length
16 | if (max === 0) {
17 | return null
18 | }
19 | const index = random(0, max - 1)
20 | return array[index]
21 | }
22 |
23 | export const shuffle = (array: readonly T[]): T[] => {
24 | return array
25 | .map(a => ({ rand: Math.random(), value: a }))
26 | .sort((a, b) => a.rand - b.rand)
27 | .map(a => a.value)
28 | }
29 |
30 | export const uid = (length: number, specials: string = '') => {
31 | const characters =
32 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + specials
33 | return iterate(
34 | length,
35 | acc => {
36 | return acc + characters.charAt(random(0, characters.length - 1))
37 | },
38 | ''
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/series.ts:
--------------------------------------------------------------------------------
1 | import { list } from './array'
2 |
3 | /**
4 | * Creates a series object around a list of values
5 | * that should be treated with order.
6 | */
7 | export const series = (
8 | items: T[],
9 | toKey: (item: T) => string | symbol = item => `${item}`
10 | ) => {
11 | const { indexesByKey, itemsByIndex } = items.reduce(
12 | (acc, item, idx) => ({
13 | indexesByKey: {
14 | ...acc.indexesByKey,
15 | [toKey(item)]: idx
16 | },
17 | itemsByIndex: {
18 | ...acc.itemsByIndex,
19 | [idx]: item
20 | }
21 | }),
22 | {
23 | indexesByKey: {} as Record,
24 | itemsByIndex: {} as Record
25 | }
26 | )
27 | /**
28 | * Given two values in the series, returns the
29 | * value that occurs earlier in the series
30 | */
31 | const min = (a: T, b: T): T => {
32 | return indexesByKey[toKey(a)] < indexesByKey[toKey(b)] ? a : b
33 | }
34 | /**
35 | * Given two values in the series, returns the
36 | * value that occurs later in the series
37 | */
38 | const max = (a: T, b: T): T => {
39 | return indexesByKey[toKey(a)] > indexesByKey[toKey(b)] ? a : b
40 | }
41 | /**
42 | * Returns the first item from the series
43 | */
44 | const first = (): T => {
45 | return itemsByIndex[0]
46 | }
47 | /**
48 | * Returns the last item in the series
49 | */
50 | const last = (): T => {
51 | return itemsByIndex[items.length - 1]
52 | }
53 | /**
54 | * Given an item in the series returns the next item
55 | * in the series or default if the given value is
56 | * the last item in the series
57 | */
58 | const next = (current: T, defaultValue?: T): T => {
59 | return (
60 | itemsByIndex[indexesByKey[toKey(current)] + 1] ?? defaultValue ?? first()
61 | )
62 | }
63 | /**
64 | * Given an item in the series returns the previous item
65 | * in the series or default if the given value is
66 | * the first item in the series
67 | */
68 | const previous = (current: T, defaultValue?: T): T => {
69 | return (
70 | itemsByIndex[indexesByKey[toKey(current)] - 1] ?? defaultValue ?? last()
71 | )
72 | }
73 | /**
74 | * A more dynamic method than next and previous that
75 | * lets you move many times in either direction.
76 | * @example series(weekdays).spin('wednesday', 3) => 'monday'
77 | * @example series(weekdays).spin('wednesday', -3) => 'friday'
78 | */
79 | const spin = (current: T, num: number): T => {
80 | if (num === 0) return current
81 | const abs = Math.abs(num)
82 | const rel = abs > items.length ? abs % items.length : abs
83 | return list(0, rel - 1).reduce(
84 | acc => (num > 0 ? next(acc) : previous(acc)),
85 | current
86 | )
87 | }
88 | return {
89 | min,
90 | max,
91 | first,
92 | last,
93 | next,
94 | previous,
95 | spin
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/string.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Capitalize the first word of the string
3 | *
4 | * capitalize('hello') -> 'Hello'
5 | * capitalize('va va voom') -> 'Va va voom'
6 | */
7 | export const capitalize = (str: string): string => {
8 | if (!str || str.length === 0) return ''
9 | const lower = str.toLowerCase()
10 | return lower.substring(0, 1).toUpperCase() + lower.substring(1, lower.length)
11 | }
12 |
13 | /**
14 | * Formats the given string in camel case fashion
15 | *
16 | * camel('hello world') -> 'helloWorld'
17 | * camel('va va-VOOM') -> 'vaVaVoom'
18 | * camel('helloWorld') -> 'helloWorld'
19 | */
20 | export const camel = (str: string): string => {
21 | const parts =
22 | str
23 | ?.replace(/([A-Z])+/g, capitalize)
24 | ?.split(/(?=[A-Z])|[\.\-\s_]/)
25 | .map(x => x.toLowerCase()) ?? []
26 | if (parts.length === 0) return ''
27 | if (parts.length === 1) return parts[0]
28 | return parts.reduce((acc, part) => {
29 | return `${acc}${part.charAt(0).toUpperCase()}${part.slice(1)}`
30 | })
31 | }
32 |
33 | /**
34 | * Formats the given string in snake case fashion
35 | *
36 | * snake('hello world') -> 'hello_world'
37 | * snake('va va-VOOM') -> 'va_va_voom'
38 | * snake('helloWord') -> 'hello_world'
39 | */
40 | export const snake = (
41 | str: string,
42 | options?: {
43 | splitOnNumber?: boolean
44 | }
45 | ): string => {
46 | const parts =
47 | str
48 | ?.replace(/([A-Z])+/g, capitalize)
49 | .split(/(?=[A-Z])|[\.\-\s_]/)
50 | .map(x => x.toLowerCase()) ?? []
51 | if (parts.length === 0) return ''
52 | if (parts.length === 1) return parts[0]
53 | const result = parts.reduce((acc, part) => {
54 | return `${acc}_${part.toLowerCase()}`
55 | })
56 | return options?.splitOnNumber === false
57 | ? result
58 | : result.replace(/([A-Za-z]{1}[0-9]{1})/, val => `${val[0]!}_${val[1]!}`)
59 | }
60 |
61 | /**
62 | * Formats the given string in dash case fashion
63 | *
64 | * dash('hello world') -> 'hello-world'
65 | * dash('va va_VOOM') -> 'va-va-voom'
66 | * dash('helloWord') -> 'hello-word'
67 | */
68 | export const dash = (str: string): string => {
69 | const parts =
70 | str
71 | ?.replace(/([A-Z])+/g, capitalize)
72 | ?.split(/(?=[A-Z])|[\.\-\s_]/)
73 | .map(x => x.toLowerCase()) ?? []
74 | if (parts.length === 0) return ''
75 | if (parts.length === 1) return parts[0]
76 | return parts.reduce((acc, part) => {
77 | return `${acc}-${part.toLowerCase()}`
78 | })
79 | }
80 |
81 | /**
82 | * Formats the given string in pascal case fashion
83 | *
84 | * pascal('hello world') -> 'HelloWorld'
85 | * pascal('va va boom') -> 'VaVaBoom'
86 | */
87 | export const pascal = (str: string): string => {
88 | const parts = str?.split(/[\.\-\s_]/).map(x => x.toLowerCase()) ?? []
89 | if (parts.length === 0) return ''
90 | return parts.map(str => str.charAt(0).toUpperCase() + str.slice(1)).join('')
91 | }
92 |
93 | /**
94 | * Formats the given string in title case fashion
95 | *
96 | * title('hello world') -> 'Hello World'
97 | * title('va_va_boom') -> 'Va Va Boom'
98 | * title('root-hook') -> 'Root Hook'
99 | * title('queryItems') -> 'Query Items'
100 | */
101 | export const title = (str: string | null | undefined): string => {
102 | if (!str) return ''
103 | return str
104 | .split(/(?=[A-Z])|[\.\-\s_]/)
105 | .map(s => s.trim())
106 | .filter(s => !!s)
107 | .map(s => capitalize(s.toLowerCase()))
108 | .join(' ')
109 | }
110 |
111 | /**
112 | * template is used to replace data by name in template strings.
113 | * The default expression looks for {{name}} to identify names.
114 | *
115 | * Ex. template('Hello, {{name}}', { name: 'ray' })
116 | * Ex. template('Hello, ', { name: 'ray' }, /<(.+?)>/g)
117 | */
118 | export const template = (
119 | str: string,
120 | data: Record,
121 | regex = /\{\{(.+?)\}\}/g
122 | ) => {
123 | return Array.from(str.matchAll(regex)).reduce((acc, match) => {
124 | return acc.replace(match[0], data[match[1]])
125 | }, str)
126 | }
127 |
128 | /**
129 | * Trims all prefix and suffix characters from the given
130 | * string. Like the builtin trim function but accepts
131 | * other characters you would like to trim and trims
132 | * multiple characters.
133 | *
134 | * ```typescript
135 | * trim(' hello ') // => 'hello'
136 | * trim('__hello__', '_') // => 'hello'
137 | * trim('/repos/:owner/:repo/', '/') // => 'repos/:owner/:repo'
138 | * trim('222222__hello__1111111', '12_') // => 'hello'
139 | * ```
140 | */
141 | export const trim = (
142 | str: string | null | undefined,
143 | charsToTrim: string = ' '
144 | ) => {
145 | if (!str) return ''
146 | const toTrim = charsToTrim.replace(/[\W]{1}/g, '\\$&')
147 | const regex = new RegExp(`^[${toTrim}]+|[${toTrim}]+$`, 'g')
148 | return str.replace(regex, '')
149 | }
150 |
--------------------------------------------------------------------------------
/src/tests/curry.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import * as _ from '..'
3 | import type { DebounceFunction } from '../curry'
4 |
5 | describe('curry module', () => {
6 | describe('compose function', () => {
7 | test('composes functions', () => {
8 | const useZero = (fn: (num: number) => number) => () => fn(0)
9 | const objectize =
10 | (fn: (obj: { num: number }) => number) => (num: number) =>
11 | fn({ num })
12 | const increment =
13 | (fn: (arg: { num: number }) => number) =>
14 | ({ num }: { num: number }) =>
15 | fn({ num: num + 1 })
16 | const returnArg = (arg: 'num') => (args: { num: number }) => args[arg]
17 |
18 | const composed = _.compose(
19 | useZero,
20 | objectize,
21 | increment,
22 | increment,
23 | returnArg('num')
24 | )
25 |
26 | const decomposed = useZero(
27 | objectize(increment(increment(returnArg('num'))))
28 | )
29 |
30 | const expected = decomposed()
31 | const result = composed()
32 |
33 | assert.equal(result, expected)
34 | assert.equal(result, 2)
35 | })
36 | test('composes async function', async () => {
37 | const useZero = (fn: (num: number) => Promise) => async () =>
38 | fn(0)
39 | const objectize =
40 | (fn: (obj: { num: number }) => Promise) =>
41 | async (num: number) =>
42 | fn({ num })
43 | const increment =
44 | (fn: (arg: { num: number }) => Promise) =>
45 | async ({ num }: { num: number }) =>
46 | fn({ num: num + 1 })
47 | const returnArg = (arg: 'num') => async (args: { num: number }) =>
48 | args[arg]
49 |
50 | const composed = _.compose(
51 | useZero,
52 | objectize,
53 | increment,
54 | increment,
55 | returnArg('num')
56 | )
57 |
58 | const decomposed = useZero(
59 | objectize(increment(increment(returnArg('num'))))
60 | )
61 |
62 | const expected = await decomposed()
63 | const result = await composed()
64 |
65 | assert.equal(result, expected)
66 | })
67 | test('composes function type overloads', () => {
68 | const useZero = (fn: (num: number) => number) => () => fn(0)
69 | const objectize =
70 | (fn: (obj: { num: number }) => number) => (num: number) =>
71 | fn({ num })
72 | const increment =
73 | (fn: (arg: { num: number }) => number) =>
74 | ({ num }: { num: number }) =>
75 | fn({ num: num + 1 })
76 | const returnArg = (arg: 'num') => (args: { num: number }) => args[arg]
77 | const returnNum = () => (num: number) => num
78 |
79 | assert.equal(_.compose(useZero, returnNum())(), 0)
80 |
81 | assert.equal(_.compose(useZero, objectize, returnArg('num'))(), 0)
82 |
83 | assert.equal(
84 | _.compose(useZero, objectize, increment, returnArg('num'))(),
85 | 1
86 | )
87 |
88 | assert.equal(
89 | _.compose(useZero, objectize, increment, increment, returnArg('num'))(),
90 | 2
91 | )
92 |
93 | assert.equal(
94 | _.compose(
95 | useZero,
96 | objectize,
97 | increment,
98 | increment,
99 | increment,
100 | returnArg('num')
101 | )(),
102 | 3
103 | )
104 |
105 | assert.equal(
106 | _.compose(
107 | useZero,
108 | objectize,
109 | increment,
110 | increment,
111 | increment,
112 | increment,
113 | returnArg('num')
114 | )(),
115 | 4
116 | )
117 |
118 | assert.equal(
119 | _.compose(
120 | useZero,
121 | objectize,
122 | increment,
123 | increment,
124 | increment,
125 | increment,
126 | increment,
127 | returnArg('num')
128 | )(),
129 | 5
130 | )
131 |
132 | assert.equal(
133 | _.compose(
134 | useZero,
135 | objectize,
136 | increment,
137 | increment,
138 | increment,
139 | increment,
140 | increment,
141 | increment,
142 | returnArg('num')
143 | )(),
144 | 6
145 | )
146 |
147 | assert.equal(
148 | _.compose(
149 | useZero,
150 | objectize,
151 | increment,
152 | increment,
153 | increment,
154 | increment,
155 | increment,
156 | increment,
157 | increment,
158 | returnArg('num')
159 | )(),
160 | 7
161 | )
162 | })
163 | })
164 |
165 | describe('partial function', () => {
166 | test('passes single args', () => {
167 | const add = (a: number, b: number) => a + b
168 | const expected = 20
169 | const partialed = _.partial(add, 10)
170 | const result = partialed(10)
171 | assert.equal(result, expected)
172 | })
173 | test('passes many args', () => {
174 | const add = (...nums: number[]) => nums.reduce((a, b) => a + b, 0)
175 | const expected = 10
176 | const result = _.partial(add, 2, 2, 2)(2, 2)
177 | assert.equal(result, expected)
178 | })
179 | })
180 |
181 | describe('partob function', () => {
182 | test('partob passes single args', () => {
183 | const add = ({ a, b }: { a: number; b: number }) => a + b
184 | const expected = 20
185 | const result = _.partob(add, { a: 10 })({ b: 10 })
186 | assert.equal(result, expected)
187 | })
188 | test('partob overrides inital with later', () => {
189 | const add = ({ a, b }: { a: number; b: number }) => a + b
190 | const expected = 15
191 | const result = _.partob(add, { a: 10 })({ a: 5, b: 10 } as any)
192 | assert.equal(result, expected)
193 | })
194 | })
195 |
196 | describe('chain function', () => {
197 | test('calls all given functions', () => {
198 | const genesis = (num: number, name: string) => 0
199 | const addFive = (num: number) => num + 5
200 | const twoX = (num: number) => num * 2
201 | const func = _.chain(genesis, addFive, twoX)
202 | const result = func(0, '')
203 | assert.equal(result, 10)
204 | })
205 |
206 | test('calls add(1), then addFive, then twoX functions by 1', () => {
207 | const add = (y: number) => (x: number) => x + y
208 | const addFive = add(5)
209 | const twoX = (num: number) => num * 2
210 | const func = _.chain(add(1), addFive, twoX)
211 | const result = func(1)
212 | assert.equal(result, 14)
213 | })
214 |
215 | test('calls add(2), then addFive, then twoX, then repeatX functions by 1', () => {
216 | const add = (y: number) => (x: number) => x + y
217 | const addFive = add(5)
218 | const twoX = (num: number) => num * 2
219 | const repeatX = (num: number) => 'X'.repeat(num)
220 | const func = _.chain(add(2), addFive, twoX, repeatX)
221 | const result = func(1)
222 | assert.equal(result, 'XXXXXXXXXXXXXXXX')
223 | })
224 |
225 | test('calls addFive, then add(2), then twoX, then repeatX functions by 1', () => {
226 | const add = (y: number) => (x: number) => x + y
227 | const addFive = add(5)
228 | const twoX = (num: number) => num * 2
229 | const repeatX = (num: number) => 'X'.repeat(num)
230 | const func = _.chain(addFive, add(2), twoX, repeatX)
231 | const result = func(1)
232 | assert.equal(result, 'XXXXXXXXXXXXXXXX')
233 | })
234 |
235 | test('calls getName, then upperCase functions as a mapper for User[]', () => {
236 | type User = { id: number; name: string }
237 | const users: User[] = [
238 | { id: 1, name: 'John Doe' },
239 | { id: 2, name: 'John Smith' },
240 | { id: 3, name: 'John Wick' }
241 | ]
242 | const getName = (item: T) => item.name
243 | const upperCase: (x: string) => Uppercase = (text: string) =>
244 | text.toUpperCase() as Uppercase
245 |
246 | const getUpperName = _.chain(getName, upperCase)
247 | const result = users.map(getUpperName)
248 | assert.deepEqual(result, ['JOHN DOE', 'JOHN SMITH', 'JOHN WICK'])
249 | })
250 | })
251 |
252 | describe('proxied function', () => {
253 | test('returns proxy that calls callback function', () => {
254 | const handler = (propertyName: string) => {
255 | if (propertyName === 'x') return 2
256 | if (propertyName === 'getName') return () => 'radash'
257 | return undefined
258 | }
259 | const proxy = _.proxied(handler) as any
260 | assert.equal(proxy.x, 2)
261 | assert.equal(proxy.getName(), 'radash')
262 | assert.isUndefined(proxy.nil)
263 | })
264 | })
265 |
266 | describe('memo function', () => {
267 | test('only executes function once', () => {
268 | const func = _.memo(() => new Date().getTime())
269 | const resultA = func()
270 | const resultB = func()
271 | assert.equal(resultA, resultB)
272 | })
273 | test('uses key to identify unique calls', () => {
274 | const func = _.memo(
275 | (arg: { user: { id: string } }) => {
276 | const ts = new Date().getTime()
277 | return `${ts}::${arg.user.id}`
278 | },
279 | {
280 | key: arg => arg.user.id
281 | }
282 | )
283 | const resultA = func({ user: { id: 'alpha' } })
284 | const resultB = func({ user: { id: 'beta' } })
285 | const resultA2 = func({ user: { id: 'alpha' } })
286 | assert.equal(resultA, resultA2)
287 | assert.notEqual(resultB, resultA)
288 | })
289 | test('calls function again when first value expires', async () => {
290 | const func = _.memo(() => new Date().getTime(), {
291 | ttl: 1
292 | })
293 | const resultA = func()
294 | await new Promise(res => setTimeout(res, 100))
295 | const resultB = func()
296 | assert.notEqual(resultA, resultB)
297 | })
298 | test('does not call function again when first value has not expired', async () => {
299 | const func = _.memo(() => new Date().getTime(), {
300 | ttl: 1000
301 | })
302 | const resultA = func()
303 | await new Promise(res => setTimeout(res, 100))
304 | const resultB = func()
305 | assert.equal(resultA, resultB)
306 | })
307 | })
308 |
309 | describe('debounce function', () => {
310 | let func: DebounceFunction
311 | const mockFunc = jest.fn()
312 | const runFunc3Times = () => {
313 | func()
314 | func()
315 | func()
316 | }
317 |
318 | beforeEach(() => {
319 | func = _.debounce({ delay: 600 }, mockFunc)
320 | })
321 |
322 | afterEach(() => {
323 | jest.clearAllMocks()
324 | })
325 |
326 | test('only executes once when called rapidly', async () => {
327 | runFunc3Times()
328 | expect(mockFunc).toHaveBeenCalledTimes(0)
329 | await _.sleep(610)
330 | expect(mockFunc).toHaveBeenCalledTimes(1)
331 | })
332 |
333 | test('does not debounce after cancel is called', () => {
334 | runFunc3Times()
335 | expect(mockFunc).toHaveBeenCalledTimes(0)
336 | func.cancel()
337 | runFunc3Times()
338 | expect(mockFunc).toHaveBeenCalledTimes(3)
339 | runFunc3Times()
340 | expect(mockFunc).toHaveBeenCalledTimes(6)
341 | })
342 |
343 | test('executes the function immediately when the flush method is called', () => {
344 | func.flush()
345 | expect(mockFunc).toHaveBeenCalledTimes(1)
346 | })
347 |
348 | test('continues to debounce after flush is called', async () => {
349 | runFunc3Times()
350 | expect(mockFunc).toHaveBeenCalledTimes(0)
351 | func.flush()
352 | expect(mockFunc).toHaveBeenCalledTimes(1)
353 | func()
354 | expect(mockFunc).toHaveBeenCalledTimes(1)
355 | await _.sleep(610)
356 | expect(mockFunc).toHaveBeenCalledTimes(2)
357 | func.flush()
358 | expect(mockFunc).toHaveBeenCalledTimes(3)
359 | })
360 |
361 | test('cancels all pending invocations when the cancel method is called', async () => {
362 | const results: boolean[] = []
363 | func()
364 | results.push(func.isPending())
365 | results.push(func.isPending())
366 | await _.sleep(610)
367 | results.push(func.isPending())
368 | func()
369 | results.push(func.isPending())
370 | await _.sleep(610)
371 | results.push(func.isPending())
372 | assert.deepEqual(results, [true, true, false, true, false])
373 | })
374 |
375 | test('returns if there is any pending invocation when the pending method is called', async () => {
376 | func()
377 | func.cancel()
378 | await _.sleep(610)
379 | expect(mockFunc).toHaveBeenCalledTimes(0)
380 | })
381 | })
382 |
383 | describe('throttle function', () => {
384 | test('throttles!', async () => {
385 | let calls = 0
386 | const func = _.throttle({ interval: 600 }, () => calls++)
387 | func()
388 | func()
389 | func()
390 | assert.equal(calls, 1)
391 | await _.sleep(610)
392 | func()
393 | func()
394 | func()
395 | assert.equal(calls, 2)
396 | })
397 |
398 | test('returns if the throttle is active', async () => {
399 | const results = []
400 | const func = _.throttle({ interval: 600 }, () => {})
401 | results.push(func.isThrottled())
402 | func()
403 | results.push(func.isThrottled())
404 | func()
405 | results.push(func.isThrottled())
406 | func()
407 | results.push(func.isThrottled())
408 | await _.sleep(610)
409 | results.push(func.isThrottled())
410 | assert.deepEqual(results, [false, true, true, true, false])
411 | })
412 | })
413 | })
414 |
415 | describe('callable function', () => {
416 | test('makes object callable', async () => {
417 | const request = {
418 | source: 'client',
419 | body: 'ford',
420 | doors: 2
421 | }
422 |
423 | const call = _.callable(request, self => (id: string) => ({ ...self, id }))
424 |
425 | expect(call.source).toBe('client')
426 | expect(call.body).toBe('ford')
427 | expect(call.doors).toBe(2)
428 | const s = call('23')
429 | expect(s.doors).toBe(2)
430 | expect(s.id).toBe('23')
431 |
432 | call.doors = 4
433 | expect(call.doors).toBe(4)
434 | const x = call('9')
435 | expect(x.doors).toBe(4)
436 | expect(x.id).toBe('9')
437 | })
438 | })
439 |
--------------------------------------------------------------------------------
/src/tests/number.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import * as _ from '..'
3 |
4 | describe('number module', () => {
5 | describe('inRange function', () => {
6 | test('handles nullish values', () => {
7 | assert.strictEqual(_.inRange(0, 1, null as any), false)
8 | assert.strictEqual(_.inRange(0, null as any, 1), false)
9 | assert.strictEqual(_.inRange(null as any, 0, 1), false)
10 | assert.strictEqual(_.inRange(0, undefined as any, 1), false)
11 | assert.strictEqual(_.inRange(undefined as any, 0, 1), false)
12 |
13 | assert.strictEqual(_.inRange(0, 1, undefined as any), true)
14 | })
15 | test('handles bad input', () => {
16 | const result = _.inRange(0, 1, {} as any)
17 | assert.strictEqual(result, false)
18 | })
19 | test('computes correctly', () => {
20 | assert.strictEqual(_.inRange(10, 0, 5), false)
21 | assert.strictEqual(_.inRange(10, 0, 20), true)
22 | assert.strictEqual(_.inRange(-10, 0, -20), true)
23 | assert.strictEqual(_.inRange(9.99, 0, 10), true)
24 | assert.strictEqual(_.inRange(Math.PI, 0, 3.15), true)
25 | })
26 | test('handles the different syntax of number type', () => {
27 | assert.strictEqual(_.inRange(0, -1, 1), true)
28 | assert.strictEqual(_.inRange(Number(0), -1, 1), true)
29 | assert.strictEqual(_.inRange(+'0', -1, 1), true)
30 | })
31 | test('handles two params', () => {
32 | assert.strictEqual(_.inRange(1, 2), true)
33 | assert.strictEqual(_.inRange(1.2, 2), true)
34 | assert.strictEqual(_.inRange(2, 1), false)
35 | assert.strictEqual(_.inRange(2, 2), false)
36 | assert.strictEqual(_.inRange(3.2, 2), false)
37 | assert.strictEqual(_.inRange(-1, 1), false)
38 | assert.strictEqual(_.inRange(-1, -10), true)
39 | })
40 | test('handles the exclusive end of the range', () => {
41 | assert.strictEqual(_.inRange(1, 0, 1), false)
42 | assert.strictEqual(_.inRange(10.0, 0, 10), false)
43 | })
44 | test('handles the inclusive start of the range', () => {
45 | assert.strictEqual(_.inRange(0, 0, 1), true)
46 | assert.strictEqual(_.inRange(10.0, 10, 20), true)
47 | })
48 | })
49 |
50 | describe('toFloat function', () => {
51 | test('handles null', () => {
52 | const result = _.toFloat(null)
53 | assert.strictEqual(result, 0.0)
54 | })
55 | test('handles undefined', () => {
56 | const result = _.toFloat(undefined)
57 | assert.strictEqual(result, 0.0)
58 | })
59 | test('uses null default', () => {
60 | const result = _.toFloat('x', null)
61 | assert.strictEqual(result, null)
62 | })
63 | test('handles bad input', () => {
64 | const result = _.toFloat({})
65 | assert.strictEqual(result, 0.0)
66 | })
67 | test('converts 20.00 correctly', () => {
68 | const result = _.toFloat('20.00')
69 | assert.strictEqual(result, 20.0)
70 | })
71 | })
72 |
73 | describe('toInt function', () => {
74 | test('handles null', () => {
75 | const result = _.toInt(null)
76 | assert.strictEqual(result, 0)
77 | })
78 | test('uses null default', () => {
79 | const result = _.toInt('x', null)
80 | assert.strictEqual(result, null)
81 | })
82 | test('handles undefined', () => {
83 | const result = _.toInt(undefined)
84 | assert.strictEqual(result, 0)
85 | })
86 | test('handles bad input', () => {
87 | const result = _.toInt({})
88 | assert.strictEqual(result, 0)
89 | })
90 | test('converts 20 correctly', () => {
91 | const result = _.toInt('20')
92 | assert.strictEqual(result, 20)
93 | })
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/src/tests/random.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import * as _ from '..'
3 |
4 | describe('random module', () => {
5 | describe('random function', () => {
6 | test('returns a number', () => {
7 | const result = _.random(0, 100)
8 | assert.isAtLeast(result, 0)
9 | assert.isAtMost(result, 100)
10 | })
11 | })
12 |
13 | describe('uid function', () => {
14 | test('generates the correct length string', () => {
15 | const result = _.uid(10)
16 | assert.equal(result.length, 10)
17 | })
18 | /**
19 | * @warning This is potentially a flaky test.
20 | * We're trying to assert that given additional
21 | * special chars our function will include them
22 | * in the random selection process to generate the
23 | * uid. However, there is always a small chance that
24 | * one is never selected. If the test is flaky, increase
25 | * the size of the uid and/or the number of underscores
26 | * in the special char addition.
27 | */
28 | test('uid generates string including special', () => {
29 | const result = _.uid(
30 | 300,
31 | '________________________________________________________________'
32 | )
33 | assert.include(result, '_')
34 | })
35 | })
36 |
37 | describe('shuffle function', () => {
38 | test('returns list with same number of items', () => {
39 | const list = [1, 2, 3, 4, 5]
40 | const result = _.shuffle(list)
41 | assert.equal(list.length, result.length)
42 | })
43 | test('returns list with same value', () => {
44 | const list = [1, 2, 3, 4, 5]
45 | const totalBefore = _.sum(list)
46 | const result = _.shuffle(list)
47 | const totalAfter = _.sum(result)
48 | assert.equal(totalBefore, totalAfter)
49 | })
50 | test('returns copy of list without mutatuing input', () => {
51 | const list = [1, 2, 3, 4, 5]
52 | const result = _.shuffle(list)
53 | assert.notEqual(list, result)
54 | assert.deepEqual(list, [1, 2, 3, 4, 5])
55 | })
56 | })
57 |
58 | describe('draw function', () => {
59 | test('returns a string from the list', () => {
60 | const letters = 'abcde'
61 | const result = _.draw(letters.split(''))
62 | assert.include(letters, result!)
63 | })
64 | test('returns a item from the list', () => {
65 | const list = [
66 | { id: 'a', word: 'hello' },
67 | { id: 'b', word: 'oh' },
68 | { id: 'c', word: 'yolo' }
69 | ]
70 | const result = _.draw(list)
71 | assert.include('abc', result!.id)
72 | })
73 | test('returns null given empty input', () => {
74 | const list: unknown[] = []
75 | const result = _.draw(list)
76 | assert.isNull(result)
77 | })
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/src/tests/series.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import * as _ from '..'
3 |
4 | describe('series module', () => {
5 | type Weekday = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday'
6 | const sut = _.series([
7 | 'monday',
8 | 'tuesday',
9 | 'wednesday',
10 | 'thursday',
11 | 'friday'
12 | ])
13 |
14 | describe('min function', () => {
15 | test('correctly returns min', () => {
16 | const result = sut.min('monday', 'tuesday')
17 | assert.equal(result, 'monday')
18 | })
19 | test('correctly returns min when second arg', () => {
20 | const result = sut.min('tuesday', 'monday')
21 | assert.equal(result, 'monday')
22 | })
23 | })
24 |
25 | describe('max function', () => {
26 | test('correctly returns max', () => {
27 | const result = sut.max('thursday', 'tuesday')
28 | assert.equal(result, 'thursday')
29 | })
30 | test('correctly returns max when second arg', () => {
31 | const result = sut.max('tuesday', 'thursday')
32 | assert.equal(result, 'thursday')
33 | })
34 | })
35 |
36 | describe('first function', () => {
37 | test('returns first item', () => {
38 | const result = sut.first()
39 | assert.equal(result, 'monday')
40 | })
41 | })
42 |
43 | describe('last function', () => {
44 | test('returns last item', () => {
45 | const result = sut.last()
46 | assert.equal(result, 'friday')
47 | })
48 | })
49 |
50 | describe('next function', () => {
51 | test('returns next item', () => {
52 | const result = sut.next('wednesday')
53 | assert.equal(result, 'thursday')
54 | })
55 | test('returns first given last exhausted', () => {
56 | const result = sut.next('friday')
57 | assert.equal(result, 'monday')
58 | })
59 | test('returns the given default when the last is exhausted', () => {
60 | const result = sut.next('friday', 'wednesday')
61 | assert.equal(result, 'wednesday')
62 | })
63 | })
64 |
65 | describe('previous function', () => {
66 | test('returns previous item', () => {
67 | const result = sut.previous('wednesday')
68 | assert.equal(result, 'tuesday')
69 | })
70 | test('returns last given first exhausted', () => {
71 | const result = sut.previous('monday')
72 | assert.equal(result, 'friday')
73 | })
74 | test('returns the given default when the first is exhausted', () => {
75 | const result = sut.previous('monday', 'wednesday')
76 | assert.equal(result, 'wednesday')
77 | })
78 | })
79 |
80 | describe('spin function', () => {
81 | test('returns current given zero', () => {
82 | const result = sut.spin('wednesday', 0)
83 | assert.equal(result, 'wednesday')
84 | })
85 | test('returns friday given -3 starting at wednesday', () => {
86 | const result = sut.spin('wednesday', -3)
87 | assert.equal(result, 'friday')
88 | })
89 | test('returns monday given 3 starting at wednesday', () => {
90 | const result = sut.spin('wednesday', 3)
91 | assert.equal(result, 'monday')
92 | })
93 | test('returns monday given 13 starting at wednesday', () => {
94 | const result = sut.spin('wednesday', 13)
95 | assert.equal(result, 'monday')
96 | })
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/tests/string.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai'
2 | import * as _ from '..'
3 |
4 | describe('string module', () => {
5 | describe('camel function', () => {
6 | test('returns correctly cased string', () => {
7 | const result = _.camel('hello world')
8 | assert.equal(result, 'helloWorld')
9 | })
10 | test('returns single word', () => {
11 | const result = _.camel('hello')
12 | assert.equal(result, 'hello')
13 | })
14 | test('returns empty string for empty input', () => {
15 | const result = _.camel(null as any)
16 | assert.equal(result, '')
17 | })
18 | test('a word in camel case should remain in camel case', () => {
19 | const result = _.camel('helloWorld')
20 | assert.equal(result, 'helloWorld')
21 | })
22 | })
23 |
24 | describe('camelCase function', () => {
25 | test('returns non alphanumerics with -space and capital', () => {
26 | const result = _.camel('Exobase Starter_flash AND-go')
27 | assert.equal(result, 'exobaseStarterFlashAndGo')
28 | })
29 | })
30 |
31 | describe('snake function', () => {
32 | test('returns correctly cased string', () => {
33 | const result = _.snake('hello world')
34 | assert.equal(result, 'hello_world')
35 | })
36 | test('must handle strings that are camelCase', () => {
37 | const result = _.snake('helloWorld')
38 | assert.equal(result, 'hello_world')
39 | })
40 | test('must handle strings that are dash', () => {
41 | const result = _.snake('hello-world')
42 | assert.equal(result, 'hello_world')
43 | })
44 | test('splits numbers that are next to letters', () => {
45 | const result = _.snake('hello-world12_19-bye')
46 | assert.equal(result, 'hello_world_12_19_bye')
47 | })
48 | test('does not split numbers when flag is set to false', () => {
49 | const result = _.snake('hello-world12_19-bye', {
50 | splitOnNumber: false
51 | })
52 | assert.equal(result, 'hello_world12_19_bye')
53 | })
54 | test('returns single word', () => {
55 | const result = _.snake('hello')
56 | assert.equal(result, 'hello')
57 | })
58 | test('returns empty string for empty input', () => {
59 | const result = _.snake(null as any)
60 | assert.equal(result, '')
61 | })
62 | })
63 |
64 | describe('snakeCase function', () => {
65 | test('returns non alphanumerics with _', () => {
66 | const result = _.snake('Exobase Starter_flash AND-go')
67 | assert.equal(result, 'exobase_starter_flash_and_go')
68 | })
69 | })
70 |
71 | describe('dash function', () => {
72 | test('returns correctly cased string', () => {
73 | const result = _.dash('hello world')
74 | assert.equal(result, 'hello-world')
75 | })
76 | test('returns single word', () => {
77 | const result = _.dash('hello')
78 | assert.equal(result, 'hello')
79 | })
80 | test('returns empty string for empty input', () => {
81 | const result = _.dash(null as any)
82 | assert.equal(result, '')
83 | })
84 | test('must handle strings that are camelCase', () => {
85 | const result = _.dash('helloWorld')
86 | assert.equal(result, 'hello-world')
87 | })
88 | test('must handle strings that are dash', () => {
89 | const result = _.dash('hello-world')
90 | assert.equal(result, 'hello-world')
91 | })
92 | })
93 |
94 | describe('dashCase function', () => {
95 | test('returns non alphanumerics with -', () => {
96 | const result = _.dash('Exobase Starter_flash AND-go')
97 | assert.equal(result, 'exobase-starter-flash-and-go')
98 | })
99 | })
100 |
101 | describe('template function', () => {
102 | test('replaces all occurrences', () => {
103 | const tmp = `
104 | Hello my name is {{name}}. I am a {{type}}.
105 | Not sure why I am {{reason}}.
106 |
107 | Thank You - {{name}}
108 | `
109 | const data = {
110 | name: 'Ray',
111 | type: 'template',
112 | reason: 'so beautiful'
113 | }
114 |
115 | const result = _.template(tmp, data)
116 | const expected = `
117 | Hello my name is ${data.name}. I am a ${data.type}.
118 | Not sure why I am ${data.reason}.
119 |
120 | Thank You - ${data.name}
121 | `
122 |
123 | assert.equal(result, expected)
124 | })
125 |
126 | test('replaces all occurrences given template', () => {
127 | const tmp = `Hello .`
128 | const data = {
129 | name: 'Ray'
130 | }
131 |
132 | const result = _.template(tmp, data, /<(.+?)>/g)
133 | assert.equal(result, `Hello ${data.name}.`)
134 | })
135 | })
136 |
137 | describe('capitalize function', () => {
138 | test('handles null', () => {
139 | const result = _.capitalize(null as any)
140 | assert.equal(result, '')
141 | })
142 | test('converts hello as Hello', () => {
143 | const result = _.capitalize('hello')
144 | assert.equal(result, 'Hello')
145 | })
146 | test('converts hello Bob as Hello bob', () => {
147 | const result = _.capitalize('hello Bob')
148 | assert.equal(result, 'Hello bob')
149 | })
150 | })
151 |
152 | describe('pascal function', () => {
153 | test('returns non alphanumerics in pascal', () => {
154 | const result = _.pascal('Exobase Starter_flash AND-go')
155 | assert.equal(result, 'ExobaseStarterFlashAndGo')
156 | })
157 | test('returns single word', () => {
158 | const result = _.pascal('hello')
159 | assert.equal(result, 'Hello')
160 | })
161 | test('returns empty string for empty input', () => {
162 | const result = _.pascal(null as any)
163 | assert.equal(result, '')
164 | })
165 | })
166 |
167 | describe('title function', () => {
168 | test('returns input formatted in title case', () => {
169 | assert.equal(_.title('hello world'), 'Hello World')
170 | assert.equal(_.title('va_va_boom'), 'Va Va Boom')
171 | assert.equal(_.title('root-hook - ok!'), 'Root Hook Ok!')
172 | assert.equal(_.title('queryItems'), 'Query Items')
173 | assert.equal(
174 | _.title('queryAllItems-in_Database'),
175 | 'Query All Items In Database'
176 | )
177 | })
178 | test('returns empty string for bad input', () => {
179 | assert.equal(_.title(null), '')
180 | assert.equal(_.title(undefined), '')
181 | })
182 | })
183 |
184 | describe('trim function', () => {
185 | test('handles bad input', () => {
186 | assert.equal(_.trim(null), '')
187 | assert.equal(_.trim(undefined), '')
188 | })
189 | test('returns input string correctly trimmed', () => {
190 | assert.equal(_.trim('\n\n\t\nhello\n\t \n', '\n\t '), 'hello')
191 | assert.equal(_.trim('hello', 'x'), 'hello')
192 | assert.equal(_.trim(' hello '), 'hello')
193 | assert.equal(_.trim(' __hello__ ', '_'), ' __hello__ ')
194 | assert.equal(_.trim('__hello__', '_'), 'hello')
195 | assert.equal(_.trim('//repos////', '/'), 'repos')
196 | assert.equal(_.trim('/repos/:owner/:repo/', '/'), 'repos/:owner/:repo')
197 | })
198 |
199 | test('handles when char to trim is special case in regex', () => {
200 | assert.equal(_.trim('_- hello_- ', '_- '), 'hello')
201 | })
202 | })
203 | })
204 |
--------------------------------------------------------------------------------
/src/typed.ts:
--------------------------------------------------------------------------------
1 | export const isSymbol = (value: any): value is symbol => {
2 | return !!value && value.constructor === Symbol
3 | }
4 |
5 | export const isArray = Array.isArray
6 |
7 | export const isObject = (value: any): value is object => {
8 | return !!value && value.constructor === Object
9 | }
10 |
11 | /**
12 | * Checks if the given value is primitive.
13 | *
14 | * Primitive Types: number , string , boolean , symbol, bigint, undefined, null
15 | *
16 | * @param {*} value value to check
17 | * @returns {boolean} result
18 | */
19 | export const isPrimitive = (value: any): boolean => {
20 | return (
21 | value === undefined ||
22 | value === null ||
23 | (typeof value !== 'object' && typeof value !== 'function')
24 | )
25 | }
26 |
27 | export const isFunction = (value: any): value is Function => {
28 | return !!(value && value.constructor && value.call && value.apply)
29 | }
30 |
31 | export const isString = (value: any): value is string => {
32 | return typeof value === 'string' || value instanceof String
33 | }
34 |
35 | export const isInt = (value: any): value is number => {
36 | return isNumber(value) && value % 1 === 0
37 | }
38 |
39 | export const isFloat = (value: any): value is number => {
40 | return isNumber(value) && value % 1 !== 0
41 | }
42 |
43 | export const isNumber = (value: any): value is number => {
44 | try {
45 | return Number(value) === value
46 | } catch {
47 | return false
48 | }
49 | }
50 |
51 | export const isDate = (value: any): value is Date => {
52 | return Object.prototype.toString.call(value) === '[object Date]'
53 | }
54 |
55 | /**
56 | * This is really a _best guess_ promise checking. You
57 | * should probably use Promise.resolve(value) to be 100%
58 | * sure you're handling it correctly.
59 | */
60 | export const isPromise = (value: any): value is Promise => {
61 | if (!value) return false
62 | if (!value.then) return false
63 | if (!isFunction(value.then)) return false
64 | return true
65 | }
66 |
67 | export const isEmpty = (value: any) => {
68 | if (value === true || value === false) return true
69 | if (value === null || value === undefined) return true
70 | if (isNumber(value)) return value === 0
71 | if (isDate(value)) return isNaN(value.getTime())
72 | if (isFunction(value)) return false
73 | if (isSymbol(value)) return false
74 | const length = (value as any).length
75 | if (isNumber(length)) return length === 0
76 | const size = (value as any).size
77 | if (isNumber(size)) return size === 0
78 | const keys = Object.keys(value).length
79 | return keys === 0
80 | }
81 |
82 | export const isEqual = (x: TType, y: TType): boolean => {
83 | if (Object.is(x, y)) return true
84 | if (x instanceof Date && y instanceof Date) {
85 | return x.getTime() === y.getTime()
86 | }
87 | if (x instanceof RegExp && y instanceof RegExp) {
88 | return x.toString() === y.toString()
89 | }
90 | if (
91 | typeof x !== 'object' ||
92 | x === null ||
93 | typeof y !== 'object' ||
94 | y === null
95 | ) {
96 | return false
97 | }
98 | const keysX = Reflect.ownKeys(x as unknown as object) as (keyof typeof x)[]
99 | const keysY = Reflect.ownKeys(y as unknown as object)
100 | if (keysX.length !== keysY.length) return false
101 | for (let i = 0; i < keysX.length; i++) {
102 | if (!Reflect.has(y as unknown as object, keysX[i])) return false
103 | if (!isEqual(x[keysX[i]], y[keysX[i]])) return false
104 | }
105 | return true
106 | }
107 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "moduleResolution": "node",
5 | "target": "es2020",
6 | "lib": ["es2020"],
7 | "esModuleInterop": true,
8 | "strict": true
9 | },
10 | "include": [
11 | "src/**/*.ts"
12 | ],
13 | "exclude": ["node_modules", "dist"]
14 | }
--------------------------------------------------------------------------------