From a284d9a37d70897af28612281f940a81c477f428 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:42:55 -0700 Subject: [PATCH 01/22] feat: add internal hash utility for efficient deep-value sets --- src/_internal/hash.ts | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/_internal/hash.ts diff --git a/src/_internal/hash.ts b/src/_internal/hash.ts new file mode 100644 index 00000000..90be03ca --- /dev/null +++ b/src/_internal/hash.ts @@ -0,0 +1,65 @@ +export function hash(value: unknown): string { + const seen = new WeakMap() + let objectCount = 0 + + function innerHash(val: unknown): string { + if (val === null) return 'null' + if (val === undefined) return 'undefined' + + const type = typeof val + + if ( + type === 'boolean' || + type === 'number' || + type === 'bigint' || + type === 'symbol' + ) { + return `${type}:${String(val)}` + } + + if (type === 'string') { + return `string:${val}` + } + + if (type === 'function') { + return `function:${val.toString()}` + } + + if (type === 'object') { + if (seen.has(val as object)) { + return `circular#${seen.get(val as object)}` + } + + seen.set(val as object, objectCount++) + + if (Array.isArray(val)) { + const items = val.map((item) => innerHash(item)) + return `array:[${items.join(',')}]` + } else if (val instanceof Set) { + const items = Array.from(val.values()) + .map((item) => innerHash(item)) + .sort() + return `set:{${items.join(',')}}` + } else if (val instanceof Map) { + const entries = Array.from(val.entries()) + .map(([key, value]) => `${innerHash(key)}=>${innerHash(value)}`) + .sort() + return `map:{${entries.join(',')}}` + } else if (val instanceof Date) { + return `date:${val.toISOString()}` + } else if (val instanceof RegExp) { + return `regexp:${val.toString()}` + } else { + const keys = Object.keys(val as object).sort() + const entries = keys.map( + (key) => `${key}:${innerHash((val as any)[key])}` + ) + return `object:{${entries.join(',')}}` + } + } + + return 'unknown' + } + + return innerHash(value) +} From d7d1a54aeb78ca5e0ff890959dddbf4570ebf11a Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:43:15 -0700 Subject: [PATCH 02/22] feat: reify Boolean.not --- src/boolean/{not.spec.ts => not.test.ts} | 8 ++++++++ src/boolean/not.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+) rename src/boolean/{not.spec.ts => not.test.ts} (64%) diff --git a/src/boolean/not.spec.ts b/src/boolean/not.test.ts similarity index 64% rename from src/boolean/not.spec.ts rename to src/boolean/not.test.ts index 44b77273..6dc58302 100644 --- a/src/boolean/not.spec.ts +++ b/src/boolean/not.test.ts @@ -17,3 +17,11 @@ type Not_Spec = [ // @ts-expect-error Test.Expect<$> ] + +it('should return the opposite boolean', () => { + expect(Boolean.not(true)).toBe(false) +}) + +it('should return the opposite boolean', () => { + expect(Boolean.not(false)).toBe(true) +}) diff --git a/src/boolean/not.ts b/src/boolean/not.ts index 3d9ebecc..285b5b38 100644 --- a/src/boolean/not.ts +++ b/src/boolean/not.ts @@ -38,3 +38,18 @@ export type _$not = T extends true ? false : true export interface Not extends Kind.Kind { f(x: Type._$cast): _$not } + +/** + * Given a boolean, return the opposite boolean. + * + * @param {boolean} b - The boolean to negate. + * + * @example + * ```ts + * import { Boolean } from "hkt-toolbelt"; + * + * const result = Boolean.not(true) + * // ^? false + * ``` + */ +export const not = ((b: boolean) => !b) as Kind._$reify From fd245b78850bed2afa775e0c2dba77e59d431d83 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:43:33 -0700 Subject: [PATCH 03/22] fix: statefulness bug in collate utility --- src/combinator/collate.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/combinator/collate.ts b/src/combinator/collate.ts index d60e6b1a..008fdcea 100644 --- a/src/combinator/collate.ts +++ b/src/combinator/collate.ts @@ -80,13 +80,15 @@ export interface Collate extends Kind.Kind { * ``` */ export const collate = ((n: number) => { - const values: unknown[] = [] - - const taker = (value: unknown) => { - values.push(value) - - return values.length === n ? values : taker - } - - return taker + const collector = + (values: unknown[] = []) => + (value: unknown) => { + const newValues = [...values, value] + if (newValues.length === n) { + return newValues + } else { + return collector(newValues) + } + } + return collector() }) as Kind._$reify From f50fc9c6e421b7ce8959e37cc97f73d83146d2c7 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:43:50 -0700 Subject: [PATCH 04/22] feat: reify not-equals utility --- .../{not-equals.spec.ts => not-equals.test.ts} | 18 ++++++++++++++++++ src/conditional/not-equals.ts | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) rename src/conditional/{not-equals.spec.ts => not-equals.test.ts} (72%) diff --git a/src/conditional/not-equals.spec.ts b/src/conditional/not-equals.test.ts similarity index 72% rename from src/conditional/not-equals.spec.ts rename to src/conditional/not-equals.test.ts index 0b3a4dc1..a55e899e 100644 --- a/src/conditional/not-equals.spec.ts +++ b/src/conditional/not-equals.test.ts @@ -57,3 +57,21 @@ type NotEquals_Spec = [ Test.Expect<$<$, [[]]>>, Test.Expect<$<$, []>> ] + +it('should return true if two values are not equal', () => { + expect(Conditional.notEquals(true)(false)).toBe(true) +}) + +it('should return false if two values are equal', () => { + expect(Conditional.notEquals(true)(true)).toBe(false) +}) + +it('can handle deep equality', () => { + expect(Conditional.notEquals([1, [2, [3, [4]]]])([1, [2, [3, [5]]]])).toBe( + true + ) +}) + +it('can handle deep equality', () => { + expect(Conditional.notEquals({ a: 1 })({ a: 1 })).toBe(false) +}) diff --git a/src/conditional/not-equals.ts b/src/conditional/not-equals.ts index 08e7379e..ed4acefc 100644 --- a/src/conditional/not-equals.ts +++ b/src/conditional/not-equals.ts @@ -1,4 +1,5 @@ import { Kind } from '..' +import { deepEqual } from '../_internal/deepEqual' /** * `_$notEquals` is a type-level function that returns `true` if `T` and `U` are @@ -40,3 +41,20 @@ interface NotEquals_T extends Kind.Kind { export interface NotEquals extends Kind.Kind { f(x: this[Kind._]): NotEquals_T } + +/** + * Given two values, return whether they are not equal. + * + * @param {unknown} a - The first value. + * @param {unknown} b - The second value. + * + * @example + * ```ts + * import { Conditional } from "hkt-toolbelt"; + * + * const result = Conditional.notEquals('foo')('bar') + * // ^? true + * ``` + */ +export const notEquals = ((a: unknown) => (b: unknown) => + !deepEqual(a, b)) as Kind._$reify From 571fa20e746cd279ba6a6189930d248f4afb2a2a Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:44:04 -0700 Subject: [PATCH 05/22] feat: reify List.chunk --- src/list/{chunk.spec.ts => chunk.test.ts} | 12 +++++++++++ src/list/chunk.ts | 26 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) rename src/list/{chunk.spec.ts => chunk.test.ts} (65%) diff --git a/src/list/chunk.spec.ts b/src/list/chunk.test.ts similarity index 65% rename from src/list/chunk.spec.ts rename to src/list/chunk.test.ts index 723247d5..26ca98cc 100644 --- a/src/list/chunk.spec.ts +++ b/src/list/chunk.test.ts @@ -26,3 +26,15 @@ type Chunk_Spec = [ */ Test.Expect<$<$, [1, 2, 3, 4, 5]>, [[1, 2, 3, 4, 5]]> ] + +it('should chunk a list into sublists of a specified size', () => { + expect(List.chunk(2)([1, 2, 3, 4, 5])).toEqual([[1, 2], [3, 4], [5]]) +}) + +it('chunking an empty list results in an empty list', () => { + expect(List.chunk(2)([])).toEqual([]) +}) + +it('chunking by 0 results in the original list', () => { + expect(List.chunk(0)([1, 2, 3, 4, 5])).toEqual([[1, 2, 3, 4, 5]]) +}) diff --git a/src/list/chunk.ts b/src/list/chunk.ts index a0fdeef4..95bddc63 100644 --- a/src/list/chunk.ts +++ b/src/list/chunk.ts @@ -68,3 +68,29 @@ interface Chunk_T extends Kind.Kind { export interface Chunk extends Kind.Kind { f(x: Type._$cast): Chunk_T } + +/** + * Given a number N, return a list of lists, where each list contains N elements. + * + * @param {number} n - The number of elements to include in each sublist. + * @param {unknown[]} x - The list to chunk. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * const result = List.chunk(2)([1, 2, 3, 4, 5]) + * // ^? [[1, 2], [3, 4], [5]] + * ``` + */ +export const chunk = ((n: number) => (x: unknown[]) => { + const result: unknown[][] = [] + + if (n === 0) return [x] + + for (let i = 0; i < x.length; i += n) { + result.push(x.slice(i, i + n)) + } + + return result +}) as Kind._$reify From 07bf31d9f0d55daca1ba4fcd825f4b8f2d6ffe3e Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:45:17 -0700 Subject: [PATCH 06/22] feat: weaken List.filter constraints --- src/list/filter.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/list/filter.ts b/src/list/filter.ts index 0984bebd..c32e26d7 100644 --- a/src/list/filter.ts +++ b/src/list/filter.ts @@ -28,8 +28,7 @@ export type _$filter< : _$filter : O -interface Filter_T boolean>> - extends Kind.Kind { +interface Filter_T extends Kind.Kind { f(x: Type._$cast[]>): _$filter } @@ -78,9 +77,7 @@ interface Filter_T boolean>> * ], [42.42, null, "hello", undefined, "world"]> // "hello, world" */ export interface Filter extends Kind.Kind { - f( - x: Type._$cast boolean>> - ): Filter_T + f(x: Type._$cast): Filter_T } /** From 9de64d829b6988faf5646e5a25a8632a5824bca8 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:45:56 -0700 Subject: [PATCH 07/22] feat: additional tests for List.maxBy order preservation --- src/list/max-by.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/list/max-by.test.ts b/src/list/max-by.test.ts index 3b4f8c54..5de33104 100644 --- a/src/list/max-by.test.ts +++ b/src/list/max-by.test.ts @@ -9,9 +9,18 @@ type MaxBy_Spec = [ /** * Can find the maximum element of a list of strings, based on length. */ - Test.Expect<$<$, ['foo', 'bars', 'qux']>, 'bars'> + Test.Expect<$<$, ['foo', 'bars', 'qux']>, 'bars'>, + + /** + * Returns the first maximal element in the list. + */ + Test.Expect<$<$, ['foob', 'bar', 'quxo']>, 'foob'> ] it('should return the element in the list that has the highest score', () => { expect(List.maxBy(Function.identity)([1, 2, 3])).toBe(3) }) + +it('should return the first maximal element in the list', () => { + expect(List.maxBy(String.length)(['foob', 'bar', 'quxo'])).toBe('foob') +}) From 2e33f139062148b7520ddad9a8a7452c9e9083a7 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:46:24 -0700 Subject: [PATCH 08/22] feat: add List.of utility to create a 1-tuple list from an element --- src/list/of.test.ts | 22 +++++++++++++++++++++ src/list/of.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/list/of.test.ts create mode 100644 src/list/of.ts diff --git a/src/list/of.test.ts b/src/list/of.test.ts new file mode 100644 index 00000000..7a454796 --- /dev/null +++ b/src/list/of.test.ts @@ -0,0 +1,22 @@ +import { $, List, Test } from '..' + +type Of_Spec = [ + /** + * Can create a list containing a single value. + */ + Test.Expect<$, [42]>, + + /** + * Can create a list containing a single value. + */ + Test.Expect<$, [42]>, + + /** + * Can create a 1-tuple containg a list. + */ + Test.Expect<$, [[1, 2, 3]]> +] + +it('should create a list containing a single value', () => { + expect(List.of(42)).toEqual([42]) +}) diff --git a/src/list/of.ts b/src/list/of.ts new file mode 100644 index 00000000..806026bc --- /dev/null +++ b/src/list/of.ts @@ -0,0 +1,48 @@ +import { Kind, Type, List } from '..' + +/** + * `_$of` is a type-level function that takes in a value `X` and returns a list + * containing only that value. + * + * @template {unknown} X - The value to include in the list. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * type Result = List._$of<42>; // [42] + * ``` + */ +export type _$of = [X] + +/** + * `Of` is a type-level function that takes in a value `X` and returns a list + * containing only that value. + * + * @template {unknown} X - The value to include in the list. + * + * @example + * ```ts + * import { $, List } from "hkt-toolbelt"; + * + * type Result = $; // [42] + * ``` + */ +export interface Of extends Kind.Kind { + f(x: this[Kind._]): _$of +} + +/** + * Given a value, return a list containing only that value. + * + * @param {unknown} x - The value to include in the list. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * const result = List.of(42) + * // ^? [42] + * ``` + */ +export const of = ((x: unknown) => [x]) as Kind._$reify From 354883b14789347ee2c2ba782bbdf5ef2bedd15f Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:46:49 -0700 Subject: [PATCH 09/22] feat: add sliding-window utility for list manipulation --- src/list/sliding-window.test.ts | 69 +++++++++++++++++++++++++ src/list/sliding-window.ts | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/list/sliding-window.test.ts create mode 100644 src/list/sliding-window.ts diff --git a/src/list/sliding-window.test.ts b/src/list/sliding-window.test.ts new file mode 100644 index 00000000..372b165f --- /dev/null +++ b/src/list/sliding-window.test.ts @@ -0,0 +1,69 @@ +import { $, List, Test, Type } from '..' + +type SlidingWindow_Spec = [ + /** + * Can slide a window of length 2 over a list. + */ + Test.Expect< + $<$, [1, 2, 3, 4, 5]>, + [[1, 2], [2, 3], [3, 4], [4, 5]] + >, + + /** + * Can slide a window of length 3 over a list. + */ + Test.Expect< + $<$, [1, 2, 3, 4, 5]>, + [[1, 2, 3], [2, 3, 4], [3, 4, 5]] + >, + + /** + * Sliding a window of equal length results in the original list. + */ + Test.Expect<$<$, [1, 2, 3, 4, 5]>, [[1, 2, 3, 4, 5]]>, + + /** + * Sliding a window of length 0 results in an empty array. + */ + Test.Expect<$<$, [1, 2, 3, 4, 5]>, [[]]>, + + /** + * Sliding a window over the length of the list results in never. + */ + Test.Expect<$<$, [1, 2, 3, 4, 5]>, never>, + + /** + * Can slide a window of length 1 over a list. + */ + Test.Expect< + $<$, [1, 2, 3, 4, 5]>, + [[1], [2], [3], [4], [5]] + > +] + +it('should slide a window of length 2 over a list', () => { + expect(List.slidingWindow(2)([1, 2, 3, 4, 5])).toEqual([ + [1, 2], + [2, 3], + [3, 4], + [4, 5] + ]) +}) + +it('sliding a window of length 0 results in an empty array', () => { + expect(List.slidingWindow(0)([1, 2, 3, 4, 5])).toEqual([[]]) +}) + +it('sliding a window of length 1 results in a single-element array', () => { + expect(List.slidingWindow(1)([1, 2, 3, 4, 5])).toEqual([ + [1], + [2], + [3], + [4], + [5] + ]) +}) + +it('sliding a window over the length of the list results in never', () => { + expect(List.slidingWindow(6)([1, 2, 3, 4, 5])).toBe(Type.never) +}) diff --git a/src/list/sliding-window.ts b/src/list/sliding-window.ts new file mode 100644 index 00000000..5f738eb7 --- /dev/null +++ b/src/list/sliding-window.ts @@ -0,0 +1,90 @@ +import { $, Kind, Type, Number as Number_, List } from '..' + +/** + * Given a length and a list, return a list of lists, where each sublist has the + * specified length and is generated by sliding a window of the specified length + * over the input list. + * + * @template {Number.Number} N - The length of the sliding window. + * @template {unknown[]} T - The list to generate the sliding window from. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * type T0 = List._$slidingWindow<2, [1, 2, 3, 4, 5]> + * // ^? [[1, 2], [2, 3], [3, 4], [4, 5]] + * ``` + */ +export type _$slidingWindow< + N extends Number_.Number, + T extends unknown[], + O extends unknown[] = [] +> = 0 extends 1 + ? never + : T extends [unknown, ...infer Tail] + ? T['length'] extends N + ? [...O, List._$take] + : _$slidingWindow]> + : O[number] extends never + ? never + : O[number] extends [] + ? [[]] + : O + +interface SlidingWindow_T extends Kind.Kind { + f(x: Type._$cast): _$slidingWindow +} + +/** + * Given a length and a list, return a list of lists, where each sublist has the + * specified length and is generated by sliding a window of the specified length + * over the input list. + * + * @template {Number.Number} N - The length of the sliding window. + * @template {unknown[]} T - The list to generate the sliding window from. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * type T0 = $<$, [1, 2, 3, 4, 5]> + * // ^? [[1, 2], [2, 3], [3, 4], [4, 5]] + * ``` + */ +export interface SlidingWindow extends Kind.Kind { + f(x: Type._$cast): SlidingWindow_T +} + +/** + * Given a length and a list, return a list of lists, where each sublist has the + * specified length and is generated by sliding a window of the specified length + * over the input list. + * + * @param {Number.Number} N - The length of the sliding window. + * @param {unknown[]} T - The list to generate the sliding window from. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * const result = List.slidingWindow(2)([1, 2, 3, 4, 5]) + * // ^? [[1, 2], [2, 3], [3, 4], [4, 5]] + * ``` + */ +export const slidingWindow = ((n: Number_.Number) => (list: unknown[]) => { + const result: unknown[][] = [] + const targetLength = Number(n) + + if (targetLength === 0) { + return [[]] + } else if (targetLength > list.length) { + return Type.never + } + + for (let i = 0; i < list.length - targetLength + 1; i++) { + result.push(list.slice(i, i + targetLength)) + } + + return result +}) as Kind._$reify From 6e14cadd6b2b3f8ca8081d53a94cc9536a2421ca Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:47:34 -0700 Subject: [PATCH 10/22] feat: add new list utils to index --- src/list/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/list/index.ts b/src/list/index.ts index d8ff0a98..6bac1987 100644 --- a/src/list/index.ts +++ b/src/list/index.ts @@ -26,6 +26,7 @@ export * from './map' export * from './map-n' export * from './max-by' export * from './min-by' +export * from './of' export * from './pair' export * from './pop' export * from './pop-n' @@ -39,6 +40,7 @@ export * from './reverse' export * from './shift' export * from './shift-n' export * from './slice' +export * from './sliding-window' export * from './some' export * from './splice' export * from './take' From a9be37bab5700ac4f44c2c9c35caff580199f125 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:47:45 -0700 Subject: [PATCH 11/22] feat: reify List.pop utility --- src/list/pop.test.ts | 27 +++++++++++++++++++++++++++ src/list/pop.ts | 15 +++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/list/pop.test.ts diff --git a/src/list/pop.test.ts b/src/list/pop.test.ts new file mode 100644 index 00000000..2ec5c9f6 --- /dev/null +++ b/src/list/pop.test.ts @@ -0,0 +1,27 @@ +import { $, List, Test } from '..' + +type Pop_Spec = [ + /** + * Can pop the last element of a list. + */ + Test.Expect<$, ['a', 'b']>, + + /** + * Popping an empty list results in never. + */ + Test.Expect<$, never>, + + /** + * Popping a list with a single element results in an empty list. + */ + Test.Expect<$, []>, + + /** + * Popping a list with a single element results in an empty list. + */ + Test.Expect<$, [1, 2]> +] + +it('should remove the last element from the list', () => { + expect(List.pop(['a', 'b', 'c'])).toEqual(['a', 'b']) +}) diff --git a/src/list/pop.ts b/src/list/pop.ts index fc2f867b..7f3fc028 100644 --- a/src/list/pop.ts +++ b/src/list/pop.ts @@ -28,3 +28,18 @@ export type _$pop = T extends [ export interface Pop extends Kind.Kind { f(x: Type._$cast): _$pop } + +/** + * Given a list, remove the last element from the list. + * + * @param {unknown[]} x - The list to remove the last element from. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * const result = List.pop(['a', 'b', 'c']) + * // ^? ['a', 'b'] + * ``` + */ +export const pop = ((x: unknown[]) => x.slice(0, -1)) as Kind._$reify From 788968757373709986795d4aa9bc9733e38e9df7 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:48:20 -0700 Subject: [PATCH 12/22] feat: add domain constraints on reified List.take --- src/list/take.test.ts | 10 +++++++++- src/list/take.ts | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/list/take.test.ts b/src/list/take.test.ts index 75b7c0bf..5f5c27e7 100644 --- a/src/list/take.test.ts +++ b/src/list/take.test.ts @@ -1,4 +1,4 @@ -import { $, List, Test } from '..' +import { $, List, Test, Type } from '..' type Take_Spec = [ /** @@ -25,3 +25,11 @@ type Take_Spec = [ it('should take the first N elements of a list', () => { expect(List.take(2)([1, 2, 3])).toEqual([1, 2]) }) + +it('taking more elements than the list contains returns never', () => { + expect(List.take(4)([1, 2, 3])).toBe(Type.never) +}) + +it('can take entire list', () => { + expect(List.take(3)([1, 2, 3])).toEqual([1, 2, 3]) +}) diff --git a/src/list/take.ts b/src/list/take.ts index 62bcb2f2..5585f7c3 100644 --- a/src/list/take.ts +++ b/src/list/take.ts @@ -59,4 +59,4 @@ export interface Take extends Kind.Kind { * ``` */ export const take = ((n: number) => (values: unknown[]) => - values.slice(0, n)) as Kind._$reify + n <= values.length ? values.slice(0, n) : Type.never) as Kind._$reify From af87859a3dbc8bac93acc0c2b96d9dcc9395819f Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:48:45 -0700 Subject: [PATCH 13/22] feat: reify List.unique and add tests --- src/list/unique.test.ts | 37 +++++++++++++++++++++++++++ src/list/unique.ts | 56 +++++++++++++++++++++++++++++++++-------- 2 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 src/list/unique.test.ts diff --git a/src/list/unique.test.ts b/src/list/unique.test.ts new file mode 100644 index 00000000..b2cfb57f --- /dev/null +++ b/src/list/unique.test.ts @@ -0,0 +1,37 @@ +import { $, List, Test } from '..' + +type Unique_Spec = [ + /** + * Can find the unique elements in a list. + */ + Test.Expect<$, [1, 2, 3]>, + + /** + * Can handle empty lists. + */ + Test.Expect<$, []>, + + /** + * Can handle non-list inputs. + */ + Test.Expect<$, number[]>, + + /** + * Can handle non-unique elements. + */ + Test.Expect<$, [1, 2, 3]> +] + +it('should remove duplicate elements from a list', () => { + expect(List.unique([1, 2, 3, 2, 1])).toEqual([1, 2, 3]) +}) + +it('should return an empty list for an empty list', () => { + expect(List.unique([])).toEqual([]) +}) + +it('should be able to handle lists of objects', () => { + expect( + List.unique([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 2 }, { a: 1 }]) + ).toEqual([{ a: 1 }, { a: 2 }, { a: 3 }]) +}) diff --git a/src/list/unique.ts b/src/list/unique.ts index 3d0b6125..a8fe990c 100644 --- a/src/list/unique.ts +++ b/src/list/unique.ts @@ -1,9 +1,19 @@ -import { Kind, List, Type } from 'hkt-toolbelt' +import { Kind, List, Type } from '..' +import { hash } from '../_internal/hash' + +type _$unique2 = T extends [ + infer Head, + ...infer Tail +] + ? List._$includes extends true + ? _$unique2 + : _$unique2 + : Result /** * `_$unique` is a type-level function that returns a new list with all * duplicate elements removed. - * + *refd * @template {unknown[]} T The input list. * * @example @@ -14,14 +24,11 @@ import { Kind, List, Type } from 'hkt-toolbelt' * // ^? [1, 2, 3] * ``` */ -export type _$unique< - T extends unknown[], - Result extends unknown[] = [] -> = T extends [infer Head, ...infer Tail] - ? List._$includes extends true - ? _$unique - : _$unique - : Result +export type _$unique = [List._$isVariadic] extends [ + true +] + ? T + : _$unique2 /** * Returns a new list with all duplicate elements removed. @@ -39,3 +46,32 @@ export type _$unique< export interface Unique extends Kind.Kind { f(x: Type._$cast): _$unique } + +/** + * Given a list, return a new list with all duplicate elements removed. + * + * @param {unknown[]} x - The list to remove duplicates from. + * + * @example + * ```ts + * import { List } from "hkt-toolbelt"; + * + * const result = List.unique([1, 2, 3, 2, 1]) + * // ^? [1, 2, 3] + * ``` + */ +export const unique = ((x: unknown[]) => { + const seen = new Set() + const unique: unknown[] = [] + + for (const element of x) { + const h = hash(element) + + if (!seen.has(h)) { + seen.add(h) + unique.push(element) + } + } + + return unique +}) as Kind._$reify From b1070664a2079ae061996f4027a65765e17629ae Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:49:07 -0700 Subject: [PATCH 14/22] feat: add 'digits' method to get digits of natural numbers --- src/natural-number/digits.test.ts | 37 +++++++++++++ src/natural-number/digits.ts | 87 +++++++++++++++++++++++++++++++ src/natural-number/index.ts | 1 + 3 files changed, 125 insertions(+) create mode 100644 src/natural-number/digits.test.ts create mode 100644 src/natural-number/digits.ts diff --git a/src/natural-number/digits.test.ts b/src/natural-number/digits.test.ts new file mode 100644 index 00000000..b5a248bb --- /dev/null +++ b/src/natural-number/digits.test.ts @@ -0,0 +1,37 @@ +import { $, Test, NaturalNumber } from '..' + +type Digits_Spec = [ + /** + * Can convert a natural number to a list of digits. + */ + Test.Expect<$, [4, 2]>, + + /** + * Zero is converted to a list of one digit. + */ + Test.Expect<$, [0]>, + + /** + * Can convert string-encoded natural numbers to a list of digits. + */ + Test.Expect<$, [4, 2]>, + + /** + * Can convert bigint literals to a list of digits. + */ + Test.Expect<$, [4, 2]>, + + /** + * Converting the 'number' type results in 'never'. + */ + Test.Expect<$, never>, + + /** + * Converting the 'string' type results in 'never'. + */ + Test.Expect<$, never> +] + +it('should convert a natural number to a list of digits', () => { + expect(NaturalNumber.digits(42)).toEqual([4, 2]) +}) diff --git a/src/natural-number/digits.ts b/src/natural-number/digits.ts new file mode 100644 index 00000000..701303b3 --- /dev/null +++ b/src/natural-number/digits.ts @@ -0,0 +1,87 @@ +import { Kind, Number as Number_, Type } from '..' +import { _$string } from '../parser' + +type _$stringToDigitMap = { + '0': 0 + '1': 1 + '2': 2 + '3': 3 + '4': 4 + '5': 5 + '6': 6 + '7': 7 + '8': 8 + '9': 9 +} + +/** + * Given a natural number, return the list of digits. + * + * @param {number} x - The natural number to convert to a list of digits. + * + * Similar to `NaturalNumber.ToList`, but returns the list of digits as numbers + * instead of strings. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * type Result = NaturalNumber._$digits<42>; // [4, 2] + * // ^? [4, 2] + */ +export type _$digits< + S extends Number_.Number, + O extends number[] = [] +> = 0 extends 1 + ? never + : Number_._$toString extends `${infer Head}${infer Tail}` + ? _$digits< + Tail, + Head extends keyof _$stringToDigitMap + ? [...O, _$stringToDigitMap[Head]] + : never + > + : O extends [] + ? never + : O + +/** + * Given a natural number, return the list of digits. + * + * @param {number} x - The natural number to convert to a list of digits. + * + * Similar to `NaturalNumber.ToList`, but returns the list of digits as numbers + * instead of strings. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * type Result = $; // [4, 2] + * // ^? [4, 2] + * ``` + */ +export interface Digits extends Kind.Kind { + f(x: Type._$cast): _$digits +} + +/** + * Given a natural number, return the list of digits. + * + * @param {number} x - The natural number to convert to a list of digits. + * + * Similar to `NaturalNumber.ToList`, but returns the list of digits as numbers + * instead of strings. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const result = NaturalNumber.digits(42) + * // ^? [4, 2] + * ``` + */ +export const digits = ((x: Number_.Number) => + Number.isInteger(x) && Number(x) >= 0 + ? `${x}`.split('').map((d) => Number(d)) + : Type.never) as Kind._$reify diff --git a/src/natural-number/index.ts b/src/natural-number/index.ts index 33e5cac0..8eb7c17a 100644 --- a/src/natural-number/index.ts +++ b/src/natural-number/index.ts @@ -1,6 +1,7 @@ export * from './add' export * from './compare' export * from './decrement' +export * from './digits' export * from './divide-by' export * from './divide' export * from './increment' From 16698afc4e7c195e13294d0c15a6685dfd1c2f9e Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:49:27 -0700 Subject: [PATCH 15/22] feat: reify natural number comparison methods --- ...pec.ts => is-greater-than-or-equal.test.ts} | 12 ++++++++++++ src/natural-number/is-greater-than-or-equal.ts | 18 ++++++++++++++++++ ...l.spec.ts => is-less-than-or-equal.test.ts} | 12 ++++++++++++ src/natural-number/is-less-than-or-equal.ts | 18 ++++++++++++++++++ ...-less-than.spec.ts => is-less-than.test.ts} | 8 ++++++++ src/natural-number/is-less-than.ts | 17 +++++++++++++++++ 6 files changed, 85 insertions(+) rename src/natural-number/{is-greater-than-or-equal.spec.ts => is-greater-than-or-equal.test.ts} (56%) rename src/natural-number/{is-less-than-or-equal.spec.ts => is-less-than-or-equal.test.ts} (56%) rename src/natural-number/{is-less-than.spec.ts => is-less-than.test.ts} (65%) diff --git a/src/natural-number/is-greater-than-or-equal.spec.ts b/src/natural-number/is-greater-than-or-equal.test.ts similarity index 56% rename from src/natural-number/is-greater-than-or-equal.spec.ts rename to src/natural-number/is-greater-than-or-equal.test.ts index abedfe93..9329d95d 100644 --- a/src/natural-number/is-greater-than-or-equal.spec.ts +++ b/src/natural-number/is-greater-than-or-equal.test.ts @@ -22,3 +22,15 @@ type IsGreaterThanOrEqual_Spec = [ // @ts-expect-error Test.Expect<$<$, 2>> ] + +it('should return whether the second number is greater than or equal to the first', () => { + expect(NaturalNumber.isGreaterThanOrEqual(3)(2)).toBe(false) +}) + +it('should return whether the second number is greater than or equal to the first', () => { + expect(NaturalNumber.isGreaterThanOrEqual(3)(3)).toBe(true) +}) + +it('should return whether the second number is greater than or equal to the first', () => { + expect(NaturalNumber.isGreaterThanOrEqual(3)(2)).toBe(false) +}) diff --git a/src/natural-number/is-greater-than-or-equal.ts b/src/natural-number/is-greater-than-or-equal.ts index 522dc55d..1f987e18 100644 --- a/src/natural-number/is-greater-than-or-equal.ts +++ b/src/natural-number/is-greater-than-or-equal.ts @@ -86,3 +86,21 @@ export interface IsGreaterThanOrEqual extends Kind.Kind { x: Type._$cast ): IsGreaterThanOrEqual_T } + +/** + * Given two numbers, return whether the second number is greater than or equal + * to the first. + * + * @param {number} a - The first number. + * @param {number} b - The second number. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const result = NaturalNumber.isGreaterThanOrEqual(3)(2) + * // ^? false + * ``` + */ +export const isGreaterThanOrEqual = ((a: number) => (b: number) => + b >= a) as Kind._$reify diff --git a/src/natural-number/is-less-than-or-equal.spec.ts b/src/natural-number/is-less-than-or-equal.test.ts similarity index 56% rename from src/natural-number/is-less-than-or-equal.spec.ts rename to src/natural-number/is-less-than-or-equal.test.ts index 42d6b91a..e0426110 100644 --- a/src/natural-number/is-less-than-or-equal.spec.ts +++ b/src/natural-number/is-less-than-or-equal.test.ts @@ -22,3 +22,15 @@ type IsLessThanOrEqual_Spec = [ // @ts-expect-error Test.Expect<$<$, 2>> ] + +it('should return whether the second number is less than or equal to the first', () => { + expect(NaturalNumber.isLessThanOrEqual(3)(2)).toBe(true) +}) + +it('should return whether the second number is less than or equal to the first', () => { + expect(NaturalNumber.isLessThanOrEqual(3)(3)).toBe(true) +}) + +it('should return whether the second number is less than or equal to the first', () => { + expect(NaturalNumber.isLessThanOrEqual(3)(4)).toBe(false) +}) diff --git a/src/natural-number/is-less-than-or-equal.ts b/src/natural-number/is-less-than-or-equal.ts index 144cce1c..c70868c3 100644 --- a/src/natural-number/is-less-than-or-equal.ts +++ b/src/natural-number/is-less-than-or-equal.ts @@ -84,3 +84,21 @@ interface IsLessThanOrEqual_T extends Kind.Kind { export interface IsLessThanOrEqual extends Kind.Kind { f(x: Type._$cast): IsLessThanOrEqual_T } + +/** + * Given two numbers, return whether the second number is less than or equal + * to the first. + * + * @param {number} a - The first number. + * @param {number} b - The second number. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const result = NaturalNumber.isLessThanOrEqual(3)(2) + * // ^? true + * ``` + */ +export const isLessThanOrEqual = ((a: number) => (b: number) => + b <= a) as Kind._$reify diff --git a/src/natural-number/is-less-than.spec.ts b/src/natural-number/is-less-than.test.ts similarity index 65% rename from src/natural-number/is-less-than.spec.ts rename to src/natural-number/is-less-than.test.ts index ad84086a..de950fc1 100644 --- a/src/natural-number/is-less-than.spec.ts +++ b/src/natural-number/is-less-than.test.ts @@ -22,3 +22,11 @@ type IsLessThan_Spec = [ // @ts-expect-error Test.Expect<$<$, 2>> ] + +it('should return whether the second number is less than the first', () => { + expect(NaturalNumber.isLessThan(3)(2)).toBe(true) +}) + +it('should return whether the second number is less than the first', () => { + expect(NaturalNumber.isLessThan(3)(3)).toBe(false) +}) diff --git a/src/natural-number/is-less-than.ts b/src/natural-number/is-less-than.ts index bee71fba..68748373 100644 --- a/src/natural-number/is-less-than.ts +++ b/src/natural-number/is-less-than.ts @@ -82,3 +82,20 @@ interface IsLessThan_T extends Kind.Kind { export interface IsLessThan extends Kind.Kind { f(x: Type._$cast): IsLessThan_T } + +/** + * Given two numbers, return whether the second number is less than the first. + * + * @param {number} a - The first number. + * @param {number} b - The second number. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const result = NaturalNumber.isLessThan(3)(2) + * // ^? true + * ``` + */ +export const isLessThan = ((a: number) => (b: number) => + b < a) as Kind._$reify From 5ac3a53056bdbddfe75ddc50ecbbd194bb1bd2c5 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:49:44 -0700 Subject: [PATCH 16/22] feat: reify natural number multiplication --- .../{multiply.spec.ts => multiply.test.ts} | 23 ++++++- src/natural-number/multiply.ts | 60 +++++++++++++------ 2 files changed, 61 insertions(+), 22 deletions(-) rename src/natural-number/{multiply.spec.ts => multiply.test.ts} (58%) diff --git a/src/natural-number/multiply.spec.ts b/src/natural-number/multiply.test.ts similarity index 58% rename from src/natural-number/multiply.spec.ts rename to src/natural-number/multiply.test.ts index a9f2b031..7d88591b 100644 --- a/src/natural-number/multiply.spec.ts +++ b/src/natural-number/multiply.test.ts @@ -1,4 +1,4 @@ -import { $, Test, NaturalNumber } from '..' +import { $, Test, NaturalNumber, Type } from '..' type Multiply_Spec = [ /** @@ -47,7 +47,24 @@ type Multiply_Spec = [ Test.Expect<$<$, '5678'>, 7006652>, /** - * Non-natural number input emits error + * Non-natural number input emits never */ - Test.Expect<$, never> + Test.Expect<$<$, 42>, never> ] + +it('should multiply two natural numbers', () => { + expect(NaturalNumber.multiply(2)(2)).toBe(4) +}) + +it('can multiply by zero', () => { + expect(NaturalNumber.multiply(0)(0)).toBe(0) +}) + +it('can multiply by a string', () => { + const result = NaturalNumber.multiply('1234')('5678') + expect(result).toBe(7006652) +}) + +it('multiplying non-natural numbers results in never', () => { + expect(NaturalNumber.multiply(-42.42)(42)).toBe(Type.never) +}) diff --git a/src/natural-number/multiply.ts b/src/natural-number/multiply.ts index 9d6f76e0..2461b657 100644 --- a/src/natural-number/multiply.ts +++ b/src/natural-number/multiply.ts @@ -1,4 +1,16 @@ -import { Type, Number, Kind, DigitList, NaturalNumber } from '..' +import { Type, Number as Number_, Kind, DigitList, NaturalNumber } from '..' + +type _$multiply2< + A extends Number_.Number, + B extends Number_.Number, + A_LIST extends DigitList.DigitList = NaturalNumber._$toList, + B_LIST extends DigitList.DigitList = NaturalNumber._$toList, + PRODUCT_LIST extends DigitList.DigitList = DigitList._$multiply< + A_LIST, + B_LIST + >, + PRODUCT = DigitList._$toNumber +> = PRODUCT /** * `_$multiply` is a type-level function that multiplies a natural number by another natural number. @@ -26,22 +38,15 @@ import { Type, Number, Kind, DigitList, NaturalNumber } from '..' * type IsZero = NaturalNumber._$multiply<42, 0>; // 0 * ``` */ -export type _$multiply< - A extends Number.Number, - B extends Number.Number, - A_LIST extends DigitList.DigitList = NaturalNumber._$toList, - B_LIST extends DigitList.DigitList = NaturalNumber._$toList, - PRODUCT_LIST extends DigitList.DigitList = DigitList._$multiply< - A_LIST, - B_LIST - >, - PRODUCT = DigitList._$toNumber -> = PRODUCT +export type _$multiply = + Number_._$isNatural extends true + ? Number_._$isNatural extends true + ? _$multiply2 + : never + : never -interface Multiply_T extends Kind.Kind { - f( - x: Type._$cast - ): Number._$isNatural extends true ? _$multiply : never +interface Multiply_T extends Kind.Kind { + f(x: Type._$cast): _$multiply } /** @@ -82,7 +87,24 @@ interface Multiply_T extends Kind.Kind { * ``` */ export interface Multiply extends Kind.Kind { - f( - x: Type._$cast - ): Number._$isNatural extends true ? Multiply_T : never + f(x: Type._$cast): Multiply_T } + +/** + * Given two natural numbers, return their product. + * + * @param {Number.Number} a - The first natural number. + * @param {Number.Number} b - The second natural number. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const result = NaturalNumber.multiply(2)(3) + * // ^? 6 + * ``` + */ +export const multiply = ((a: Number_.Number) => (b: Number_.Number) => + Number_.isNatural(a as never) && Number_.isNatural(b as never) + ? Number(a) * Number(b) + : Type.never) as Kind._$reify From d41eddda3f4924fb92f5ae083ab8e57303615e33 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:50:07 -0700 Subject: [PATCH 17/22] feat: reify natural number type-level check --- ...{is-natural.spec.ts => is-natural.test.ts} | 20 ++++++++++++++ src/number/is-natural.ts | 26 +++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) rename src/number/{is-natural.spec.ts => is-natural.test.ts} (66%) diff --git a/src/number/is-natural.spec.ts b/src/number/is-natural.test.ts similarity index 66% rename from src/number/is-natural.spec.ts rename to src/number/is-natural.test.ts index 6fbde4a4..bae22f5f 100644 --- a/src/number/is-natural.spec.ts +++ b/src/number/is-natural.test.ts @@ -46,3 +46,23 @@ type IsNaturalNumber_Spec = [ */ Test.Expect<$, false> ] + +it('should check if a number is a natural number', () => { + expect(Number.isNatural(42)).toBe(true) +}) + +it('values which are not natural numbers should not be natural numbers', () => { + expect(Number.isNatural(42.42)).toBe(false) +}) + +it('can recognize negative numbers', () => { + expect(Number.isNatural(-42)).toBe(false) +}) + +it('zero is a natural number', () => { + expect(Number.isNatural(0)).toBe(true) +}) + +it('can evaluate string numbers', () => { + expect(Number.isNatural('42')).toBe(true) +}) diff --git a/src/number/is-natural.ts b/src/number/is-natural.ts index 5154eaad..9f8b8514 100644 --- a/src/number/is-natural.ts +++ b/src/number/is-natural.ts @@ -1,4 +1,4 @@ -import { Number, Type, Kind } from '..' +import { Number as Number_, Type, Kind } from '..' /** * `Number.IsNatural` is a type-level function that checks if a number is a natural number. @@ -13,9 +13,9 @@ import { Number, Type, Kind } from '..' * type T2 = Number._$isNatural<-1> // false * type T3 = Number._$isNatural<1.5> // false */ -export type _$isNatural = - Number._$isInteger extends true - ? Number._$sign extends '+' +export type _$isNatural = + Number_._$isInteger extends true + ? Number_._$sign extends '+' ? true : false : false @@ -34,5 +34,21 @@ export type _$isNatural = * type T3 = $ // false */ export interface IsNatural extends Kind.Kind { - f(x: Type._$cast): _$isNatural + f(x: Type._$cast): _$isNatural } + +/** + * Given a number, return whether or not it is a natural number. + * + * @param {Number_.Number} x - The number to check. + * + * @example + * ```ts + * import { Number } from "hkt-toolbelt"; + * + * const result = Number.isNatural(42) + * // ^? true + * ``` + */ +export const isNatural = ((x: Number_.Number) => + Number.isInteger(Number(x)) && Number(x) >= 0) as Kind._$reify From 9663109652ea50f002b2eab9aa0e5cf38068dd9c Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:50:22 -0700 Subject: [PATCH 18/22] feat: reify String.length --- src/string/{length.spec.ts => length.test.ts} | 4 ++++ src/string/length.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+) rename src/string/{length.spec.ts => length.test.ts} (83%) diff --git a/src/string/length.spec.ts b/src/string/length.test.ts similarity index 83% rename from src/string/length.spec.ts rename to src/string/length.test.ts index f9f7c396..904cd98f 100644 --- a/src/string/length.spec.ts +++ b/src/string/length.test.ts @@ -21,3 +21,7 @@ type Length_Spec = [ */ Test.Expect<$, number> ] + +it('should return the length of a string', () => { + expect(String.length('foo')).toBe(3) +}) diff --git a/src/string/length.ts b/src/string/length.ts index 3364165f..3b64ec22 100644 --- a/src/string/length.ts +++ b/src/string/length.ts @@ -28,3 +28,18 @@ export type _$length = export interface Length extends Kind.Kind { f(x: Type._$cast): _$length } + +/** + * Given a string, return the length of the string. + * + * @param {string} x - The string to get the length of. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.length('hello') + * // ^? 5 + * ``` + */ +export const length = ((x: string) => x.length) as Kind._$reify From 14e723c10c3e1ff121fce30a5cd02d130027b08b Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:50:31 -0700 Subject: [PATCH 19/22] feat: reify String.reverse --- src/string/{reverse.spec.ts => reverse.test.ts} | 8 ++++++++ src/string/reverse.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) rename src/string/{reverse.spec.ts => reverse.test.ts} (85%) diff --git a/src/string/reverse.spec.ts b/src/string/reverse.test.ts similarity index 85% rename from src/string/reverse.spec.ts rename to src/string/reverse.test.ts index 5d6ca7c5..87281758 100644 --- a/src/string/reverse.spec.ts +++ b/src/string/reverse.test.ts @@ -45,3 +45,11 @@ type Reverse_Spec = [ */ Test.Expect<$, string> ] + +it('should reverse a string', () => { + expect(String.reverse('foo')).toBe('oof') +}) + +it('should reverse an empty string', () => { + expect(String.reverse('')).toBe('') +}) diff --git a/src/string/reverse.ts b/src/string/reverse.ts index ae92bfd6..171a8719 100644 --- a/src/string/reverse.ts +++ b/src/string/reverse.ts @@ -27,3 +27,19 @@ export type _$reverse< export interface Reverse extends Kind.Kind { f(x: Type._$cast): _$reverse } + +/** + * Given a string, return the string with the characters in reverse order. + * + * @param {string} x - The string to reverse. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.reverse('foo') + * // ^? 'oof' + * ``` + */ +export const reverse = ((x: string) => + [...x].reverse().join('')) as Kind._$reify From 5048dcd04b9d9de6d42caa89cbad052ca1283f0e Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:55:33 -0700 Subject: [PATCH 20/22] fix: remove unused imports --- src/list/of.ts | 2 +- src/list/sliding-window.ts | 2 +- src/natural-number/digits.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/list/of.ts b/src/list/of.ts index 806026bc..7531622a 100644 --- a/src/list/of.ts +++ b/src/list/of.ts @@ -1,4 +1,4 @@ -import { Kind, Type, List } from '..' +import { Kind } from '..' /** * `_$of` is a type-level function that takes in a value `X` and returns a list diff --git a/src/list/sliding-window.ts b/src/list/sliding-window.ts index 5f738eb7..caebbd20 100644 --- a/src/list/sliding-window.ts +++ b/src/list/sliding-window.ts @@ -1,4 +1,4 @@ -import { $, Kind, Type, Number as Number_, List } from '..' +import { Kind, Type, Number as Number_, List } from '..' /** * Given a length and a list, return a list of lists, where each sublist has the diff --git a/src/natural-number/digits.ts b/src/natural-number/digits.ts index 701303b3..c0d21cfd 100644 --- a/src/natural-number/digits.ts +++ b/src/natural-number/digits.ts @@ -1,5 +1,4 @@ import { Kind, Number as Number_, Type } from '..' -import { _$string } from '../parser' type _$stringToDigitMap = { '0': 0 From b4650a0587cb0ca52dba7b044defc84b642ca076 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 16:55:45 -0700 Subject: [PATCH 21/22] fix: linting errors in internal hash utility --- src/_internal/hash.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/_internal/hash.ts b/src/_internal/hash.ts index 90be03ca..ee1b36b9 100644 --- a/src/_internal/hash.ts +++ b/src/_internal/hash.ts @@ -6,31 +6,29 @@ export function hash(value: unknown): string { if (val === null) return 'null' if (val === undefined) return 'undefined' - const type = typeof val - if ( - type === 'boolean' || - type === 'number' || - type === 'bigint' || - type === 'symbol' + typeof val === 'boolean' || + typeof val === 'number' || + typeof val === 'bigint' || + typeof val === 'symbol' ) { - return `${type}:${String(val)}` + return `${typeof val}:${String(val)}` } - if (type === 'string') { + if (typeof val === 'string') { return `string:${val}` } - if (type === 'function') { + if (typeof val === 'function') { return `function:${val.toString()}` } - if (type === 'object') { - if (seen.has(val as object)) { - return `circular#${seen.get(val as object)}` + if (typeof val === 'object') { + if (seen.has(val)) { + return `circular#${seen.get(val)}` } - seen.set(val as object, objectCount++) + seen.set(val, objectCount++) if (Array.isArray(val)) { const items = val.map((item) => innerHash(item)) @@ -50,9 +48,9 @@ export function hash(value: unknown): string { } else if (val instanceof RegExp) { return `regexp:${val.toString()}` } else { - const keys = Object.keys(val as object).sort() + const keys = Object.keys(val).sort() const entries = keys.map( - (key) => `${key}:${innerHash((val as any)[key])}` + (key) => `${key}:${innerHash(val[key as keyof typeof val])}` ) return `object:{${entries.join(',')}}` } From 338b677f7ba5b61880a41d91a127a63fb75e9d71 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 5 Oct 2024 17:01:29 -0700 Subject: [PATCH 22/22] feat: add changelog for 0.24.7 --- changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index 74518b9c..fd4c6ef7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## [0.24.7] + +- Add `NaturalNumber.Digits` to get the digits of a natural number. +- Add `List.Of` to create a list containing a single value. +- Add `List.SlidingWindow` to slide a window of a certain length over a list. +- Reify various natural number utilities. +- Reify various string and list utilities. +- Fix statefulness bug in reified `List.chunk`. + ## [0.24.6] - Add `String.FromCharCode` to convert a character code to a string. - Add `String.IsLetter` to check if a string is a letter.