@wixc3/common
Advanced tools
Comparing version 2.2.1 to 3.0.0
{ | ||
"name": "@wixc3/common", | ||
"version": "2.2.1", | ||
"version": "3.0.0", | ||
"description": "Common utils, usable in all environments", | ||
"main": "dist/index.js", | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.js", | ||
"author": "Wix.com", | ||
@@ -11,4 +12,4 @@ "sideEffects": false, | ||
"src", | ||
"test", | ||
"!dist/tsconfig.tsbuildinfo" | ||
"!dist/*/test", | ||
"!dist/*/tsconfig.tsbuildinfo" | ||
], | ||
@@ -23,3 +24,6 @@ "license": "MIT", | ||
"@file-services/types": "^7.0.1" | ||
}, | ||
"devDependencies": { | ||
"@microsoft/api-documenter": "^7.19.22" | ||
} | ||
} |
205
src/chain.ts
@@ -1,86 +0,165 @@ | ||
import { at, concat, every, filter, find, first, Flat, flat, flatMap, forEach, includes, isEmpty, last, map, Mapping, next, Predicate, prev, reduce, size, some, unique } from "./iterables" | ||
import { mapValue } from "./objects" | ||
import { | ||
at, | ||
concat, | ||
every, | ||
filter, | ||
find, | ||
first, | ||
Flat, | ||
flat, | ||
flatMap, | ||
forEach, | ||
includes, | ||
isEmpty, | ||
join, | ||
last, | ||
map, | ||
Mapping, | ||
next, | ||
Predicate, | ||
prev, | ||
reduce, | ||
size, | ||
skip, | ||
some, | ||
sort, | ||
unique, | ||
} from './iterables'; | ||
import { mapValues } from './objects'; | ||
/** | ||
* {@label Iter} | ||
* Chain iterable operations, each acting on the output of the previous step | ||
* | ||
* @example <caption>When the action is per item, the result is accessible as *iterable*</caption> | ||
* @example When the action is per item, the result is accessible as *iterable* | ||
* ``` | ||
* chain([0,1,2]) | ||
* .filter(i => i) | ||
* .map(i => i**2) | ||
* .iterable => [1,4] | ||
* @example <caption>When the action returns an element (as in first, next, reduce etc) the the result is accessible as *value*</caption> | ||
* chain([0,1,2]).filter(i => i).first().value => 1 | ||
* @example <caption>However, iterable is always accessible, as a single element iterable</caption> | ||
* chain([0,1,2]).filter(i => i).first().iterable => [1] | ||
* @example <caption>**Note** if the action returned undefined, iterable will be empty </caption> | ||
* chain([]).first().iterable => [] | ||
* chain([]).first().value => undefined | ||
* @param value | ||
* @returns | ||
* .iterable | ||
* // => [1,4] | ||
* ``` | ||
* @example When the action returns an element (as in first, next, reduce etc) the the result is accessible as *value* | ||
* ``` | ||
* chain([0,1,2]).filter(i => i).first().value | ||
* // => 1 | ||
* ``` | ||
* @example Iterable is always accessible, as a single element iterable | ||
* ``` | ||
* chain([0,1,2]).filter(i => i).first().iterable | ||
* // => [1] | ||
* ``` | ||
* @example <b>Note</b> if the action returned undefined, iterable will be empty | ||
* ``` | ||
* chain([]).first().iterable // => [] | ||
* chain([]).first().value // => undefined | ||
* ``` | ||
* @param value - initial iterable | ||
* @returns Chainable action on iterable | ||
*/ | ||
export function chain<T>(iterable: Iterable<T>): IterableChain<T> | ||
export function chain<T, V extends NotIterable<T>>(value: V): ValueChain<V> | ||
export function chain<T>(value: Iterable<T>): IterableChain<T>; | ||
/** | ||
* {@label Iter} | ||
* Chain iterable operations, each acting on the output of the previous step | ||
* @example When the action is per item, the result is accessible as *iterable* | ||
* ``` | ||
* chain([0,1,2]) | ||
* .filter(i => i) | ||
* .map(i => i**2) | ||
* .iterable | ||
* // => [1,4] | ||
* ``` | ||
* @example When the action returns an element (as in first, next, reduce etc) the the result is accessible as *value* | ||
* ``` | ||
* chain("hello").map(i => i.split("")).first().value | ||
* // => "h" | ||
* ``` | ||
* @example Iterable is always accessible, as a single element iterable | ||
* ``` | ||
* chain([0,1,2]).filter(i => i).first().iterable | ||
* // => [1] | ||
* ``` | ||
* @example <b>Note</b> if the action returned undefined, iterable will be empty | ||
* ``` | ||
* chain([]).first().iterable // => [] | ||
* chain([]).first().value // => undefined | ||
* ``` | ||
* @param value - initial value | ||
* @returns Chainable action on iterable | ||
*/ | ||
export function chain<T, V extends NotIterable<T>>(value: V): ValueChain<V>; | ||
export function chain<T>(value: T) { | ||
const iterable = (value === undefined | ||
? [] | ||
: value === null | ||
? [null] | ||
: Symbol.iterator in value | ||
? value | ||
: [value]) as Iter<T> | ||
const iterable = ( | ||
value === undefined ? [] : value === null ? [null] : Symbol.iterator in value ? value : [value] | ||
) as Iter<T>; | ||
return iterable === value | ||
? chainIter(iterable) | ||
: chainElement(value) | ||
return iterable === value ? chainIter(iterable) : chainElement(value); | ||
} | ||
function chainIter<T>(iterable: Iterable<T>): IterableChain<T> { | ||
const toIter = { map, flatMap, filter, concat, flat, unique } | ||
const toElm = { last, first, isEmpty, size, at, next, prev, find, some, includes, every, reduce } | ||
const toIter = { skip, map, flatMap, filter, concat, flat, unique, sort } as const; | ||
const toElm = { join, last, first, isEmpty, size, at, next, prev, find, some, includes, every, reduce } as const; | ||
const boundToIter = mapValues( | ||
toIter, | ||
(v) => | ||
(...args: unknown[]) => | ||
chainIter( | ||
(v as (iterable: Iterable<unknown>, ...args: unknown[]) => Iterable<unknown>)(iterable, ...args) | ||
) | ||
); | ||
const boundToElm = mapValues( | ||
toElm, | ||
(v) => | ||
(...args: unknown[]) => | ||
chainElement((v as (iterable: Iterable<unknown>, ...args: unknown[]) => unknown)(iterable, ...args)) | ||
); | ||
return { | ||
value: iterable, | ||
iterable, | ||
...mapValue(toIter, v => (...args: any[]) => chainIter(v(iterable, ...args) as Iterable<unknown>)), | ||
...mapValue(toElm, v => (...args: any[]) => chainElement(v(iterable, ...args) as Iterable<unknown>)), | ||
...boundToIter, | ||
...boundToElm, | ||
forEach: (mapping: Mapping<T, unknown>) => { | ||
forEach(iterable, mapping) | ||
return chainIter(iterable) | ||
} | ||
} as any as IterableChain<T> | ||
forEach(iterable, mapping); | ||
return chainIter(iterable); | ||
}, | ||
} as IterableChain<T>; | ||
} | ||
function chainElement<T>(value: T): ValueChain<T> { | ||
const iterable = (value === undefined ? [] : [value]) as Iterable<T> | ||
const iterable = (value === undefined ? [] : [value]) as Iterable<T>; | ||
return { | ||
...chainIter(iterable), | ||
value | ||
} | ||
value, | ||
}; | ||
} | ||
type IterableChain<T> = Chain<T> & {value:Iterable<T>} | ||
type ValueChain<T> = Chain<T> & {value:T} | ||
type NotIterable<T> = T extends Iterable<unknown> ? never : T | ||
type Iter<T> = T extends Iterable<infer E> ? Iterable<E> : Iterable<T> | ||
type Chain<T> = { | ||
last: () => ValueChain<T> | ||
first: () => ValueChain<T> | ||
isEmpty: () => ValueChain<boolean> | ||
size: () => ValueChain<number> | ||
at: (index: number) => ValueChain<T> | ||
next: () => ValueChain<T> | ||
prev: () => ValueChain<T> | ||
unique: () => IterableChain<T> | ||
map: <S>(m: Mapping<T, S>) => IterableChain<S> | ||
flatMap: <S>(m: Mapping<T, S>) => IterableChain<Flat<S>> | ||
filter: (p: Predicate<T>) => IterableChain<T> | ||
concat: (...iterables: Iterable<T>[]) => IterableChain<T> | ||
forEach: (fn: Mapping<T, unknown>) => IterableChain<T> | ||
find: (p: Predicate<T>) => ValueChain<T> | ||
includes: (element: T) => ValueChain<boolean> | ||
some: (p: Predicate<T>) => ValueChain<boolean> | ||
every: (p: Predicate<T>) => ValueChain<boolean> | ||
flat: () => IterableChain<Flat<T>> | ||
reduce: <A>(reducer:(acc:A, item:T)=>A, initial:A) => ValueChain<A> | ||
iterable: Iterable<T> | ||
} | ||
export type IterableChain<T> = Chain<T> & { value: Iterable<T> }; | ||
export type ValueChain<T> = Chain<T> & { value: T }; | ||
export type NotIterable<T> = T extends Iterable<unknown> ? never : T; | ||
export type Iter<T> = T extends Iterable<infer E> ? Iterable<E> : Iterable<T>; | ||
export type Chain<T> = { | ||
last: () => ValueChain<T>; | ||
first: () => ValueChain<T>; | ||
isEmpty: () => ValueChain<boolean>; | ||
size: () => ValueChain<number>; | ||
at: (index: number) => ValueChain<T>; | ||
next: () => ValueChain<T>; | ||
prev: () => ValueChain<T>; | ||
unique: () => IterableChain<T>; | ||
map: <S>(m: Mapping<T, S>) => IterableChain<S>; | ||
flatMap: <S>(m: Mapping<T, S>) => IterableChain<Flat<S>>; | ||
filter: (p: Predicate<T>) => IterableChain<T>; | ||
concat: (...iterables: Iterable<T>[]) => IterableChain<T>; | ||
forEach: (fn: Mapping<T, unknown>) => IterableChain<T>; | ||
find: (p: Predicate<T>) => ValueChain<T>; | ||
includes: (element: T) => ValueChain<boolean>; | ||
some: (p: Predicate<T>) => ValueChain<boolean>; | ||
sort: (p: Predicate<T, number>) => IterableChain<T>; | ||
every: (p: Predicate<T>) => ValueChain<boolean>; | ||
flat: () => IterableChain<Flat<T>>; | ||
join: () => ValueChain<string>; | ||
skip: (count: number) => IterableChain<T> | ||
reduce: <A>(reducer: (acc: A, item: T) => A, initial: A) => ValueChain<A>; | ||
iterable: Iterable<T>; | ||
}; |
@@ -12,3 +12,3 @@ export type PromiseResolveCb<T> = (value: T | PromiseLike<T>) => void; | ||
* A closure-less alternative to new Promise(...) | ||
* @returns | ||
* @returns | ||
*/ | ||
@@ -15,0 +15,0 @@ export function deferred<T = void>(): IDeferredPromise<T> { |
@@ -8,3 +8,3 @@ export function enumValues<T extends string>(enumObj: { [key: string]: T }): IterableIterator<T>; | ||
// TODO: validate | ||
export function* enumValues<T>(enumObj: Record<string,T>): IterableIterator<T> { | ||
export function* enumValues<T>(enumObj: Record<string, T>): IterableIterator<T> { | ||
let isStringEnum = true; | ||
@@ -11,0 +11,0 @@ for (const property in enumObj) { |
@@ -7,4 +7,5 @@ import { isPlainObject } from './objects'; | ||
*/ | ||
export const toError = (value: unknown): Error => | ||
value instanceof Error ? value : new Error(value === undefined ? undefined : String(value)); | ||
export function toError(value: unknown): Error { | ||
return value instanceof Error ? value : new Error(value === undefined ? undefined : String(value)); | ||
} | ||
@@ -14,6 +15,8 @@ /** | ||
*/ | ||
export const getErrorCode = (error: Error & { code?: string }): string | undefined => { | ||
export function getErrorCode(error: Error & { code?: string }): string | undefined { | ||
return typeof error.code === 'string' ? error.code : undefined; | ||
}; | ||
} | ||
/** | ||
*/ | ||
export class ErrorWithCode extends Error { | ||
@@ -23,3 +26,3 @@ public code?: string; | ||
public constructor(message?: string, options?: { code?: string; cause?: unknown }) { | ||
const { code, ...nativeErrorOptions } = options ?? {}; | ||
const { code, ...nativeErrorOptions } = options ?? {}; | ||
super(message, nativeErrorOptions); | ||
@@ -33,2 +36,3 @@ this.code = code; | ||
* @example | ||
* ```ts | ||
* declare const align: 'left' | 'right' | 'middle'; | ||
@@ -41,2 +45,3 @@ * switch (align) { | ||
* } | ||
* ``` | ||
*/ | ||
@@ -70,5 +75,7 @@ export class UnreachableCaseError extends Error { | ||
export function isErrorLikeObject(error: unknown): error is Error { | ||
return isPlainObject(error) | ||
&& typeof (error as {name?:string}).name === 'string' | ||
&& typeof (error as {message?:string}).message === 'string'; | ||
return ( | ||
isPlainObject(error) && | ||
typeof (error as { name?: string }).name === 'string' && | ||
typeof (error as { message?: string }).message === 'string' | ||
); | ||
} |
@@ -0,19 +1,25 @@ | ||
/** | ||
* @returns a no operation function | ||
*/ | ||
export const noop = () => undefined; | ||
/** | ||
* @returns a function that returns a resolved Promise | ||
*/ | ||
export const asyncNoop = () => Promise.resolve(); | ||
/** | ||
* | ||
* @param fn | ||
* Make a function executable only once, following calls are ignored | ||
* @returns fn, wrapped to run only upon first execution | ||
*/ | ||
export const once = <T extends (...args:unknown[])=>unknown>(fn:T):T => { | ||
export function once<T extends (...args: any[]) => any>(fn: T): T { | ||
let run = false; | ||
let result:ReturnType<T>; | ||
return ((...args:unknown[]) => { | ||
let result: ReturnType<T>; | ||
return ((...args: unknown[]) => { | ||
if (!run) { | ||
run = true; | ||
result = fn(...args) as ReturnType<T> | ||
result = fn(...args) as ReturnType<T>; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
return result; | ||
}) as T | ||
}) as T; | ||
} |
@@ -1,20 +0,18 @@ | ||
export * from './strings' | ||
export * from './numbers' | ||
export * from './objects' | ||
export * from './iterables' | ||
export * from './chain' | ||
export * from './same' | ||
export * from './errors' | ||
export * from './sets' | ||
export * from './debouncer' | ||
export * from './enum-values' | ||
export * from './index' | ||
export * from './lru-cache' | ||
export * from './platform' | ||
export * from './signal' | ||
export * from './functions' | ||
export * from './deferred' | ||
export * from './set-multi-map' | ||
export * from './event-emitter' | ||
export * from './types' | ||
export * from './maps' | ||
/** | ||
* @packageDocumentation | ||
* Useful utils for strings, iterables, objects, maps, promises and other commonly used structures | ||
*/ | ||
export * from './strings'; | ||
export * from './numbers'; | ||
export * from './objects'; | ||
export * from './iterables'; | ||
export * from './chain'; | ||
export * from './same'; | ||
export * from './errors'; | ||
export * from './sets'; | ||
export * from './enum-values'; | ||
export * from './index'; | ||
export * from './platform'; | ||
export * from './functions'; | ||
export * from './types'; | ||
export * from './maps'; |
@@ -1,16 +0,16 @@ | ||
import { isDefined, Nullable } from "./types" | ||
import { isDefined, Nullable } from './types'; | ||
export type Mapping<S, T> = (src: S) => T | ||
export type Predicate<S, V = boolean> = (src: S) => V | ||
export type Flat<T> = T extends Iterable<infer A> ? A : T | ||
export type Mapping<S, T> = (src: S) => T; | ||
export type Predicate<S, V = boolean> = (src: S) => V; | ||
export type Flat<T> = T extends Iterable<infer A> ? A : T; | ||
/** | ||
* @param iterable | ||
* @returns the last element of iterable | ||
* Picks the last element of an iterable | ||
* @returns undefined for null/empty iterable | ||
*/ | ||
export function last<T>(iterable: Nullable<Iterable<T>>): T | undefined { | ||
let last!: T | ||
let last!: T; | ||
if (iterable) { | ||
for (const value of iterable) { | ||
last = value | ||
last = value; | ||
} | ||
@@ -22,36 +22,37 @@ } | ||
/** | ||
* @param iterable | ||
* @returns the first element of iterable | ||
* Picks the first element of an iterable | ||
* @returns undefined for null/empty iterable | ||
*/ | ||
export const first = <T>(iterable: Nullable<Iterable<T>>): T | undefined => | ||
at(iterable, 0) | ||
export function first<T>(iterable: Nullable<Iterable<T>>): T | undefined { | ||
return at(iterable, 0); | ||
} | ||
/** | ||
* @param iterable | ||
* @returns the first element of iterable | ||
* Checks if an iterable is empty | ||
*/ | ||
export function isEmpty(iterable: Iterable<unknown>): boolean { | ||
for (const _ of iterable) { | ||
return false | ||
return false; | ||
} | ||
return true | ||
return true; | ||
} | ||
/** | ||
* @param iterable | ||
* @returns the elements count of iterable | ||
* Evaluate the size of an iterable | ||
* @returns elements count of iterable, 0 if null | ||
*/ | ||
export function size(iterable: Iterable<unknown>): number { | ||
let size = 0 | ||
for (const _ of iterable) { | ||
size++; | ||
export function size(iterable: Nullable<Iterable<unknown>>): number { | ||
let size = 0; | ||
if (iterable) { | ||
for (const _ of iterable) { | ||
size++; | ||
} | ||
} | ||
return size | ||
return size; | ||
} | ||
/** | ||
* Finds element by index, including negative index | ||
* @see Array.at | ||
* @param iterable | ||
* @param index | ||
* @returns the element at the given index | ||
* @returns undefined if invalid index or null iterable | ||
*/ | ||
@@ -61,3 +62,3 @@ export function at<T>(iterable: Nullable<Iterable<T>>, index: number): T | undefined { | ||
if (index < 0) { | ||
return Array.from(iterable).at(index) | ||
return Array.from(iterable).at(index); | ||
} | ||
@@ -67,62 +68,59 @@ let i = 0; | ||
if (i++ === index) { | ||
return v | ||
return v; | ||
} | ||
} | ||
} | ||
return undefined | ||
return undefined; | ||
} | ||
/** | ||
* @param iterable | ||
* @param item | ||
* @returns the element after item, undefined if last or not found | ||
* Find the element following an item | ||
* @returns undefined if item is last or not found | ||
*/ | ||
export function next<T>(iterable: Nullable<Iterable<T>>, item: T): T | undefined { | ||
let wasFound = false | ||
let wasFound = false; | ||
if (iterable) { | ||
for (const v of iterable) { | ||
if (wasFound) { | ||
return v | ||
return v; | ||
} | ||
if (v === item) { | ||
wasFound = true | ||
wasFound = true; | ||
} | ||
} | ||
} | ||
return undefined | ||
return undefined; | ||
} | ||
/** | ||
* @param iterable | ||
* @param item | ||
* @returns the elements before item, undefined if first or not found | ||
* Find the element before an item | ||
* @returns undefined if item is first or not found | ||
*/ | ||
export function prev<T>(iterable: Nullable<Iterable<T>>, item: T): T | undefined { | ||
let prev!: T | ||
let prev!: T; | ||
if (iterable) { | ||
for (const v of iterable) { | ||
if (v === item) { | ||
return prev | ||
return prev; | ||
} | ||
prev = v | ||
prev = v; | ||
} | ||
} | ||
return undefined | ||
return undefined; | ||
} | ||
/** | ||
* @param iterable | ||
* @param by an element identifier function | ||
* @returns an iterable with unique elements | ||
* Creates iterable of unique elements | ||
* @param by - an element identifier (hash) function | ||
*/ | ||
export function* unique<T>(iterable: Nullable<Iterable<T>>, by: Predicate<T, unknown> = i => i): Iterable<T> { | ||
const known = new Set<unknown>() | ||
export function* unique<T>(iterable: Nullable<Iterable<T>>, by: Predicate<T, unknown> = (i) => i): Iterable<T> { | ||
const known = new Set<unknown>(); | ||
if (iterable) { | ||
for (const v of iterable) { | ||
if (!known.has(by(v))) { | ||
known.add(by(v)) | ||
known.add(by(v)); | ||
yield v; | ||
} | ||
} | ||
known.clear() | ||
known.clear(); | ||
} | ||
@@ -132,6 +130,4 @@ } | ||
/** | ||
* Map iterable elements | ||
* @see Array.map | ||
* @param iterable | ||
* @param mapFn | ||
* @returns a mapped iterable | ||
*/ | ||
@@ -141,3 +137,3 @@ export function* map<S, T>(iterable: Nullable<Iterable<S>>, mapFn: Mapping<S, T>): Iterable<T> { | ||
for (const v of iterable) { | ||
yield mapFn(v) | ||
yield mapFn(v); | ||
} | ||
@@ -149,8 +145,6 @@ } | ||
* @see Array.flatMap | ||
* @param iterable | ||
* @param mapFn | ||
* @returns a mapped, flattened iterables | ||
*/ | ||
export function* flatMap<S, T>(iterable: Nullable<Iterable<S>>, mapFn: Mapping<S, T | Iterable<T>>): Iterable<Flat<T>> { | ||
yield* flat(map(iterable, mapFn)) | ||
yield* flat(map(iterable, mapFn)); | ||
} | ||
@@ -160,5 +154,2 @@ | ||
* @see Array.filter | ||
* @param iterable | ||
* @param predicate | ||
* @returns a filtered iterable | ||
*/ | ||
@@ -169,3 +160,3 @@ export function* filter<T>(iterable: Nullable<Iterable<T>>, predicate: Predicate<T>): Iterable<T> { | ||
if (predicate(v)) { | ||
yield v | ||
yield v; | ||
} | ||
@@ -178,3 +169,2 @@ } | ||
* @see Array.concat | ||
* @param iterables | ||
* @returns a concatenated iterable | ||
@@ -192,4 +182,2 @@ */ | ||
* @see Array.forEach | ||
* @param iterable | ||
* @param fn | ||
*/ | ||
@@ -199,3 +187,4 @@ export function forEach<T>(iterable: Nullable<Iterable<T>>, fn: Mapping<T, unknown>): void { | ||
for (const v of iterable) { | ||
fn(v) | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument | ||
fn(v); | ||
} | ||
@@ -207,4 +196,2 @@ } | ||
* @see Array.find | ||
* @param iterable | ||
* @param predicate | ||
* @returns the first element the satisfies the predicate | ||
@@ -216,7 +203,7 @@ */ | ||
if (predicate(v)) { | ||
return v | ||
return v; | ||
} | ||
} | ||
} | ||
return undefined | ||
return undefined; | ||
} | ||
@@ -226,8 +213,6 @@ | ||
* @see Array.includes | ||
* @param iterable | ||
* @param item | ||
* @returns item is an element of iterable | ||
*/ | ||
export function includes<T>(iterable: Nullable<Iterable<T>>, item: T): boolean { | ||
return !!find(iterable, i => i === item) | ||
return !!find(iterable, (i) => i === item); | ||
} | ||
@@ -237,8 +222,6 @@ | ||
* @see Array.some | ||
* @param iterable | ||
* @param predicate | ||
* @returns there is an element satisfies the predicate | ||
*/ | ||
export function some<T>(iterable: Nullable<Iterable<T>>, predicate: Predicate<T>): boolean { | ||
return !!find(iterable, predicate) | ||
return !!find(iterable, predicate); | ||
} | ||
@@ -248,4 +231,2 @@ | ||
* @see Array.every | ||
* @param iterable | ||
* @param predicate | ||
* @returns true is all elements satisfy the predicate | ||
@@ -256,6 +237,6 @@ */ | ||
if (!predicate(v)) { | ||
return false | ||
return false; | ||
} | ||
} | ||
return true | ||
return true; | ||
} | ||
@@ -265,5 +246,4 @@ | ||
* @see Array.flat | ||
* @param deep if true, repeat the flattening until all elements are not iterable | ||
* @param iterable | ||
* @returns a flattened iterable, | ||
* @param deep - if true, repeat the flattening until all elements are not iterable | ||
* @returns a flattened iterable, | ||
* where elements that are iterable are spread into the result | ||
@@ -276,3 +256,3 @@ */ | ||
// @ts-expect-error v is definitely iterable | ||
yield* (deep ? flat(v) : v) | ||
yield* deep ? flat(v) : v; | ||
} else { | ||
@@ -286,12 +266,11 @@ yield v as Flat<T>; | ||
/** | ||
* | ||
* @param iterable | ||
* @returns an histogram map (element=>count) | ||
* Calculate a histogram of iterable elements | ||
* @returns an histogram map (element=\>count) | ||
*/ | ||
export function histogram<T>(iterable: Iterable<T>) { | ||
const histogram = new Map<T, number>() | ||
forEach(iterable, i => { | ||
const count = histogram.get(i) || 0 | ||
histogram.set(i, count + 1) | ||
}) | ||
const histogram = new Map<T, number>(); | ||
forEach(iterable, (i) => { | ||
const count = histogram.get(i) || 0; | ||
histogram.set(i, count + 1); | ||
}); | ||
return histogram; | ||
@@ -301,19 +280,14 @@ } | ||
/** | ||
* | ||
* @param x | ||
* @returns true if x is iterable | ||
*/ | ||
export function isIterable(x: any): x is Iterable<unknown> { | ||
return isDefined(x) && typeof x === 'object' && (Symbol.iterator in x) | ||
return isDefined(x) && typeof x === 'object' && Symbol.iterator in x; | ||
} | ||
/** | ||
* @see Array.sort | ||
* @param iterable | ||
* @param by comparator | ||
* @see Array<T>.sort | ||
* @param by - comparator, returns a negative value if a should precede b | ||
*/ | ||
export function sort<T>(iterable: Nullable<Iterable<T>>, by?: (a: T, b: T) => number): Iterable<T> { | ||
return iterable | ||
? [...iterable].sort(by) | ||
: [] | ||
return iterable ? [...iterable].sort(by) : []; | ||
} | ||
@@ -323,5 +297,2 @@ | ||
* @see Array.reduce | ||
* @param iterable | ||
* @param reducer | ||
* @param initial | ||
* @returns reduced object | ||
@@ -333,3 +304,3 @@ */ | ||
for (const item of iterable) { | ||
acc = reducer(acc, item) | ||
acc = reducer(acc, item); | ||
} | ||
@@ -340,14 +311,58 @@ } | ||
/** | ||
* @see Array.join | ||
*/ | ||
export function join<T extends string>(iterable: Nullable<Iterable<T>>, separator: string): string { | ||
if (!iterable) { | ||
return ''; | ||
} | ||
let result = first(iterable) || ''; | ||
let prev: string | null = null; | ||
for (const v of skip(iterable, 1)) { | ||
if (prev) { | ||
result = result + separator + prev; | ||
} | ||
prev = v; | ||
} | ||
if (prev) { | ||
result = result + separator + prev; | ||
} | ||
return result; | ||
} | ||
export const groupBy = <T, K extends keyof T>(elements: Iterable<T>, property: K): Map<T[K], T[]> => | ||
reduce(elements, (acc, element) => { | ||
const propertyValue = acc.get(element[property]); | ||
if (propertyValue) { | ||
propertyValue.push(element); | ||
} else { | ||
acc.set(element[property], [element]); | ||
/** | ||
* Skips the first elements of an iterable | ||
*/ | ||
export function* skip<T>(iterable: Nullable<Iterable<T>>, count: number): Iterable<T> { | ||
if (iterable) { | ||
for (const item of iterable) { | ||
if (count === 0) { | ||
yield item; | ||
} else { | ||
count--; | ||
} | ||
} | ||
} | ||
} | ||
return acc; | ||
}, new Map<T[K], T[]>()); | ||
/** | ||
* Groups elements by the value of a property | ||
* @returns A map of the value to an array of elements | ||
*/ | ||
export function groupBy<T, K extends keyof T>(elements: Iterable<T>, property: K): Map<T[K], T[]> { | ||
return reduce( | ||
elements, | ||
(acc, element) => { | ||
const propertyValue = acc.get(element[property]); | ||
if (propertyValue) { | ||
propertyValue.push(element); | ||
} else { | ||
acc.set(element[property], [element]); | ||
} | ||
return acc; | ||
}, | ||
new Map<T[K], T[]>() | ||
); | ||
} |
@@ -1,2 +0,2 @@ | ||
import { isMap, Nullable } from "./types" | ||
import { isMap, Nullable } from './types'; | ||
@@ -7,27 +7,26 @@ /** | ||
* @see Map.has | ||
* @param obj | ||
* @param key | ||
* @returns | ||
*/ | ||
export function has<O extends object>(obj: O, key: keyof O): boolean; | ||
/** | ||
* Similar to Map.has, but works for plain objects, and returns false | ||
* for null maps | ||
* @see Map.has | ||
*/ | ||
export function has<K>(obj: Map<K, unknown>, key: K): boolean; | ||
export function has(obj: null | undefined, key: unknown | never): false; | ||
export function has(obj: Nullable<object | Map<unknown, unknown>>, key: any): boolean { | ||
return !!obj && ( | ||
return ( | ||
!!obj && | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
typeof (obj as any).has === 'function' | ||
? (obj as Map<any, any>).has(key) | ||
: key in (obj as object)) | ||
(typeof (obj as any).has === 'function' ? (obj as Map<any, any>).has(key) : key in (obj as object)) | ||
); | ||
} | ||
type MapValue<T> = T extends Map<infer _, infer V> ? V : never | ||
type ObjValue<O extends object, K extends keyof O = keyof O> = O[K] | ||
export type MapValue<T> = T extends Map<infer _, infer V> ? V : never; | ||
export type ObjValue<O extends object, K extends keyof O = keyof O> = O[K]; | ||
/** | ||
* Similar to Map.get, but works for plain objects, and returns undefined | ||
* Similar to Map.get, but works for plain objects, and returns undefined | ||
* for null maps and missing keys | ||
* @see Map.get | ||
* @param obj | ||
* @param key | ||
* @return found value, *undefined* if map/value to not exist | ||
* @returns found value, *undefined* if map/value to not exist | ||
*/ | ||
@@ -41,19 +40,14 @@ export function get<O extends object, K extends keyof O>(obj: O, key: K): ObjValue<O, K>; | ||
? (obj as Map<any, any>).get(key) | ||
// @ts-expect-error definitely not a map or nullish | ||
: obj[key as string] | ||
: // @ts-expect-error definitely not a map or nullish | ||
obj[key as string]; | ||
} | ||
return | ||
return; | ||
} | ||
/** | ||
* Returns a value by key, throws if the value is missing or the map null | ||
* @param map | ||
* @param key | ||
* @param errorMessage | ||
* @returns | ||
*/ | ||
export function getValue<T extends object>(map: T, key: keyof T, errorMessage?: string): ObjValue<T> | ||
export function getValue<K, V, T extends Map<K, V>>(map: T, key: K, errorMessage?: string): MapValue<T> | ||
export function getValue<T extends null | undefined>(map: T, key: unknown | never, errorMessage?: string): never | ||
export function getValue<T extends object>(map: T, key: keyof T, errorMessage?: string): ObjValue<T>; | ||
export function getValue<K, V, T extends Map<K, V>>(map: T, key: K, errorMessage?: string): MapValue<T>; | ||
export function getValue<T extends null | undefined>(map: T, key: unknown | never, errorMessage?: string): never; | ||
export function getValue<T extends Map<K, V> | object, K, V>(map: Nullable<T>, key: KeyOf<T>, errorMessage?: string) { | ||
@@ -63,30 +57,26 @@ // @ts-expect-error hmm | ||
// @ts-expect-error hmmm | ||
return get(map, key) | ||
return get(map, key); | ||
} | ||
throw new Error(errorMessage || `Missing map`) | ||
throw new Error(errorMessage || `Missing map`); | ||
} | ||
type KeyOf<T> = T extends Map<infer K, infer _> ? K : | ||
T extends object ? keyof T : never | ||
type KeyOf<T> = T extends Map<infer K, infer _> ? K : T extends object ? keyof T : never; | ||
/** | ||
* @see Map.keys | ||
* @see Object.keys | ||
* @param map | ||
* @returns an iterable of the map/object keys | ||
*/ | ||
export function keys<K, O extends Map<K, any>>(map: O): Iterable<K> | ||
export function keys<O extends object>(map: O): Iterable<keyof O> | ||
export function keys<K, O extends Map<K, any>>(map: O): Iterable<K>; | ||
export function keys<O extends object>(map: O): Iterable<keyof O>; | ||
export function keys<K, O extends object | Map<K, any>>(obj: O) { | ||
return isMap(obj) | ||
? obj.keys() | ||
: Object.keys(obj) | ||
return isMap(obj) ? obj.keys() : Object.keys(obj); | ||
} | ||
export function values<K, O extends Map<K, any>>(map: O): Iterable<MapValue<O>> | ||
export function values<O extends object>(map: O): Iterable<ObjValue<O>> | ||
/** | ||
* @see Map.values | ||
* @see Object.values | ||
*/ | ||
export function values<K, O extends Map<K, any>>(map: O): Iterable<MapValue<O>>; | ||
export function values<O extends object>(map: O): Iterable<ObjValue<O>>; | ||
export function values<K, O extends object | Map<K, any>>(obj: O) { | ||
return isMap(obj) | ||
? obj.values() | ||
: Object.values(obj) | ||
return isMap(obj) ? obj.values() : Object.values(obj); | ||
} | ||
import { sleep } from 'promise-assist'; | ||
import { chain } from './chain'; | ||
import type { UnionToIntersection } from './types'; | ||
@@ -16,3 +17,6 @@ export function exclude<R>(...excluded: R[]) { | ||
* returns an object composed of the picked object properties | ||
* @example pick({ a: 1, b: 2 }, ['a']) // => { a: 1 } | ||
* @example | ||
* ```ts | ||
* pick({ a: 1, b: 2 }, ['a']) // => { a: 1 } | ||
* ``` | ||
*/ | ||
@@ -27,29 +31,48 @@ export function pick<O extends object, K extends keyof O>(record: O, keys: Iterable<K>): Pick<O, K> { | ||
export const mapObject = (obj: object, mapping: (entry: [string, any]) => [string, any]) => | ||
Object.fromEntries( | ||
Object.entries(obj) | ||
.map(mapping)) | ||
/** | ||
* Maps key value pairs of a plain object | ||
*/ | ||
export function mapObject(obj: object, mapping: (entry: [string, any]) => [string, any]) { | ||
return Object.fromEntries(Object.entries(obj).map(mapping)); | ||
} | ||
/** | ||
* Maps values of a plain object | ||
*/ | ||
export function mapValues<T extends object, R>( | ||
obj: T, | ||
mapping: (value: T[keyof T], k?: keyof T) => R | ||
): { [_ in keyof T]: R } { | ||
return Object.fromEntries( | ||
Object.entries(obj).map(([k, v]: [string, T[keyof T]]) => [k, mapping(v, k as keyof T)]) | ||
) as { [_ in keyof T]: R }; | ||
} | ||
export const mapValue = (obj: object, mapping: (value: any) => any) => | ||
Object.fromEntries( | ||
Object.entries(obj) | ||
.map(([k, v]) => [k, mapping(v)])) | ||
/** | ||
* Maps values of a plain object | ||
*/ | ||
export function mapKeys<T extends object, R extends string>( | ||
obj: object, | ||
mapping: (key: keyof T, value?: T[keyof T]) => R | ||
): Record<R, T[keyof T]> { | ||
return Object.fromEntries( | ||
Object.entries(obj).map(([k, v]: [string, T[keyof T]]) => [mapping(k as keyof T, v), v]) | ||
) as Record<R, T[keyof T]>; | ||
} | ||
export const mapKeys = (obj: object, mapping: (key: string) => string) => | ||
Object.fromEntries( | ||
Object.entries(obj) | ||
.map(([k, v]) => [mapping(k), v])) | ||
/** | ||
* Checks that value is a POJO | ||
*/ | ||
export function isPlainObject(value: unknown): value is Record<string | number | symbol, unknown> { | ||
return value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype; | ||
} | ||
export function isPlainObject(value: unknown): value is Record<string|number|symbol, any>{ | ||
return value !== null | ||
&& typeof value === 'object' | ||
&& Object.getPrototypeOf(value) === Object.prototype | ||
/** | ||
* Logs an error | ||
*/ | ||
export function reportError(ex: unknown) { | ||
// eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return | ||
console.error(ex); | ||
} | ||
// eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return | ||
export const reportError = (ex: unknown) => console.error(ex); | ||
/** | ||
@@ -69,3 +92,3 @@ * Awaits a record of promises, and returns a record of their results. | ||
for (const [key, promise] of Object.entries(obj)) { | ||
out[key] = await promise | ||
out[key] = await promise; | ||
} | ||
@@ -97,7 +120,9 @@ return out as Out; | ||
* First takes on value collisions. | ||
* | ||
* @returns a new object with the values as keys and the keys as values | ||
* @example { a: 'y', b: 'z'} => { y: 'a', z: 'b' } | ||
* @example | ||
* ```ts | ||
* reverseObject({ a: 'y', b: 'z'}) // => { y: 'a', z: 'b' } | ||
* ``` | ||
*/ | ||
export const reverseObject = (obj: Record<string, string | false | undefined>) => { | ||
export function reverseObject(obj: Record<string, string | false | undefined>) { | ||
const reversedObject: Record<string, string> = {}; | ||
@@ -112,46 +137,49 @@ | ||
return reversedObject; | ||
}; | ||
} | ||
/** | ||
* Returns an object where missing keys and values/keys | ||
* Returns an object where missing keys and values/keys | ||
* that satisfy shouldUseDefault | ||
* to the value in shouldUseDefault. | ||
* | ||
* to the value in shouldUseDefault. | ||
* | ||
* @example | ||
* defaults({}, {a:0}) => {a:0} | ||
* defaults({a:1}, {a:0}) => {a:1} | ||
* defaults({a:{}}, {a:{b:1}}) => {a:{b:1}} | ||
* | ||
* ```ts | ||
* defaults({}, {a:0}) // => {a:0} | ||
* defaults({a:1}, {a:0}) // => {a:1} | ||
* defaults({a:{}}, {a:{b:1}}) // => {a:{b:1}} | ||
* ``` | ||
* by default, any undefined value will be replaced | ||
* @param source | ||
* @param defaultValues | ||
* @param deep [true] perform a deep comparison | ||
* @example | ||
* defaults({a:{}}, {a:{b:1}}, false) => {a:{}} | ||
* @param shouldUseDefault [(v,k)=>v===undefined] value/key for which shouldUseDefault returns true will be taken from defaultValues, ignoring source | ||
* k is provided as a dot separated path | ||
* @example | ||
* defaults({a:{b:1}}, {a:{b:2}}, true, (_,k)=>k==='a.b') => {a:{b:2}} | ||
* defaults({a:1}, {a:2}, true, v=>v===1) => {a:2} | ||
* @param deep - [true] perform a deep comparison | ||
* @example | ||
* ```ts | ||
* defaults({a:{}}, {a:{b:1}}, false) // => {a:{}} | ||
* ``` | ||
* @param shouldUseDefault - value/key for which shouldUseDefault | ||
* returns true will be taken from defaultValues, | ||
* ignoring source. | ||
* k is provided as a dot separated path | ||
* @example | ||
* ```ts | ||
* defaults({a:{b:1}}, {a:{b:2}}, true, (_,k)=>k==='a.b') // => {a:{b:2}} | ||
* defaults({a:1}, {a:2}, true, v=>v===1) // => {a:2} | ||
* ``` | ||
* @returns a new object with merged source and defaultValues | ||
*/ | ||
export function defaults<S, D>(_source: S, _defaultValues: D, deep = true, shouldUseDefault = (v: unknown, _key: string) => v === undefined): S & D { | ||
const parseObj = (src: any, dft: any, parentKey = ''): any => { | ||
export function defaults<S extends object, D extends object>( | ||
_source: S, | ||
_defaultValues: D, | ||
deep = true, | ||
shouldUseDefault = (v: unknown, _key: string) => v === undefined | ||
): S & D { | ||
const parseObj = (src: unknown, dft: unknown, parentKey = ''): Record<string, unknown> => { | ||
if (isPlainObject(src)) { | ||
const result = {} as Record<string, any> | ||
const result = {} as Record<string, any>; | ||
for (const [key, value] of Object.entries(src)) { | ||
const fullKey = (parentKey ? parentKey + '.' : '') + key | ||
const _default = isPlainObject(dft) | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
? (dft as any)[key] | ||
: undefined | ||
const fullKey = (parentKey ? parentKey + '.' : '') + key; | ||
const _default = isPlainObject(dft) ? dft[key] : undefined; | ||
if (shouldUseDefault(value, fullKey)) { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
result[key] = _default | ||
result[key] = _default; | ||
} else { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
result[key] = deep | ||
? parseObj(value, _default, fullKey) | ||
: value | ||
result[key] = deep ? parseObj(value, _default, fullKey) : value; | ||
} | ||
@@ -162,7 +190,8 @@ } | ||
if (!(key in src)) { | ||
result[key] = _default | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
result[key] = _default; | ||
} | ||
} | ||
} | ||
return result | ||
return result; | ||
} else { | ||
@@ -172,13 +201,14 @@ // @ts-expect-error non object values should only affect type when misused | ||
} | ||
} | ||
return parseObj(_source, _defaultValues) as S & D | ||
}; | ||
return parseObj(_source, _defaultValues) as S & D; | ||
} | ||
/** | ||
* @param obj The object to query | ||
* @param path The path of the property to get. | ||
* @param obj - The object to query | ||
* @param path - The path of the property to get. | ||
* @returns The value at `path` of `object` id exists, `undefined` otherwise | ||
* @example | ||
* getIn({ a: { b: 'c' } }, ['a', 'b']) | ||
* // => c | ||
* ```ts | ||
* getIn({ a: { b: 'c' } }, ['a', 'b']) // => c | ||
* ``` | ||
*/ | ||
@@ -190,38 +220,37 @@ export function getIn(obj: Record<string, any>, path: string[]): unknown { | ||
const DELETE = Symbol() | ||
type Remap<T> = Partial<Record<keyof T, string | typeof DELETE>> | ||
type UnionToIntersection<U> = | ||
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never | ||
type Remapped<T extends object, R> = | ||
UnionToIntersection< | ||
R extends Partial<Record<keyof T, string | typeof DELETE>> | ||
? { [K in keyof T]: K extends keyof R | ||
? R[K] extends string | ||
? { [L in R[K]]: T[K] } | ||
: never | ||
: { [L in K]: T[L] } | ||
}[keyof T] | ||
: never> | ||
const DELETE = Symbol(); | ||
export type Remap<T> = Partial<Record<keyof T, string | typeof DELETE>>; | ||
export type Remapped<T extends object, R> = UnionToIntersection< | ||
R extends Partial<Record<keyof T, string | typeof DELETE>> | ||
? { | ||
[K in keyof T]: K extends keyof R | ||
? R[K] extends string | ||
? { [L in R[K]]: T[K] } | ||
: never | ||
: { [L in K]: T[L] }; | ||
}[keyof T] | ||
: never | ||
>; | ||
type Remapp = { | ||
<T extends object, R extends Remap<T>>(obj: T, rename: R): Remapped<T, R> | ||
readonly DELETE: typeof DELETE | ||
} | ||
export type RemapFunc = { | ||
<T extends object, R extends Remap<T>>(obj: T, rename: R): Remapped<T, R>; | ||
readonly DELETE: typeof DELETE; | ||
}; | ||
/** | ||
* remaps keys of obj based on rename map object, | ||
* @param obj | ||
* @param rename key:key name in obj, value: new name (string) OR remap.DELETE | ||
* @returns | ||
* remaps keys of obj based on rename map object, | ||
* @param rename - key:key name in obj, value: new name (string) OR remap.DELETE | ||
* @returns | ||
*/ | ||
export const remap:Remapp = <T extends object, R extends Remap<T>>(obj: T, rename: R): Remapped<T, R> => Object.fromEntries( | ||
export const remap: RemapFunc = <T extends object, R extends Remap<T>>(obj: T, rename: R): Remapped<T, R> => | ||
Object.fromEntries( | ||
chain(Object.entries(obj)) | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
.filter(([key]) => (rename as any)[key] !== remap.DELETE) | ||
.map(([key, value]) => | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
[(rename as any)[key] || key, value] as [string, any]) | ||
.iterable | ||
) as Remapped<T, R> | ||
.map( | ||
([key, value]) => | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
[(rename as any)[key] || key, value] as [string, any] | ||
).iterable | ||
) as Remapped<T, R>; | ||
// @ts-expect-error setting the DELETE const | ||
remap.DELETE = DELETE | ||
remap.DELETE = DELETE; |
@@ -1,8 +0,7 @@ | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access | ||
const _process:{type?:string, platform?:string} = (globalThis as any)['process']; | ||
const _process: { type?: string; platform?: string } = (globalThis as any)['process']; | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access | ||
const _navigator:{platform:string} = (globalThis as any)['navigator']; | ||
const _navigator: { platform: string } = (globalThis as any)['navigator']; | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access | ||
const platform:string|undefined = _process?.platform; | ||
const platform: string | undefined = _process?.platform; | ||
@@ -9,0 +8,0 @@ export const isMac = platform ? platform === 'darwin' : _navigator?.platform.includes('Mac'); |
@@ -1,51 +0,63 @@ | ||
import { histogram, isIterable, size } from "./iterables" | ||
import { isPlainObject } from "./objects" | ||
import { isMap, isSet } from "./types" | ||
import { histogram, isIterable, size } from './iterables'; | ||
import { isPlainObject } from './objects'; | ||
import { isMap, isSet } from './types'; | ||
/** | ||
* deep comparison of two items | ||
* Deep comparison of two objects | ||
* @example | ||
* ```ts | ||
* same({a:1, b:2},{b:2, a:1}) // => true | ||
* same(new Map([['a',1],['b',2]]), new Map([['b',2],['a',1]])) // => true | ||
* same(new Map([[{a:1},{b:1}]]]), new Map([[{a:1},{b:1}]]])) // => true | ||
* same([0,1], [1,0]) // => false | ||
* same([0,1], [1,0], true) // => true | ||
* ``` | ||
* @remarks | ||
* items are "the same" if: | ||
* | ||
* - a === b (for anything other than iterables & POJO) | ||
* - a and b are POJO with same entries (order ignored, *same* used to compare values) | ||
* - a and b are Maps with same entries (order ignored, *same* used to compare keys & values) | ||
* - a and b are Sets with same values (order ignored, *same* used to compare values) | ||
* - a and b are POJO with same entries (order ignored, {@link same} used to compare values) | ||
* | ||
* - a and b are Maps with same entries (order ignored, {@link same} used to compare keys & values) | ||
* | ||
* - a and b are Sets with same values (order ignored, {@link same} used to compare values) | ||
* | ||
* - a and b are iterable (that are not Set or Map) with the same values, order checked if unordered=false (default) | ||
* @param a | ||
* @param b | ||
* @param unordered [false] Note: relevant only in array like iterables. objects, sets and maps are *never checked for order* of entries | ||
* @returns | ||
* | ||
* @param unordered - [false] Note: relevant only in array like iterables. objects, sets and maps are *never checked for order* of entries | ||
*/ | ||
export function same<T>(a: T, b: T, unordered = false): boolean { | ||
if (a === b) return true | ||
if (a === b) return true; | ||
if (!isIterable(a) || !isIterable(b)) { | ||
if (isPlainObject(a) && isPlainObject(b)) { | ||
return sameIgnoreOrder(Object.entries(a), Object.entries(b), unordered) | ||
return sameIgnoreOrder(Object.entries(a), Object.entries(b), unordered); | ||
} | ||
return a === b | ||
return a === b; | ||
} | ||
return unordered || | ||
(isMap(a) && isMap(b)) || | ||
(isSet(a) && isSet(b)) | ||
? sameIgnoreOrder(a, b, unordered) : sameInOrder(a, b, unordered) | ||
return unordered || (isMap(a) && isMap(b)) || (isSet(a) && isSet(b)) | ||
? sameIgnoreOrder(a, b, unordered) | ||
: sameInOrder(a, b, unordered); | ||
} | ||
function sameInOrder<T>(a: T, b: T, unordered:boolean) { | ||
function sameInOrder<T>(a: T, b: T, unordered: boolean) { | ||
if (!isIterable(a) || !isIterable(b)) { | ||
if (isPlainObject(a) && isPlainObject(b)) { | ||
return sameIgnoreOrder(Object.entries(a), Object.entries(b), unordered) | ||
return sameIgnoreOrder(Object.entries(a), Object.entries(b), unordered); | ||
} | ||
return a === b | ||
return a === b; | ||
} | ||
const itA = a[Symbol.iterator]() | ||
const itB = b[Symbol.iterator]() | ||
const itA = a[Symbol.iterator](); | ||
const itB = b[Symbol.iterator](); | ||
for (let [aa, bb] = [itA.next(), itB.next()]; ; [aa, bb] = [itA.next(), itB.next()]) { | ||
if (!same(aa.value, bb.value, unordered)) { | ||
return false | ||
return false; | ||
} | ||
if (aa.done !== bb.done) { | ||
return false | ||
return false; | ||
} | ||
if (aa.done) { | ||
return true | ||
return true; | ||
} | ||
@@ -55,16 +67,16 @@ } | ||
function sameIgnoreOrder<T>(a: Iterable<T>, b: Iterable<T>, unordered:boolean) { | ||
const [aa, bb] = [histogram(a), histogram(b)] | ||
function sameIgnoreOrder<T>(a: Iterable<T>, b: Iterable<T>, unordered: boolean) { | ||
const [aa, bb] = [histogram(a), histogram(b)]; | ||
if (size(aa) !== size(bb)) { | ||
return false | ||
return false; | ||
} | ||
for (const [key, count] of aa) { | ||
let wasFound = false | ||
let wasFound = false; | ||
for (const [bkey, bcount] of bb) { | ||
if (same(key,bkey, unordered)) { | ||
wasFound = true | ||
if (same(key, bkey, unordered)) { | ||
wasFound = true; | ||
if (bcount !== count) { | ||
return false | ||
return false; | ||
} else { | ||
break | ||
break; | ||
} | ||
@@ -74,7 +86,6 @@ } | ||
if (!wasFound) { | ||
return false | ||
return false; | ||
} | ||
} | ||
return true | ||
return true; | ||
} | ||
@@ -1,7 +0,6 @@ | ||
import { getIn } from "./objects"; | ||
import { getIn } from './objects'; | ||
/** | ||
* Throws if value is not a string | ||
* @param value | ||
* @param errorMessage | ||
*/ | ||
@@ -14,7 +13,5 @@ export function assertIsString(value: any, errorMessage = 'Value is not string'): asserts value is string { | ||
// | ||
/** | ||
* Replaced non alphanumeric character with CSS unicode representation | ||
* @see https://drafts.csswg.org/cssom/#escape-a-character-as-code-point | ||
* @param str | ||
* Replaced non alphanumeric character with {@link https://drafts.csswg.org/cssom/#escape-a-character-as-code-point|CSS unicode representation} | ||
* @returns CSS safe string | ||
@@ -28,3 +25,5 @@ */ | ||
/** | ||
*/ | ||
export enum NamingConvention { | ||
@@ -37,5 +36,5 @@ KebabCase = 'kebab-case', | ||
/** | ||
* | ||
* @param namingConvention | ||
* @returns true if namingConvention is a supported NamingConvention | ||
* Checks if namingConvention is supported | ||
* @returns true if namingConvention is a supported {@link NamingConvention} | ||
*/ | ||
@@ -48,4 +47,3 @@ export function isValidNamingConvention(namingConvention: string): namingConvention is NamingConvention { | ||
* Capitalize the first letter of a string | ||
* @param val | ||
* @returns val with the first letter capitalized | ||
*/ | ||
@@ -58,8 +56,11 @@ export function capitalizeFirstLetter(val: string): string { | ||
* Breaks down a string to words, dropping non letters and numbers | ||
* @example <caption>Spaces</caption> splitIntoWords("Hello world") => ["Hello", "world"] | ||
* @example <caption>Numbers</caption> splitIntoWords("Hello123world") => ["Hello", "123" "world"] | ||
* @example <caption>Acronyms</caption> splitIntoWords("Hello WRL") => ["Hello", "WRL"] | ||
* @example <caption>Capitalized</caption> splitIntoWords("HelloWorld") => ["Hello", "World"] | ||
* @example <caption>Others characters</caption> splitIntoWords("Hello_world--"") => ["Hello", "world"] | ||
* @param str | ||
* @example | ||
* ```ts | ||
* splitIntoWords("Hello world") // => ["Hello", "world"] | ||
* splitIntoWords("Hello123world") // => ["Hello", "123" "world"] | ||
* splitIntoWords("Hello WRL") // => ["Hello", "WRL"] | ||
* splitIntoWords("HelloWorld") // => ["Hello", "World"] | ||
* splitIntoWords("Hello_world--") // => ["Hello", "world"] | ||
* ``` | ||
* @returns An array of words contained in str | ||
@@ -77,4 +78,3 @@ */ | ||
* Converts a string to kebab-case | ||
* @param str | ||
* @returns str in kebab-case-convention | ||
*/ | ||
@@ -87,4 +87,3 @@ export function toKebabCase(str: string): string { | ||
* Converts a string to PascalCase | ||
* @param str | ||
* @returns str in PascalCaseConvention | ||
*/ | ||
@@ -97,7 +96,8 @@ export function toPascalCase(str: string): string { | ||
/** | ||
* Similar to toPascalCase, but drops heading non-letters | ||
* @example toPascalCaseJsIdentifier("123helloWorld") => "HelloWorld" | ||
* @see toPascalCase | ||
* @param str | ||
* @returns str in PascalCaseConvention that starts with a letter | ||
* Similar to {@link toPascalCase}, but drops heading non-letters | ||
* @example | ||
* ```ts | ||
* toPascalCaseJsIdentifier("123helloWorld") // => "HelloWorld" | ||
* ``` | ||
*/ | ||
@@ -111,4 +111,3 @@ export function toPascalCaseJsIdentifier(str: string): string { | ||
* Converts a string to camelCase | ||
* @param str | ||
* @returns str in camelCaseConvention | ||
*/ | ||
@@ -124,5 +123,3 @@ export function toCamelCase(str: string): string { | ||
* Converts string formatting to a naming convention | ||
* @param str | ||
* @param namingConvention | ||
* @returns | ||
*/ | ||
@@ -141,3 +138,4 @@ export function toNamingConvention(str: string, namingConvention: NamingConvention): string { | ||
/** | ||
* like toKebabCase, but prepends '-' if first character of input is UpperCase | ||
* like {@link toKebabCase}, but prepends '-' if first character of input is UpperCase | ||
*/ | ||
@@ -153,3 +151,4 @@ export function toCSSKebabCase(str: string): string { | ||
/** | ||
* like toCamelCase, but capitalizes first character if input starts with '-' | ||
* like {@link toCamelCase}, but capitalizes first character if input starts with '-' | ||
*/ | ||
@@ -165,6 +164,5 @@ export function toCSSCamelCase(str: string): string { | ||
/** | ||
* Finds line an column by position index | ||
* @param content | ||
* @param pos | ||
* @param newline zero based line number and character | ||
* Finds line an column by position index | ||
* @returns zero based line number and character | ||
*/ | ||
@@ -203,6 +201,4 @@ export function indexToLineAndColumn( | ||
/** | ||
* | ||
* @param str | ||
* @param substr | ||
* @returns true is str contains substr, ignoring capitalization | ||
* Checks if str contains substr ignoring capitalization | ||
*/ | ||
@@ -213,2 +209,10 @@ export function includesCaseInsensitive(str: string, substr: string): boolean { | ||
/** | ||
* Matches the indentation of modified to the one of reference | ||
* @param reference - | ||
* @param modified - | ||
* @param newline - | ||
* @returns | ||
*/ | ||
export function equalIdents(reference: string, modified: string, newline = '\n') { | ||
@@ -233,5 +237,6 @@ const referenceArr = reference.split(newline); | ||
* Remove line indentation (heading whitespace) | ||
* @param modified | ||
* @param separator | ||
* @returns | ||
* @param modified- | ||
* @param separator- | ||
* @returns | ||
*/ | ||
@@ -247,2 +252,29 @@ export function noIdents(modified: string, separator = '\n') { | ||
/** | ||
* Shifts all indentation to the left | ||
* using the line with the least indentation as a baseline | ||
*/ | ||
export function minimalIndent(str: string) { | ||
const lines = str.split('\n'); | ||
const min = lines.reduce((min, l) => Math.min(l.replace(/(\s*)(.*)/g, '$1').length, min), 0); | ||
return lines.map((l) => l.slice(min)).join('\n'); | ||
} | ||
/** | ||
* Remove white spaces including empty lines | ||
*/ | ||
export function noWhiteSpace(str: string) { | ||
return str | ||
.split('\n') | ||
.map((line) => line.replaceAll(/\s+/g, ' ').trim()) | ||
.filter((i) => i) | ||
.join('\n'); | ||
} | ||
/** | ||
* Checks is value is a string | ||
* @param value - | ||
* @returns | ||
*/ | ||
export const isString = (value: unknown): value is string => typeof value === 'string'; | ||
@@ -252,7 +284,13 @@ | ||
/** | ||
* @param context A context for the compiler | ||
* Similar to templated string, | ||
* given a fixed context object returns a function that parses strings in it | ||
* @param context- A context for the compiler | ||
* @returns A template compiler function which accepts a template and compile it with `context` | ||
* @example | ||
* templateCompilerProvider({ greetings: 'Hello', person: { name: 'Elad' } })('${greetings} ${person.name}!') | ||
* // => Hello Elad! | ||
* ```ts | ||
* const compile = templateCompilerProvider({ greetings: 'Hello', person: { name: 'Elad' } }) | ||
* compile('${greetings} ${person.name}!')// => Hello Elad! | ||
* compile('${person.name} is awesome')// => Elad is awesome | ||
* ``` | ||
*/ | ||
@@ -268,1 +306,25 @@ export function templateCompilerProvider(context: Record<string, any>) { | ||
} | ||
/** | ||
* Generates a string repeating [str] [count] times | ||
*/ | ||
export function repeat(str: string, count: number) { | ||
return [...new Array<void>(count)].map(() => str).join(''); | ||
} | ||
/** | ||
* Returns a string safe to be used in RegExp | ||
* @see https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex | ||
*/ | ||
export function escapeRegExp(str: string) { | ||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
// $& means the whole matched string | ||
} | ||
/** | ||
* Removes comments from string | ||
* Note that there's lexical no parsing, so stuff like "//'//" will not work | ||
*/ | ||
export function naiveStripComments(str:string) { | ||
return str.replaceAll(/\/\*.+?\*\//gs, '').replaceAll(/\s*(?<!:)\/\/.*\n?/g, '') | ||
} |
import { expect } from 'chai'; | ||
import { getCartesianProductOfArrays } from '..' | ||
import { getCartesianProductOfArrays } from '..'; | ||
@@ -40,2 +40,2 @@ describe('getCartesianProductOfArrays', () => { | ||
}); | ||
}) | ||
}); |
@@ -1,39 +0,41 @@ | ||
import { expect } from "chai" | ||
import { chain } from "../chain" | ||
// eslint-disable-next-line | ||
// @ts-ignore | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
import gc from 'expose-gc/function' | ||
import { expect } from 'chai'; | ||
import { chain } from '../chain'; | ||
// @ts-expect-error no types | ||
import gc from 'expose-gc/function'; | ||
const forceGc = gc as () => void; | ||
describe('performance', () => { | ||
it('it faster than the array equivalent function for large iterables', () => { | ||
const veryLargeArray = Array.from(new Array(2 ** 18)).map((_, i) => i % 2 ** 32) | ||
gc() | ||
const arrayStart = performance.now() | ||
const veryLargeArray = Array.from(new Array(2 ** 18)).map((_, i) => i % 2 ** 32); | ||
forceGc(); | ||
const arrayStart = performance.now(); | ||
const result = veryLargeArray | ||
.map(i => i - 10_000) | ||
.filter(i => i > 0) | ||
.map(i => `${i}`) | ||
.filter(i => i.includes('9')) | ||
.flatMap(i => i.split('')) | ||
.map((i) => i - 10_000) | ||
.filter((i) => i > 0) | ||
.map((i) => `${i}`) | ||
.filter((i) => i.includes('9')) | ||
.flatMap((i) => i.split('')) | ||
// equivalent to "unique" | ||
.reduce((acc, i) => { acc.add(i); return acc }, new Set<string>()) | ||
.values() | ||
gc() | ||
const arrayTime = performance.now() - arrayStart | ||
.reduce((acc, i) => { | ||
acc.add(i); | ||
return acc; | ||
}, new Set<string>()) | ||
.values(); | ||
forceGc(); | ||
const arrayTime = performance.now() - arrayStart; | ||
const iterStart = performance.now() | ||
const iterStart = performance.now(); | ||
const iterResult = chain(veryLargeArray) | ||
.map(i => i - 10_000) | ||
.filter(i => i > 0) | ||
.map(i => `${i}`) | ||
.filter(i => i.includes('9')) | ||
.flatMap(i => i.split('')) | ||
.unique().iterable | ||
gc() | ||
const iterTime = performance.now() - iterStart | ||
.map((i) => i - 10_000) | ||
.filter((i) => i > 0) | ||
.map((i) => `${i}`) | ||
.filter((i) => i.includes('9')) | ||
.flatMap((i) => i.split('')) | ||
.unique().iterable; | ||
forceGc(); | ||
const iterTime = performance.now() - iterStart; | ||
expect([...result]).to.eql([...iterResult]) | ||
expect(iterTime).to.be.lessThan(arrayTime) | ||
}) | ||
}) | ||
expect([...result]).to.eql([...iterResult]); | ||
expect(iterTime).to.be.lessThan(arrayTime); | ||
}); | ||
}); |
@@ -11,11 +11,10 @@ import chai, { expect } from 'chai'; | ||
it('passes the call only once', () => { | ||
const fn = stub().returns(true) | ||
const onceFn = once(fn) | ||
expect(fn).to.not.have.been.called | ||
expect(onceFn(1)).to.equal(true) | ||
expect(onceFn(2)).to.equal(true) | ||
expect(fn).to.have.callCount(1) | ||
expect(fn).to.have.been.calledWith(1) | ||
const fn = stub().returns(true); | ||
const onceFn = once(fn); | ||
expect(fn).to.not.have.been.called; | ||
expect(onceFn(1)).to.equal(true); | ||
expect(onceFn(2)).to.equal(true); | ||
expect(fn).to.have.callCount(1); | ||
expect(fn).to.have.been.calledWith(1); | ||
}); | ||
}); | ||
@@ -1,39 +0,63 @@ | ||
import { expect } from "chai" | ||
import { last, at, first, unique, next, prev, concat, flat, map, flatMap, filter, forEach, find, includes, some, every, isEmpty, size, reduce, groupBy } from ".." | ||
import { expect } from 'chai'; | ||
import { | ||
last, | ||
at, | ||
first, | ||
unique, | ||
next, | ||
prev, | ||
concat, | ||
flat, | ||
map, | ||
flatMap, | ||
filter, | ||
forEach, | ||
find, | ||
includes, | ||
some, | ||
every, | ||
isEmpty, | ||
size, | ||
reduce, | ||
groupBy, | ||
join, | ||
skip, | ||
} from '../iterables'; | ||
describe('iterables', () => { | ||
it('last', () => { | ||
expect(last([1, 2, 3])).to.equal(3) | ||
expect(last([])).to.equal(undefined) | ||
}) | ||
expect(last([1, 2, 3])).to.equal(3); | ||
expect(last([])).to.equal(undefined); | ||
}); | ||
it('first', () => { | ||
expect(first([1, 2, 3])).to.equal(1) | ||
expect(first([])).to.equal(undefined) | ||
}) | ||
expect(first([1, 2, 3])).to.equal(1); | ||
expect(first([])).to.equal(undefined); | ||
}); | ||
it('find', () => { | ||
expect(find([0, 1, 2], i => !!i)).to.equal(1) | ||
}) | ||
expect(find([0, 1, 2], (i) => !!i)).to.equal(1); | ||
}); | ||
it('includes', () => { | ||
expect(includes([0, 1, 2], 2)).to.equal(true) | ||
}) | ||
expect(includes([0, 1, 2], 2)).to.equal(true); | ||
}); | ||
it('some', () => { | ||
expect(some([0, 1, 2], i => i == 1)).to.equal(true) | ||
expect(some([0, 2], i => i == 1)).to.equal(false) | ||
}) | ||
expect(some([0, 1, 2], (i) => i == 1)).to.equal(true); | ||
expect(some([0, 2], (i) => i == 1)).to.equal(false); | ||
}); | ||
it('every', () => { | ||
expect(every([1, 2], i => !!i)).to.equal(true) | ||
expect(every([0, 1, 2], i => !!i)).to.equal(false) | ||
}) | ||
expect(every([1, 2], (i) => !!i)).to.equal(true); | ||
expect(every([0, 1, 2], (i) => !!i)).to.equal(false); | ||
}); | ||
describe('at', () => { | ||
it('returns the element at a positive index', () => { | ||
expect(at([1, 2, 3], 0)).to.equal(1) | ||
expect(at([1, 2, 3], 1)).to.equal(2) | ||
}) | ||
expect(at([1, 2, 3], 0)).to.equal(1); | ||
expect(at([1, 2, 3], 1)).to.equal(2); | ||
}); | ||
it('returns the element at a negative index', () => { | ||
expect(at([1, 2, 3], -1)).to.equal(3) | ||
}) | ||
expect(at([1, 2, 3], -1)).to.equal(3); | ||
}); | ||
it('returns undefined when out of bounds', () => { | ||
expect(at([1, 2, 3], 4)).to.equal(undefined) | ||
expect(at([1, 2, 3], -4)).to.equal(undefined) | ||
}) | ||
}) | ||
expect(at([1, 2, 3], 4)).to.equal(undefined); | ||
expect(at([1, 2, 3], -4)).to.equal(undefined); | ||
}); | ||
}); | ||
@@ -43,3 +67,3 @@ describe('unique', () => { | ||
expect([...unique(['1', 1, 1, true])]).to.eql(['1', 1, true]); | ||
expect([...unique([0,1,2,3], i => i%2)]).to.eql([0,1]); | ||
expect([...unique([0, 1, 2, 3], (i) => i % 2)]).to.eql([0, 1]); | ||
}); | ||
@@ -72,42 +96,60 @@ }); | ||
expect([...concat([0, 1, 2], [3], [4, 5])]).to.eql([0, 1, 2, 3, 4, 5]); | ||
}) | ||
}); | ||
it(`flat`, () => { | ||
expect([...flat([0, [1, 2]])]).to.eql([0, 1, 2]); | ||
expect([...flat([0, [1, [2]]], true)]).to.eql([0, 1, 2]); | ||
}) | ||
}); | ||
it(`isEmpty`, () => { | ||
expect(isEmpty([])).to.eql(true); | ||
expect(isEmpty([1])).to.eql(false); | ||
}) | ||
}); | ||
it(`size`, () => { | ||
expect(size([])).to.eql(0); | ||
expect(size([0, 1])).to.eql(2); | ||
}) | ||
}); | ||
it(`map`, () => { | ||
expect([...map([0, 1, 2], i => i ** 2)]).to.eql([0, 1, 4]); | ||
}) | ||
expect([...map([0, 1, 2], (i) => i ** 2)]).to.eql([0, 1, 4]); | ||
}); | ||
it(`flatMap`, () => { | ||
expect([...flatMap([0, 1, 2], i => i ? [i, i] : i)]).to.eql([0, 1, 1, 2, 2]); | ||
}) | ||
expect([...flatMap([0, 1, 2], (i) => (i ? [i, i] : i))]).to.eql([0, 1, 1, 2, 2]); | ||
}); | ||
it(`filter`, () => { | ||
expect([...filter([0, 1, 2], i => !!i)]).to.eql([1, 2]); | ||
}) | ||
expect([...filter([0, 1, 2], (i) => !!i)]).to.eql([1, 2]); | ||
}); | ||
it(`forEach`, () => { | ||
const r = [] as number[] | ||
forEach([0, 1, 2], i => r.push(i)) | ||
const r = [] as number[]; | ||
forEach([0, 1, 2], (i) => r.push(i)); | ||
expect(r).to.eql([0, 1, 2]); | ||
}) | ||
it('reduce', ()=>{ | ||
const input = [0,1,2,3] | ||
const reducer = (acc:number, i:number) => acc + i | ||
expect(reduce(input, reducer, 0)).to.equal(input.reduce(reducer, 0)) | ||
}) | ||
it('groupBy', ()=>{ | ||
expect(groupBy([{a:1, b:0},{a:1,b:1}, {a:2}], 'a')).to.eql( | ||
}); | ||
it('reduce', () => { | ||
const input = [0, 1, 2, 3]; | ||
const reducer = (acc: number, i: number) => acc + i; | ||
expect(reduce(input, reducer, 0)).to.equal(input.reduce(reducer, 0)); | ||
}); | ||
it('join', () => { | ||
expect(join(['0', '1', '2'], ',')).to.eql('0,1,2'); | ||
expect(join([], ',')).to.eql(''); | ||
expect(join(null, ',')).to.eql(''); | ||
expect(join(['0'], ',')).to.eql('0'); | ||
}); | ||
it('skip', () => { | ||
expect([...skip([0, 1, 2, 3], 1)]).to.eql([1, 2, 3]); | ||
expect([...skip([], 1)]).to.eql([]); | ||
expect([...skip([0, 1], 0)]).to.eql([0, 1]); | ||
expect([...skip([0, 1], 3)]).to.eql([]); | ||
}); | ||
it('groupBy', () => { | ||
expect(groupBy([{ a: 1, b: 0 }, { a: 1, b: 1 }, { a: 2 }], 'a')).to.eql( | ||
new Map([ | ||
[1, [{a:1,b:0}, {a:1, b:1}]], | ||
[2, [{a:2}]] | ||
[ | ||
1, | ||
[ | ||
{ a: 1, b: 0 }, | ||
{ a: 1, b: 1 }, | ||
], | ||
], | ||
[2, [{ a: 2 }]], | ||
]) | ||
) | ||
}) | ||
}) | ||
); | ||
}); | ||
}); |
@@ -1,23 +0,23 @@ | ||
import { expect } from "chai" | ||
import { getValue } from "../maps" | ||
import { expect } from 'chai'; | ||
import { getValue } from '../maps'; | ||
describe('maps', ()=>{ | ||
describe('maps', () => { | ||
describe('getValue', () => { | ||
const map = new Map([[0, 1]]) | ||
const obj = {'0':1} | ||
const map = new Map([[0, 1]]); | ||
const obj = { '0': 1 }; | ||
it('thrown when the value is missing', () => { | ||
expect(() => { | ||
getValue(map, 2, 'missing!') | ||
}).to.throw('missing!') | ||
}) | ||
getValue(map, 2, 'missing!'); | ||
}).to.throw('missing!'); | ||
}); | ||
it('thrown when the map is nullish', () => { | ||
expect(() => { | ||
getValue(null, 2, 'no map') | ||
}).to.throw('no map') | ||
}) | ||
getValue(null, 2, 'no map'); | ||
}).to.throw('no map'); | ||
}); | ||
it('returns the value', () => { | ||
expect(getValue(map, 0)).to.equal(1) | ||
expect(getValue(obj, '0')).to.equal(1) | ||
}) | ||
}) | ||
}) | ||
expect(getValue(map, 0)).to.equal(1); | ||
expect(getValue(obj, '0')).to.equal(1); | ||
}); | ||
}); | ||
}); |
@@ -14,15 +14,19 @@ import { expect } from 'chai'; | ||
it(`merges to POJOs`, () => { | ||
expect(defaults({ a: 0, b: 1 }, { a: 1, b: 2, c: 3 })).to.eql({ a: 0, b: 1, c: 3 }) | ||
}) | ||
expect(defaults({ a: 0, b: 1 }, { a: 1, b: 2, c: 3 })).to.eql({ a: 0, b: 1, c: 3 }); | ||
}); | ||
it(`deep=true (default)`, () => { | ||
expect(defaults({ a: { a: 10, b: 20 } }, { a: { a: 20, c: 30 } })).to.eql({ a: { a: 10, b: 20, c: 30 } }) | ||
}) | ||
expect(defaults({ a: { a: 10, b: 20 } }, { a: { a: 20, c: 30 } })).to.eql({ a: { a: 10, b: 20, c: 30 } }); | ||
}); | ||
it(`deep=false`, () => { | ||
expect(defaults({ a: { a: 10 } }, { a: { b: 20 } }, false)).to.eql({ a: { a: 10 } }) | ||
}) | ||
expect(defaults({ a: { a: 10 } }, { a: { b: 20 } }, false)).to.eql({ a: { a: 10 } }); | ||
}); | ||
it(`shouldUseDefault arg`, () => { | ||
expect(defaults({ a: { a: 10, b: 20 } }, { a: { a: 20, c: 30 } }, true, i => i === 10)).to.eql({ a: { a: 20, b: 20, c: 30 } }) | ||
expect(defaults({ a: { a: 10, b: 20 } }, { a: { a: 20, c: 30 } }, true, (_, key) => key === 'a.a')).to.eql({ a: { a: 20, b: 20, c: 30 } }) | ||
}) | ||
}) | ||
expect(defaults({ a: { a: 10, b: 20 } }, { a: { a: 20, c: 30 } }, true, (i) => i === 10)).to.eql({ | ||
a: { a: 20, b: 20, c: 30 }, | ||
}); | ||
expect(defaults({ a: { a: 10, b: 20 } }, { a: { a: 20, c: 30 } }, true, (_, key) => key === 'a.a')).to.eql({ | ||
a: { a: 20, b: 20, c: 30 }, | ||
}); | ||
}); | ||
}); | ||
@@ -32,25 +36,25 @@ describe('remap', () => { | ||
const input = { a: 0, b: 1 }; | ||
const output: { c: number, b: number } = remap(input, { a: 'c' }) | ||
expect(output).to.eql({ c: 0, b: 1 }) | ||
}) | ||
const output: { c: number; b: number } = remap(input, { a: 'c' }); | ||
expect(output).to.eql({ c: 0, b: 1 }); | ||
}); | ||
it('removes keys mapped to remap.DELETE', () => { | ||
const input = { a: 0, b: 1 }; | ||
const output: { b: number } = remap(input, { a: remap.DELETE }) | ||
expect(output).to.eql({ b: 1 }) | ||
}) | ||
}) | ||
const output: { b: number } = remap(input, { a: remap.DELETE }); | ||
expect(output).to.eql({ b: 1 }); | ||
}); | ||
}); | ||
describe('isPlainObject', () => { | ||
it('returns true for POJO', () => { | ||
class A { } | ||
expect(isPlainObject({})).to.be.true | ||
expect(isPlainObject(null)).to.be.false | ||
expect(isPlainObject(undefined)).to.be.false | ||
expect(isPlainObject(new A())).to.be.false | ||
expect(isPlainObject(new Map())).to.be.false | ||
}) | ||
class A {} | ||
expect(isPlainObject({})).to.be.true; | ||
expect(isPlainObject(null)).to.be.false; | ||
expect(isPlainObject(undefined)).to.be.false; | ||
expect(isPlainObject(new A())).to.be.false; | ||
expect(isPlainObject(new Map())).to.be.false; | ||
}); | ||
it('allows direct access to fields', () => { | ||
const a = { a: 1 } | ||
expect(isPlainObject(a) && a.a).to.eql(1) | ||
}) | ||
}) | ||
const a = { a: 1 }; | ||
expect(isPlainObject(a) && a.a).to.eql(1); | ||
}); | ||
}); |
@@ -1,79 +0,79 @@ | ||
import { expect } from "chai" | ||
import { expect } from 'chai'; | ||
// eslint-disable-next-line | ||
// @ts-ignore | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
import { same } from "../same" | ||
import { toMap } from "../types" | ||
import { same } from '../same'; | ||
import { toMap } from '../types'; | ||
describe('same', () => { | ||
describe('types unaffected by "unordered" param', ()=>{ | ||
describe('types unaffected by "unordered" param', () => { | ||
it('compares native values', () => { | ||
expect(same(1, 1)).to.eql(true) | ||
expect(same(1, 0)).to.eql(false) | ||
expect(same(null, undefined)).to.eql(false) | ||
expect(same(null, null)).to.eql(true) | ||
expect(same('a', 'a')).to.eql(true) | ||
expect(same('a', 'b')).to.eql(false) | ||
}) | ||
expect(same(1, 1)).to.eql(true); | ||
expect(same(1, 0)).to.eql(false); | ||
expect(same(null, undefined)).to.eql(false); | ||
expect(same(null, null)).to.eql(true); | ||
expect(same('a', 'a')).to.eql(true); | ||
expect(same('a', 'b')).to.eql(false); | ||
}); | ||
it('compares POJOs', () => { | ||
expect(same({}, {})).to.eql(true) | ||
expect(same({ a: 1 }, { a: 1 })).to.eql(true) | ||
expect(same({ a: 1 }, { a: 2 })).to.eql(false) | ||
expect(same({ a: undefined }, {})).to.eql(false) | ||
expect(same({ a: undefined }, { a: null })).to.eql(false) | ||
expect(same({ a: 1, b: 2 }, { b: 2, a: 1 })).to.eql(true) | ||
expect(same({ a: { b: 1 } }, { a: { b: 1 } })).to.eql(true) | ||
expect(same({ a: { b: 1 } }, { a: { b: 2 } })).to.eql(false) | ||
}) | ||
expect(same({}, {})).to.eql(true); | ||
expect(same({ a: 1 }, { a: 1 })).to.eql(true); | ||
expect(same({ a: 1 }, { a: 2 })).to.eql(false); | ||
expect(same({ a: undefined }, {})).to.eql(false); | ||
expect(same({ a: undefined }, { a: null })).to.eql(false); | ||
expect(same({ a: 1, b: 2 }, { b: 2, a: 1 })).to.eql(true); | ||
expect(same({ a: { b: 1 } }, { a: { b: 1 } })).to.eql(true); | ||
expect(same({ a: { b: 1 } }, { a: { b: 2 } })).to.eql(false); | ||
}); | ||
it('compares Maps', () => { | ||
expect(same(new Map(), new Map())).to.eql(true) | ||
expect(same(toMap({}), toMap({}))).to.eql(true) | ||
expect(same(toMap({ a: 1 }), toMap({ a: 1 }))).to.eql(true) | ||
expect(same(toMap({ a: 1 }), toMap({ a: 2 }))).to.eql(false) | ||
expect(same(toMap({ a: undefined }), toMap({}))).to.eql(false) | ||
expect(same(toMap({ a: undefined }), toMap({ a: null }))).to.eql(false) | ||
expect(same(toMap({ a: 1, b: 2 }), toMap({ b: 2, a: 1 }))).to.eql(true) | ||
expect(same(toMap({ a: { b: 1 } }), toMap({ a: { b: 1 } }))).to.eql(true) | ||
expect(same(toMap({ a: { b: 1 } }), toMap({ a: { b: 2 } }))).to.eql(false) | ||
}) | ||
expect(same(new Map(), new Map())).to.eql(true); | ||
expect(same(toMap({}), toMap({}))).to.eql(true); | ||
expect(same(toMap({ a: 1 }), toMap({ a: 1 }))).to.eql(true); | ||
expect(same(toMap({ a: 1 }), toMap({ a: 2 }))).to.eql(false); | ||
expect(same(toMap({ a: undefined }), toMap({}))).to.eql(false); | ||
expect(same(toMap({ a: undefined }), toMap({ a: null }))).to.eql(false); | ||
expect(same(toMap({ a: 1, b: 2 }), toMap({ b: 2, a: 1 }))).to.eql(true); | ||
expect(same(toMap({ a: { b: 1 } }), toMap({ a: { b: 1 } }))).to.eql(true); | ||
expect(same(toMap({ a: { b: 1 } }), toMap({ a: { b: 2 } }))).to.eql(false); | ||
}); | ||
it('compares Sets', () => { | ||
expect(same(new Set(), new Set())).to.eql(true) | ||
expect(same(new Set([1]), new Set([1]))).to.eql(true) | ||
expect(same(new Set([1, 2, 3]), new Set([1, 2, 3]))).to.eql(true) | ||
expect(same(new Set([1]), new Set([2]))).to.eql(false) | ||
}) | ||
}) | ||
expect(same(new Set(), new Set())).to.eql(true); | ||
expect(same(new Set([1]), new Set([1]))).to.eql(true); | ||
expect(same(new Set([1, 2, 3]), new Set([1, 2, 3]))).to.eql(true); | ||
expect(same(new Set([1]), new Set([2]))).to.eql(false); | ||
}); | ||
}); | ||
describe('ordered', () => { | ||
it('compares flat arrays', () => { | ||
expect(same([],[])).to.eql(true) | ||
expect(same([1,2],[1,2])).to.eql(true) | ||
expect(same([1,2],[2,1])).to.eql(false) | ||
}) | ||
expect(same([], [])).to.eql(true); | ||
expect(same([1, 2], [1, 2])).to.eql(true); | ||
expect(same([1, 2], [2, 1])).to.eql(false); | ||
}); | ||
it('compares iterables', () => { | ||
expect(same([1,2][Symbol.iterator](),[1,2][Symbol.iterator]())).to.eql(true) | ||
expect(same([1,2][Symbol.iterator](),[2,1][Symbol.iterator]())).to.eql(false) | ||
}) | ||
expect(same([1, 2][Symbol.iterator](), [1, 2][Symbol.iterator]())).to.eql(true); | ||
expect(same([1, 2][Symbol.iterator](), [2, 1][Symbol.iterator]())).to.eql(false); | ||
}); | ||
it('compares nested arrays', () => { | ||
expect(same([[[]]],[[[]]])).to.eql(true) | ||
expect(same([1,[2,3]],[1,[2,3]])).to.eql(true) | ||
expect(same([1,[3,2]],[1,[3,2]])).to.eql(true) | ||
}) | ||
it('ignores the order within objects, maps and sets', ()=>{ | ||
expect(same([{b:0, a:1}],[{a:1, b:0}])).to.eql(true) | ||
expect(same([toMap({b:0, a:1})],[toMap({a:1, b:0})])).to.eql(true) | ||
expect(same([new Set([1,2])],[new Set([2,1])])).to.eql(true) | ||
}) | ||
it('checks the order of arrays within objects', ()=>{ | ||
expect(same([{a:[0,1]}],[{a:[1,0]}])).to.eql(false) | ||
}) | ||
}) | ||
describe('unordered array', ()=>{ | ||
it('ignores order of flat arrays', ()=>{ | ||
expect(same([0,1,2],[2,1,0], true)).to.eql(true) | ||
expect(same([0,1,2],[2,1,0], true)).to.eql(true) | ||
}) | ||
it('ignores order of nested arrays', ()=>{ | ||
expect(same([0,[1,2]],[[2,1],0], true)).to.eql(true) | ||
}) | ||
}) | ||
}) | ||
expect(same([[[]]], [[[]]])).to.eql(true); | ||
expect(same([1, [2, 3]], [1, [2, 3]])).to.eql(true); | ||
expect(same([1, [3, 2]], [1, [3, 2]])).to.eql(true); | ||
}); | ||
it('ignores the order within objects, maps and sets', () => { | ||
expect(same([{ b: 0, a: 1 }], [{ a: 1, b: 0 }])).to.eql(true); | ||
expect(same([toMap({ b: 0, a: 1 })], [toMap({ a: 1, b: 0 })])).to.eql(true); | ||
expect(same([new Set([1, 2])], [new Set([2, 1])])).to.eql(true); | ||
}); | ||
it('checks the order of arrays within objects', () => { | ||
expect(same([{ a: [0, 1] }], [{ a: [1, 0] }])).to.eql(false); | ||
}); | ||
}); | ||
describe('unordered array', () => { | ||
it('ignores order of flat arrays', () => { | ||
expect(same([0, 1, 2], [2, 1, 0], true)).to.eql(true); | ||
expect(same([0, 1, 2], [2, 1, 0], true)).to.eql(true); | ||
}); | ||
it('ignores order of nested arrays', () => { | ||
expect(same([0, [1, 2]], [[2, 1], 0], true)).to.eql(true); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
import { splitIntoWords, toCamelCase, toKebabCase, toPascalCase } from '..'; | ||
import { noWhiteSpace, repeat, splitIntoWords, naiveStripComments, toCamelCase, toKebabCase, toPascalCase } from '..'; | ||
import { expect } from 'chai'; | ||
@@ -37,1 +37,30 @@ | ||
}); | ||
it('repeat', () => { | ||
expect(repeat('[]', 3)).to.eql('[][][]'); | ||
}); | ||
it('noWhiteSpace', () => { | ||
expect(noWhiteSpace('no whitespace')).to.equal('no whitespace'); | ||
expect(noWhiteSpace('\t\ttabs\t')).to.equal('tabs'); | ||
expect(noWhiteSpace(' single line ')).to.equal('single line'); | ||
expect(noWhiteSpace(' multiple \r\n lines ')).to.equal('multiple\nlines'); | ||
expect(noWhiteSpace(' empty \n\n \n lines ')).to.equal('empty\nlines'); | ||
}); | ||
describe('stripComments', ()=>{ | ||
it('removes /* */ comments', ()=>{ | ||
expect(naiveStripComments(`no /* success removing */comments`)).to.equal('no comments') | ||
expect(naiveStripComments(`no /* | ||
success removing | ||
*/comments`)).to.equal('no comments') | ||
}) | ||
it('removes // comments', ()=>{ | ||
expect(naiveStripComments(`no comments// after code`)).to.equal('no comments') | ||
expect(naiveStripComments(`// line comments\nno comments`)).to.equal('no comments') | ||
}) | ||
it('does not identify :// as comment', ()=>{ | ||
expect(naiveStripComments(`http://url //my site`)).to.equal('http://url') | ||
}) | ||
}) |
{ | ||
"extends": "../../../tsconfig.base.json", | ||
"extends": "./tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "../dist/esm", | ||
"module": "ESNext" | ||
}, | ||
"references": [] | ||
"module": "esnext" | ||
} | ||
} |
@@ -0,8 +1,45 @@ | ||
/** | ||
* The resolved value of T (if a promise, otherwise simply T) | ||
*/ | ||
export type Awaited<T> = T extends PromiseLike<infer U> ? U : T; | ||
export type Nullable<T> = T|null|undefined | ||
export const isMap = <K = any, V = any>(m: any): m is Map<K, V> => m instanceof Map | ||
export const isSet = <V = any>(m: any): m is Set<V> => m instanceof Set | ||
/** | ||
* T or null/undefined | ||
*/ | ||
export type Nullable<T> = T | null | undefined; | ||
/** | ||
* Validates s is an instance of Map | ||
* @returns true if s is a Map | ||
*/ | ||
export const isMap = <K = any, V = any>(m: any): m is Map<K, V> => m instanceof Map; | ||
/** | ||
* Validates s is an instance of Set | ||
* @returns true if s is a Set | ||
*/ | ||
export const isSet = <V = any>(s: any): s is Set<V> => s instanceof Set; | ||
/** | ||
* Given a value of type Nullable<T>, validates value is T | ||
* @returns true if value is defined (not null or undefined) | ||
*/ | ||
export function isDefined<T>(value: Nullable<T>): value is T { | ||
return value !== null && value !== undefined; | ||
} | ||
export const toMap = (a:object) => new Map(Object.entries(a)) | ||
/** | ||
* Coverts and object into a Map | ||
* @param obj - POJO | ||
* @returns A map with the same entries as obj | ||
*/ | ||
export const toMap = (obj: object) => new Map(Object.entries(obj)); | ||
/** | ||
* Make an intersection type from union | ||
* @example | ||
* ```ts | ||
* const a:UnionToIntersection<{a:string}|{b:string}> = {a:'ok', b:'also ok'} | ||
* ``` | ||
*/ | ||
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
296295
5582
0
156
1
147
1