diff --git a/src/__tests__/and.spec.ts b/src/core/__tests__/and.spec.ts similarity index 100% rename from src/__tests__/and.spec.ts rename to src/core/__tests__/and.spec.ts diff --git a/src/__tests__/apply.spec.ts b/src/core/__tests__/apply.spec.ts similarity index 100% rename from src/__tests__/apply.spec.ts rename to src/core/__tests__/apply.spec.ts diff --git a/src/__tests__/chain.spec.ts b/src/core/__tests__/chain.spec.ts similarity index 100% rename from src/__tests__/chain.spec.ts rename to src/core/__tests__/chain.spec.ts diff --git a/src/__tests__/cond.spec.ts b/src/core/__tests__/cond.spec.ts similarity index 100% rename from src/__tests__/cond.spec.ts rename to src/core/__tests__/cond.spec.ts diff --git a/src/__tests__/constant.spec.ts b/src/core/__tests__/constant.spec.ts similarity index 100% rename from src/__tests__/constant.spec.ts rename to src/core/__tests__/constant.spec.ts diff --git a/src/__tests__/equals.spec.ts b/src/core/__tests__/equals.spec.ts similarity index 100% rename from src/__tests__/equals.spec.ts rename to src/core/__tests__/equals.spec.ts diff --git a/src/__tests__/first.spec.ts b/src/core/__tests__/first.spec.ts similarity index 100% rename from src/__tests__/first.spec.ts rename to src/core/__tests__/first.spec.ts diff --git a/src/__tests__/greaterThan.spec.ts b/src/core/__tests__/greaterThan.spec.ts similarity index 100% rename from src/__tests__/greaterThan.spec.ts rename to src/core/__tests__/greaterThan.spec.ts diff --git a/src/__tests__/greaterThanEquals.spec.ts b/src/core/__tests__/greaterThanEquals.spec.ts similarity index 100% rename from src/__tests__/greaterThanEquals.spec.ts rename to src/core/__tests__/greaterThanEquals.spec.ts diff --git a/src/__tests__/identity.spec.ts b/src/core/__tests__/identity.spec.ts similarity index 100% rename from src/__tests__/identity.spec.ts rename to src/core/__tests__/identity.spec.ts diff --git a/src/__tests__/index.spec.ts b/src/core/__tests__/index.spec.ts similarity index 100% rename from src/__tests__/index.spec.ts rename to src/core/__tests__/index.spec.ts diff --git a/src/__tests__/isBetween.spec.ts b/src/core/__tests__/isBetween.spec.ts similarity index 100% rename from src/__tests__/isBetween.spec.ts rename to src/core/__tests__/isBetween.spec.ts diff --git a/src/__tests__/last.spec.ts b/src/core/__tests__/last.spec.ts similarity index 100% rename from src/__tests__/last.spec.ts rename to src/core/__tests__/last.spec.ts diff --git a/src/__tests__/lessThan.spec.ts b/src/core/__tests__/lessThan.spec.ts similarity index 100% rename from src/__tests__/lessThan.spec.ts rename to src/core/__tests__/lessThan.spec.ts diff --git a/src/__tests__/lessThanEquals.ts b/src/core/__tests__/lessThanEquals.ts similarity index 100% rename from src/__tests__/lessThanEquals.ts rename to src/core/__tests__/lessThanEquals.ts diff --git a/src/__tests__/map.spec.ts b/src/core/__tests__/map.spec.ts similarity index 100% rename from src/__tests__/map.spec.ts rename to src/core/__tests__/map.spec.ts diff --git a/src/__tests__/not.spec.ts b/src/core/__tests__/not.spec.ts similarity index 100% rename from src/__tests__/not.spec.ts rename to src/core/__tests__/not.spec.ts diff --git a/src/__tests__/nth.spec.ts b/src/core/__tests__/nth.spec.ts similarity index 100% rename from src/__tests__/nth.spec.ts rename to src/core/__tests__/nth.spec.ts diff --git a/src/__tests__/omit.spec.ts b/src/core/__tests__/omit.spec.ts similarity index 100% rename from src/__tests__/omit.spec.ts rename to src/core/__tests__/omit.spec.ts diff --git a/src/__tests__/or.spec.ts b/src/core/__tests__/or.spec.ts similarity index 100% rename from src/__tests__/or.spec.ts rename to src/core/__tests__/or.spec.ts diff --git a/src/__tests__/pipe.spec.ts b/src/core/__tests__/pipe.spec.ts similarity index 100% rename from src/__tests__/pipe.spec.ts rename to src/core/__tests__/pipe.spec.ts diff --git a/src/__tests__/pluck.spec.ts b/src/core/__tests__/pluck.spec.ts similarity index 100% rename from src/__tests__/pluck.spec.ts rename to src/core/__tests__/pluck.spec.ts diff --git a/src/__tests__/rest.spec.ts b/src/core/__tests__/rest.spec.ts similarity index 100% rename from src/__tests__/rest.spec.ts rename to src/core/__tests__/rest.spec.ts diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..ea1493e --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,131 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const index = (i: number) => (items: T[]): T | undefined => items[i] + +export const nth = (n: number) => index(n - 1) + +export const first = index(0) + +export const last = (items: T[]): T => items[items.length - 1] + +export const rest = ([, ...remaining]: T[]) => remaining + +export const constant = (value: T) => () => value + +export const identity = (value: T) => value + +export const not = (fn: (...args: Args) => T) => ( + ...args: Args +) => !fn(...args) + +export const equals = (value: T) => (data: T): boolean => value === data + +export const greaterThan = (value: number) => (data: number): boolean => + data > value + +export const greaterThanEquals = (value: number) => (data: number): boolean => + data >= value + +export const lessThan = (value: number) => (data: number): boolean => + data < value + +export const lessThanEquals = (value: number) => (data: number): boolean => + data <= value + +export const or = ( + ...options: Array<(...args: Args) => unknown> +) => (...args: Args): boolean => options.some(fn => fn(...args)) + +export const and = ( + ...options: Array<(...args: Args) => unknown> +) => (...args: Args): boolean => options.every(fn => fn(...args)) + +export const isBetween = (a: number, b: number, inclusive = false) => + inclusive + ? and(greaterThanEquals(a), lessThanEquals(b)) + : and(greaterThan(a), lessThan(b)) + +export const pluck = (key: T) => (data: U): U[T] => + data[key] + +export const omit = (key: T) => (data: U): Omit => { + const cloned = { ...data } + delete cloned[key] + return cloned +} + +export function pipe( + a: (data: A) => B, + b: (data: B) => C, + c: (data: C) => D, + d: (data: D) => E, +): (data: A) => E +export function pipe( + a: (data: A) => B, + b: (data: B) => C, + c: (data: C) => D, +): (data: A) => D +export function pipe( + a: (data: A) => B, + b: (data: B) => C, +): (data: A) => C +export function pipe(...fns: ((data: any) => any)[]) { + return (data: any) => fns.reduce((sum, fn) => fn(sum), data) +} + +export function apply(fn: (a: A) => T): (data: [A]) => T +export function apply(fn: (a: A, b: B) => T): (data: [A, B]) => T +export function apply( + fn: (a: A, b: B, c: C) => T, +): (data: [A, B, C]) => T +export function apply(fn: (...args: any[]) => T): (args: any) => T { + return (args: any) => fn(...args) +} + +export const map = (fn: (a: T) => U) => (data: T[]): U[] => data.map(fn) +export const chain = (fn: (a: T) => U) => (data: T): U => fn(data) + +type Predicate = (data: T) => unknown +type Transform = (data: T) => U +type Condition = [Predicate, Transform] + +export function cond( + conditions: Condition[], +): (data: T) => U | undefined +export function cond( + conditions: Condition[], + orElse: Transform, +): (data: T) => U +export function cond( + conditions: Condition[], + orElse?: Transform, +): (data: T) => U | undefined { + return (data: T) => { + const found = conditions.find(([predicate]) => predicate(data)) + + if (found) { + return found[1](data) + } + + return orElse ? orElse(data) : undefined + } +} + +export function isPlainObject(obj: unknown) { + // Basic check for Type object that's not null + if (typeof obj == "object" && obj !== null) { + // If Object.getPrototypeOf supported, use it + if (typeof Object.getPrototypeOf == "function") { + const proto = Object.getPrototypeOf(obj) + return proto === Object.prototype || proto === null + } + + // Otherwise, use internal class + // This should be reliable as if getPrototypeOf not supported, is pre-ES5 + return Object.prototype.toString.call(obj) == "[object Object]" + } + + // Not an object + return false +} + +export const isUndefined = (data: unknown) => data === undefined diff --git a/src/index.ts b/src/index.ts index 497104e..86307b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,111 +1,2 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export const index = (i: number) => (items: T[]): T | undefined => items[i] - -export const nth = (n: number) => index(n - 1) - -export const first = index(0) - -export const last = (items: T[]): T => items[items.length - 1] - -export const rest = ([, ...remaining]: T[]) => remaining - -export const constant = (value: T) => () => value - -export const identity = (value: T) => value - -export const not = (fn: (...args: Args) => T) => ( - ...args: Args -) => !fn(...args) - -export const equals = (value: T) => (data: T): boolean => value === data - -export const greaterThan = (value: number) => (data: number): boolean => - data > value - -export const greaterThanEquals = (value: number) => (data: number): boolean => - data >= value - -export const lessThan = (value: number) => (data: number): boolean => - data < value - -export const lessThanEquals = (value: number) => (data: number): boolean => - data <= value - -export const or = ( - ...options: Array<(...args: Args) => unknown> -) => (...args: Args): boolean => options.some(fn => fn(...args)) - -export const and = ( - ...options: Array<(...args: Args) => unknown> -) => (...args: Args): boolean => options.every(fn => fn(...args)) - -export const isBetween = (a: number, b: number, inclusive = false) => - inclusive - ? and(greaterThanEquals(a), lessThanEquals(b)) - : and(greaterThan(a), lessThan(b)) - -export const pluck = (key: T) => (data: U): U[T] => - data[key] - -export const omit = (key: T) => (data: U): Omit => { - const cloned = { ...data } - delete cloned[key] - return cloned -} - -export function pipe( - a: (data: A) => B, - b: (data: B) => C, - c: (data: C) => D, - d: (data: D) => E, -): (data: A) => E -export function pipe( - a: (data: A) => B, - b: (data: B) => C, - c: (data: C) => D, -): (data: A) => D -export function pipe( - a: (data: A) => B, - b: (data: B) => C, -): (data: A) => C -export function pipe(...fns: ((data: any) => any)[]) { - return (data: any) => fns.reduce((sum, fn) => fn(sum), data) -} - -export function apply(fn: (a: A) => T): (data: [A]) => T -export function apply(fn: (a: A, b: B) => T): (data: [A, B]) => T -export function apply( - fn: (a: A, b: B, c: C) => T, -): (data: [A, B, C]) => T -export function apply(fn: (...args: any[]) => T): (args: any) => T { - return (args: any) => fn(...args) -} - -export const map = (fn: (a: T) => U) => (data: T[]): U[] => data.map(fn) -export const chain = (fn: (a: T) => U) => (data: T): U => fn(data) - -type Predicate = (data: T) => unknown -type Transform = (data: T) => U -type Condition = [Predicate, Transform] - -export function cond( - conditions: Condition[], -): (data: T) => U | undefined -export function cond( - conditions: Condition[], - orElse: Transform, -): (data: T) => U -export function cond( - conditions: Condition[], - orElse?: Transform, -): (data: T) => U | undefined { - return (data: T) => { - const found = conditions.find(([predicate]) => predicate(data)) - - if (found) { - return found[1](data) - } - - return orElse ? orElse(data) : undefined - } -} +export * from "./core/index" +export * from "./memo/index" diff --git a/src/memo/__tests__/memoize.spec.ts b/src/memo/__tests__/memoize.spec.ts new file mode 100644 index 0000000..295b191 --- /dev/null +++ b/src/memo/__tests__/memoize.spec.ts @@ -0,0 +1,253 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { memoize } from "../index" + +function makeMemoized(useEqualityForMutableObjects = false) { + const callback = jest.fn() + + const fn = memoize((...args: any[]): string => { + callback(...args) + + return `Value: Args Length = ${args.length}` + }, useEqualityForMutableObjects) + + return { + callback, + fn, + } +} + +describe("memoize", () => { + it("should memoize primitives", () => { + const { callback, fn } = makeMemoized() + + // First run + fn(1, "two", true) + fn(1, "two", true) + + // Second run + fn("two", 1, true) + fn("two", 1, true) + + expect(callback).toHaveBeenCalledTimes(2) + }) + + it("should memoize objects", () => { + const { callback, fn } = makeMemoized() + + const o1: any = { one: 1 } + const o2: any = { two: 2 } + const o3: any = { three: 3 } + + // First run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o1.one = 11 + + // Second run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o2.twoMore = 22 + + // Third run + fn(o1, o2, o3) + fn(o1, o2, o3) + + delete o3.three + + // Fourth run + fn(o1, o2, o3) + fn(o1, o2, o3) + + expect(callback).toHaveBeenCalledTimes(4) + }) + + it("should memoize objects by equality rather than contents", () => { + const { callback, fn } = makeMemoized(true) + + const o1: any = { one: 1 } + const o2: any = { two: 2 } + const o3: any = { three: 3 } + + // First run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o1.one = 11 + + // Second run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o2.twoMore = 22 + + // Third run + fn(o1, o2, o3) + fn(o1, o2, o3) + + delete o3.three + + // Fourth run + fn(o1, o2, o3) + fn(o1, o2, o3) + + expect(callback).toHaveBeenCalledTimes(1) + }) + + it("should memoize arrays", () => { + const { callback, fn } = makeMemoized() + + const a1 = [1] + const a2 = [2] + const a3 = [3] + + // First run + fn(a1, a2, a3) + fn(a1, a2, a3) + + a1.push(11) + + // Second run + fn(a1, a2, a3) + fn(a1, a2, a3) + + a2.push(22) + + // Third run + fn(a1, a2, a3) + fn(a1, a2, a3) + + a3.splice(0, 1) + + // Fourth run + fn(a1, a2, a3) + fn(a1, a2, a3) + + expect(callback).toHaveBeenCalledTimes(4) + }) + + it("should memoize arrays by equality rather than contents", () => { + const { callback, fn } = makeMemoized(true) + + const a1 = [1] + const a2 = [2] + const a3 = [3] + + // First run + fn(a1, a2, a3) + fn(a1, a2, a3) + + a1.push(11) + + // Second run + fn(a1, a2, a3) + fn(a1, a2, a3) + + a2.push(22) + + // Third run + fn(a1, a2, a3) + fn(a1, a2, a3) + + a3.splice(0, 1) + + // Fourth run + fn(a1, a2, a3) + fn(a1, a2, a3) + + expect(callback).toHaveBeenCalledTimes(1) + }) + + it("should memoize zero arguments functions", () => { + const { callback, fn } = makeMemoized() + + fn() + fn() + + expect(callback).toHaveBeenCalledTimes(1) + }) + + it("should memoize variadic functions", () => { + const { callback, fn } = makeMemoized() + + const o1: any = { one: 1 } + const o2: any = { two: 2 } + const o3: any = { three: 3 } + + expect(fn(o1, o2, o3)).toEqual("Value: Args Length = 3") + expect(fn(o1, o2, o3)).toEqual("Value: Args Length = 3") + + expect(fn(o1, o2)).toEqual("Value: Args Length = 2") + expect(fn(o1, o2)).toEqual("Value: Args Length = 2") + + expect(fn(o1)).toEqual("Value: Args Length = 1") + expect(fn(o1)).toEqual("Value: Args Length = 1") + + expect(callback).toHaveBeenCalledTimes(3) + }) + + it("should memoize ES6 Maps", () => { + const { callback, fn } = makeMemoized() + + const o1 = new Map([["one", 1]]) + const o2 = new Map([["two", 2]]) + const o3 = new Map([["three", 3]]) + + // First run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o1.set("one", 11) + + // Second run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o2.set("twoMore", 22) + + // Third run + fn(o1, o2, o3) + fn(o1, o2, o3) + + o3.delete("three") + + // Fourth run + fn(o1, o2, o3) + fn(o1, o2, o3) + + expect(callback).toHaveBeenCalledTimes(4) + }) + + it("should memoize ES6 Sets", () => { + const { callback, fn } = makeMemoized() + + const s1 = new Set([1]) + const s2 = new Set([2]) + const s3 = new Set([3]) + + // First run + fn(s1, s2, s3) + fn(s1, s2, s3) + + s1.add(11) + + // Second run + fn(s1, s2, s3) + fn(s1, s2, s3) + + s2.add(22) + + // Third run + fn(s1, s2, s3) + fn(s1, s2, s3) + + s3.delete(3) + + // Fourth run + fn(s1, s2, s3) + fn(s1, s2, s3) + + expect(callback).toHaveBeenCalledTimes(4) + }) +}) diff --git a/src/memo/index.ts b/src/memo/index.ts new file mode 100644 index 0000000..0865c4f --- /dev/null +++ b/src/memo/index.ts @@ -0,0 +1,179 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { identity, isPlainObject, isUndefined, not } from "../core/index" + +interface TreeNode { + children: Map> | null + value: T | undefined +} + +function makeNode(): TreeNode { + const o = Object.create(null) + + o.children = new Map>() + o.value = undefined + + return o +} + +const mutableObjectCache = new Map() + +function stringifyIfNecessary( + o: T, + useEqualityForMutableObjects: boolean, +): T | string { + if ( + Array.isArray(o) || + isPlainObject(o) || + o instanceof Map || + o instanceof Set + ) { + if (useEqualityForMutableObjects) { + const stringKey = mutableObjectCache.get(o as any) + + if (stringKey) { + return stringKey + } + + const nextStringKey = mutableObjectCache.size.toString() + mutableObjectCache.set(o as any, nextStringKey) + + return nextStringKey + } + + if (o instanceof Map || o instanceof Set) { + return JSON.stringify(Array.from(o)) + } + + return JSON.stringify(o) + } + + return o +} + +class Cache { + root = makeNode() + useEqualityForMutableObjects = false + + constructor(useEqualityForMutableObjects: boolean) { + this.useEqualityForMutableObjects = useEqualityForMutableObjects + } + + has(args: any[]): boolean { + return not(isUndefined)(this.get(args)) + } + + get(args: any[]): T | undefined { + let previousNode = this.root + + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < args.length; i++) { + const arg = args[i] + const key = stringifyIfNecessary(arg, this.useEqualityForMutableObjects) + + // Found in tree, continue + if (previousNode.children && previousNode.children.has(key)) { + const node = previousNode.children.get(key) + + if (node) { + previousNode = node + } + } else { + return undefined + } + } + + return previousNode.value + } + + set(args: any[], value: T): void { + let previousNode = this.root + + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < args.length; i++) { + const arg = args[i] + const key = stringifyIfNecessary(arg, this.useEqualityForMutableObjects) + + let node + + if (previousNode.children) { + // Found in tree, continue + if (previousNode.children.has(key)) { + node = previousNode.children.get(key) + } else { + node = makeNode() + previousNode.children = previousNode.children.set(key, node) + } + + if (node) { + previousNode = node + } + } + } + + previousNode.value = value + } + + toJS(): object { + function serialize(data: any): any { + if (Array.isArray(data)) { + return data.map(serialize) + } + + if (isPlainObject(data)) { + return Object.keys(data).reduce( + (sum, k: string) => { + sum[k] = serialize(data[k]) + + return sum + }, + {} as { + [key: string]: any + }, + ) + } + + if (data && data.toJS) { + return data.toJS() + } + + return data + } + + return serialize(this.root) + } + + toString() { + return JSON.stringify(this.toJS(), undefined, 2) + } +} + +export function memoize any>( + fn: T, + useEqualityForMutableObjects = false, +): T { + const cache = new Cache(useEqualityForMutableObjects) + + function memoized(...args: any[]) { + if (cache.has(args)) { + return cache.get(args) + } + + const result = fn(...args) + + cache.set(args, result) + + return result + } + + return memoized as any +} + +export function maybeCallback(fn?: T | null) { + return fn || identity +} + +export function basicAlways(value: T): () => T { + return () => value +} + +export const always = memoize(basicAlways)