From 0f4137cd4eb95ce9882a36958a8ba454855a8827 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 14:34:04 -0800 Subject: [PATCH 01/27] feat: implement Object deep-entries utility --- src/object/deep-entries.test.ts | 47 +++++++++++++++ src/object/deep-entries.ts | 100 ++++++++++++++++++++++++++++++++ src/object/index.ts | 3 + 3 files changed, 150 insertions(+) create mode 100644 src/object/deep-entries.test.ts create mode 100644 src/object/deep-entries.ts diff --git a/src/object/deep-entries.test.ts b/src/object/deep-entries.test.ts new file mode 100644 index 00000000..cdeb9f39 --- /dev/null +++ b/src/object/deep-entries.test.ts @@ -0,0 +1,47 @@ +import { $, Test, Object } from '..' + +type DeepEntries_Spec = [ + /** + * Simple object + */ + Test.Expect< + $, + [['foo', 'bar'], ['baz', 42]] + >, + + /** + * Nested object + */ + Test.Expect< + $, + [['name', 'first', 'john'], ['name', 'last', 'smith'], ['age', 25]] + >, + + /** + * Deeper nesting + */ + Test.Expect< + $, + [['a', 'b', 'c', 'd']] + >, + + /** + * Mixed nesting (arrays are treated as values) + */ + Test.Expect< + $, + [['x', ['y', 'z']], ['w', 'v', 1]] + > +] + +it('should return the entries of an object', () => { + expect(Object.deepEntries({ a: 1, b: 2, c: 3 })).toEqual([ + ['a', 1], + ['b', 2], + ['c', 3] + ]) +}) + +it('should return the entries of an object', () => { + expect(Object.deepEntries({ a: { b: 1 } })).toEqual([['a', 'b', 1]]) +}) diff --git a/src/object/deep-entries.ts b/src/object/deep-entries.ts new file mode 100644 index 00000000..9edc199c --- /dev/null +++ b/src/object/deep-entries.ts @@ -0,0 +1,100 @@ +import { Type, Kind, Object as Object_ } from '..' + +type _PrependKeyToEntries = { + [I in keyof E]: E[I] extends unknown[] ? [K, ...E[I]] : never +} + +type _DeepEntriesKey< + O extends Record, + K extends PropertyKey +> = + O[K] extends Record + ? _PrependKeyToEntries> + : [[K, O[K]]] + +/** + * Given an object, return its deep entries as a list of tuples, each of which + * represent a path to a value in the object. + * + * Arrays are considered to be values, and are not traversed. + * + * @template O - The object to get the deep entries of. + * + * @example + * ```ts + * import { Object } from "hkt-toolbelt"; + * + * type T0 = Object._$deepEntries<{ name: { first: 'ada', last: 'lovelace' } }> + * // ^? [["name", "first", "ada"], ["name", "last", "lovelace"]] + * ``` + */ +export type _$deepEntries< + O extends Record, + Keys extends unknown[] = Object_._$keys, + Acc extends unknown[][] = [], + DEEP_KEYS = _DeepEntriesKey> +> = Keys extends [infer K, ...infer Rest] + ? K extends keyof O + ? _$deepEntries]> + : never + : Acc + +/** + * Given an object, return its deep entries as a list of tuples, each of which + * represent a path to a value in the object. + * + * Arrays are considered to be values, and are not traversed. + * + * @example + * ```ts + * import { $, Object } from "hkt-toolbelt"; + * + * type T0 = $ + * // ^? [["name", "first", "ada"], ["name", "last", "lovelace"]] + * ``` + */ +export interface DeepEntries extends Kind.Kind { + f( + x: Type._$cast> + ): _$deepEntries +} + +/** + * Given an object, return its deep entries as a list of tuples, each of which + * represent a path to a value in the object. + * + * Arrays are considered to be values, and are not traversed. + * + * @param {Record} x - The object to get the deep entries of. + * + * @example + * ```ts + * import { Object } from "hkt-toolbelt"; + * + * const T0 = Object.deepEntries({ name: { first: 'ada', last: 'lovelace' } }) + * // ^? [["name", "first", "ada"], ["name", "last", "lovelace"]] + * ``` + */ +export const deepEntries = (( + obj: Record +): unknown[][] => { + const results: unknown[][] = [] + + for (const key in obj) { + const value = obj[key] + + // If value is an object (non-null) and not an array, recurse + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + const nested = (deepEntries as Function)(value) + // Prepend the current key to each nested path + for (const entry of nested) { + results.push([key, ...entry]) + } + } else { + // Arrays and other primitives are considered final values + results.push([key, value]) + } + } + + return results +}) as unknown as Kind._$reify diff --git a/src/object/index.ts b/src/object/index.ts index 6535004a..2580931b 100644 --- a/src/object/index.ts +++ b/src/object/index.ts @@ -2,6 +2,7 @@ export * from './assign' export * from './at-key' export * from './at-path-in-object' export * from './at' +export * from './deep-entries' export * from './at-path' export * from './at-path-n' export * from './deep-input-of' @@ -9,6 +10,8 @@ export * from './deep-map-values' export * from './defaults' export * from './emplace' export * from './entries' +export * from './from-entries' +export * from './is-object' export * from './keys' export * from './map-keys' export * from './map-values' From a127200527e592b80acce2433abcd18631c3ec72 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 14:34:51 -0800 Subject: [PATCH 02/27] feat: implement from-entries object util --- src/object/from-entries.test.ts | 38 +++++++++++++++++++++ src/object/from-entries.ts | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/object/from-entries.test.ts create mode 100644 src/object/from-entries.ts diff --git a/src/object/from-entries.test.ts b/src/object/from-entries.test.ts new file mode 100644 index 00000000..81d87ec8 --- /dev/null +++ b/src/object/from-entries.test.ts @@ -0,0 +1,38 @@ +import { $, Test, Object } from '..' + +type FromEntries_Spec = [ + /** + * Can convert a list of key-value pairs to a record. + */ + Test.Expect< + $, + { foo: 'bar'; baz: 42 } + >, + + /** + * Can convert a list of key-value pairs to a record. + */ + Test.Expect< + $, + { foo: 'bar'; baz: 42; qux: 'corge' } + > +] + +it('should convert a list of key-value pairs to a record', () => { + expect( + Object.fromEntries([ + ['foo', 'bar'], + ['baz', 42] + ]) + ).toEqual({ foo: 'bar', baz: 42 }) +}) + +it('should convert a list of key-value pairs to a record', () => { + expect( + Object.fromEntries([ + ['foo', 'bar'], + ['baz', 42], + ['qux', 'corge'] + ]) + ).toEqual({ foo: 'bar', baz: 42, qux: 'corge' }) +}) diff --git a/src/object/from-entries.ts b/src/object/from-entries.ts new file mode 100644 index 00000000..1a82f54e --- /dev/null +++ b/src/object/from-entries.ts @@ -0,0 +1,58 @@ +import { Union, Type, Kind } from '..' + +/** + * Given a list of key-value pairs, return a record. + * + * @template {[PropertyKey, unknown][]} T - The list of key-value pairs. + * @returns {Record} The record. + * + * @example + * ```ts + * import { $, Object } from "hkt-toolbelt"; + * + * type T0 = Object._$fromEntries<[['foo', 'bar'], ['baz', 42]]> + * // ^? { foo: 'bar', baz: 42 } + * ``` + */ +export type _$fromEntries = + Union._$toIntersection< + { + [K in keyof T]: { [key in T[K][0]]: T[K][1] } + }[number] + > + +/** + * Given a list of key-value pairs, return a record. + * + * @template {[PropertyKey, unknown][]} T - The list of key-value pairs. + * @returns {Record} The record. + * + * @example + * ```ts + * import { $, Object } from "hkt-toolbelt"; + * + * type T0 = $ + * // ^? { foo: 'bar', baz: 42 } + * ``` + */ +export interface FromEntries extends Kind.Kind { + f( + x: Type._$cast + ): _$fromEntries +} + +/** + * Given a list of key-value pairs, return a record. + * + * @template {[PropertyKey, unknown][]} T - The list of key-value pairs. + * @returns {Record} The record. + * + * @example + * ```ts + * import { $, Object } from "hkt-toolbelt"; + * + * const T0 = Object.fromEntries([['foo', 'bar'], ['baz', 42]]) + * // ^? { foo: 'bar', baz: 42 } + */ +export const fromEntries = ((x: [PropertyKey, unknown][]) => + Object.fromEntries(x)) as Kind._$reify From 8a0f530e1e97c8c0dc74bd143c7927796d116f51 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 14:35:07 -0800 Subject: [PATCH 03/27] feat: implement is-object util --- src/object/is-object.test.ts | 30 ++++++++++++++++++++ src/object/is-object.ts | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/object/is-object.test.ts create mode 100644 src/object/is-object.ts diff --git a/src/object/is-object.test.ts b/src/object/is-object.test.ts new file mode 100644 index 00000000..98d4a633 --- /dev/null +++ b/src/object/is-object.test.ts @@ -0,0 +1,30 @@ +import { $, Test, Object } from '..' + +type IsObject_Spec = [ + /** + * Can check if a number is an object. + */ + Test.Expect<$, false>, + + /** + * Can check if an object is an object. + */ + Test.Expect<$, true>, + + /** + * Can check if a function is an object. + */ + Test.Expect<$ 42>, false> +] + +it('should return true if the input is an object', () => { + expect(Object.isObject({ foo: 'bar' })).toBe(true) +}) + +it('should return false if the input is not an object', () => { + expect(Object.isObject(42)).toBe(false) +}) + +it('should return false if the input is a function', () => { + expect(Object.isObject(() => 42)).toBe(false) +}) diff --git a/src/object/is-object.ts b/src/object/is-object.ts new file mode 100644 index 00000000..2eaa1167 --- /dev/null +++ b/src/object/is-object.ts @@ -0,0 +1,53 @@ +import { Type, Kind } from '..' + +/** + * Given a value, return whether or not it is an object. + * + * @param {unknown} x - The value to check. + * @return {boolean} Whether or not the value is an object. + * + * @example + * ```ts + * import { Object } from "hkt-toolbelt"; + * + * const T0 = Object._$isObject<42>; // false + * const T1 = Object._$isObject<{ foo: 'bar' }>; // true + * ``` + */ +export type _$isObject = + T extends Record ? true : false + +/** + * Given a value, return whether or not it is an object. + * + * @param {unknown} x - The value to check. + * @return {boolean} Whether or not the value is an object. + * + * @example + * ```ts + * import { Object } from "hkt-toolbelt"; + * + * const T0 = $; // false + * const T1 = $; // true + * ``` + */ +export interface IsObject extends Kind.Kind { + f(x: Type._$cast): _$isObject +} + +/** + * Given a value, return whether or not it is an object. + * + * @param {unknown} x - The value to check. + * @return {boolean} Whether or not the value is an object. + * + * @example + * ```ts + * import { Object } from "hkt-toolbelt"; + * + * const T0 = $<$>; // false + * const T1 = $<$>; // true + * ``` + */ +export const isObject = ((x: unknown) => + typeof x === 'object' && x !== null) as unknown as Kind._$reify From 61fb76a3a15cd0eb17a8ae17dd4974a36c0cb511 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 16:27:25 -0800 Subject: [PATCH 04/27] feat: reify divide-by nat num util --- .../{divide-by.spec.ts => divide-by.test.ts} | 10 ++++ src/natural-number/divide-by.ts | 56 +++++++++++++++---- 2 files changed, 54 insertions(+), 12 deletions(-) rename src/natural-number/{divide-by.spec.ts => divide-by.test.ts} (76%) diff --git a/src/natural-number/divide-by.spec.ts b/src/natural-number/divide-by.test.ts similarity index 76% rename from src/natural-number/divide-by.spec.ts rename to src/natural-number/divide-by.test.ts index f8a116c7..6b4e255b 100644 --- a/src/natural-number/divide-by.spec.ts +++ b/src/natural-number/divide-by.test.ts @@ -46,3 +46,13 @@ type DivideBy_Spec = [ */ Test.Expect<$<$, 100>, 1> ] + +it('should return the result of dividing two natural numbers', () => { + const result = NaturalNumber.divideBy(2)(10) + expect(result).toBe(5) +}) + +it('should return the result of dividing two natural numbers', () => { + const result = NaturalNumber.divideBy(17)(123) + expect(result).toBe(7) +}) diff --git a/src/natural-number/divide-by.ts b/src/natural-number/divide-by.ts index 208de0e3..e6e307b3 100644 --- a/src/natural-number/divide-by.ts +++ b/src/natural-number/divide-by.ts @@ -1,20 +1,37 @@ -import { Type, Number, Kind, NaturalNumber } from '..' +import { Type, Number as Number_, Kind, NaturalNumber } from '..' -interface DivideBy_T extends Kind.Kind { - f( - x: Type._$cast - ): Number._$isNatural extends true - ? NaturalNumber._$divide +/** + * Given two natural numbers, divide the second by the first, and return the + * floored result as a natural number. + * + * @param {Number_.Number} A - The denominator. + * @param {Number_.Number} B - The numerator. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * type T0 = NaturalNumber._$divideBy<10, 42>; // 4 + * ``` + */ +export type _$divideBy = + Number_._$isNatural extends true + ? Number_._$isNatural extends true + ? NaturalNumber._$divide + : never : never + +export interface DivideBy_T extends Kind.Kind { + f(x: Type._$cast): _$divideBy } /** * `DivideBy` is a type-level function that takes in two natural number types, * `A` and `B`, and returns the result of dividing `B` by `A`. * - * @template {Number.Number} A - A natural number to divide by. - * @template {Number.Number} B - A natural number to be divided. - * @returns {Number.Number} A natural number type or `never`. + * @template {Number_.Number} A - A natural number to divide by. + * @template {Number_.Number} B - A natural number to be divided. + * @returns {Number_.Number} A natural number type or `never`. * * The parameters are reversed from `Divide`. This is useful for partial * application, i.e. to test divisibility. @@ -42,7 +59,22 @@ interface DivideBy_T extends Kind.Kind { * ``` */ export interface DivideBy extends Kind.Kind { - f( - x: Type._$cast - ): Number._$isNatural extends true ? DivideBy_T : never + f(x: Type._$cast): DivideBy_T } + +/** + * Given two natural numbers, divide the second by the first, and return the + * floored result as a natural number. + * + * @param {Number_.Number} A - The denominator. + * @param {Number_.Number} B - The numerator. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const T0 = NaturalNumber.divideBy(10)(42); // 4 + * ``` + */ +export const divideBy = ((a: Number_.Number) => (b: Number_.Number) => + Math.floor(Number(b) / Number(a))) as Kind._$reify From 373a9b9fd6c02932d1c52639963a090f94d77ee0 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 16:31:29 -0800 Subject: [PATCH 05/27] feat: reify nat num divide util --- .../{divide.spec.ts => divide.test.ts} | 10 +++++ src/natural-number/divide.ts | 45 +++++++++++++------ 2 files changed, 41 insertions(+), 14 deletions(-) rename src/natural-number/{divide.spec.ts => divide.test.ts} (77%) diff --git a/src/natural-number/divide.spec.ts b/src/natural-number/divide.test.ts similarity index 77% rename from src/natural-number/divide.spec.ts rename to src/natural-number/divide.test.ts index 9bde66e8..b2ed91cb 100644 --- a/src/natural-number/divide.spec.ts +++ b/src/natural-number/divide.test.ts @@ -51,3 +51,13 @@ type Divide_Spec = [ */ Test.Expect<$<$, 99>, 1> ] + +it('should return the result of dividing two natural numbers', () => { + const result = NaturalNumber.divide(42)(10) + expect(result).toBe(4) +}) + +it('should return the result of dividing two natural numbers', () => { + const result = NaturalNumber.divide(123)(17) + expect(result).toBe(7) +}) diff --git a/src/natural-number/divide.ts b/src/natural-number/divide.ts index ab469972..150912b8 100644 --- a/src/natural-number/divide.ts +++ b/src/natural-number/divide.ts @@ -1,13 +1,13 @@ -import { Type, Number, Kind, DigitList, NaturalNumber } from '..' +import { Type, Number as Number_, Kind, DigitList, NaturalNumber } from '..' /** * `_$divide` is a type-level function that performs the division operation. * It takes in two natural numbers `A` and `B` representing the dividend and divisor respectively, * and returns the result of dividing `A` by `B`. * - * @template {Number.Number} A - A natural number to divide. - * @template {Number.Number} B - A natural number to divide by. - * @returns {Number.Number} A natural number type. + * @template {Number_.Number} A - A natural number to divide. + * @template {Number_.Number} B - A natural number to divide by. + * @returns {Number_.Number} A natural number type. * * If `A` is not a multiple of `B`, the quotient is returned and the remainder is thrown away. * @@ -30,8 +30,8 @@ import { Type, Number, Kind, DigitList, NaturalNumber } from '..' * ``` */ export type _$divide< - A extends Number.Number, - B extends Number.Number, + A extends Number_.Number, + B extends Number_.Number, A_LIST extends DigitList.DigitList = NaturalNumber._$toList, B_LIST extends DigitList.DigitList = NaturalNumber._$toList, QUOTIENT_LIST = DigitList._$divide, @@ -40,19 +40,19 @@ export type _$divide< > > = QUOTIENT -interface Divide_T extends Kind.Kind { +interface Divide_T extends Kind.Kind { f( - x: Type._$cast - ): Number._$isNatural extends true ? _$divide : never + x: Type._$cast + ): Number_._$isNatural extends true ? _$divide : never } /** * `Divide` is a type-level function that takes in two natural numbers and performs a division operation. * It returns the result of the division operation. * - * @template {Number.Number} A - A natural number to divide. - * @template {Number.Number} B - A natural number to divide by. - * @returns {Number.Number} A natural number type. + * @template {Number_.Number} A - A natural number to divide. + * @template {Number_.Number} B - A natural number to divide by. + * @returns {Number_.Number} A natural number type. * * If `A` is not a multiple of `B`, the quotient is returned and the remainder is thrown away. * @@ -87,6 +87,23 @@ interface Divide_T extends Kind.Kind { */ export interface Divide extends Kind.Kind { f( - x: Type._$cast - ): Number._$isNatural extends true ? Divide_T : never + x: Type._$cast + ): Number_._$isNatural extends true ? Divide_T : never } + +/** + * Given two natural numbers, divide the first by the second, and return the + * floored result as a natural number. + * + * @param {Number_.Number} A - The numerator. + * @param {Number_.Number} B - The denominator. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const T0 = NaturalNumber.divide(42)(10); // 4 + * ``` + */ +export const divide = ((a: Number_.Number) => (b: Number_.Number) => + Math.floor(Number(a) / Number(b))) as Kind._$reify From 915f86a1bb9ab9e1271e19a264d15771ede41578 Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 16:33:52 -0800 Subject: [PATCH 06/27] feat: reify is-even util --- .../{is-even.spec.ts => is-even.test.ts} | 8 ++++++ src/natural-number/is-even.ts | 27 ++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) rename src/natural-number/{is-even.spec.ts => is-even.test.ts} (87%) diff --git a/src/natural-number/is-even.spec.ts b/src/natural-number/is-even.test.ts similarity index 87% rename from src/natural-number/is-even.spec.ts rename to src/natural-number/is-even.test.ts index 75df3b06..1ae28c6e 100644 --- a/src/natural-number/is-even.spec.ts +++ b/src/natural-number/is-even.test.ts @@ -96,3 +96,11 @@ type IsEven_Spec = [ */ Test.Expect<$, true> ] + +it('should return true if the input is an even number', () => { + expect(NaturalNumber.isEven(42)).toBe(true) +}) + +it('should return false if the input is an odd number', () => { + expect(NaturalNumber.isEven(43)).toBe(false) +}) diff --git a/src/natural-number/is-even.ts b/src/natural-number/is-even.ts index 65b9e680..d2da09cc 100644 --- a/src/natural-number/is-even.ts +++ b/src/natural-number/is-even.ts @@ -1,14 +1,14 @@ -import { Type, Kind, Number, NaturalNumber, DigitList } from '..' +import { Type, Kind, Number as Number_, NaturalNumber, DigitList } from '..' /** * `_$isEven` is a type-level function that takes in a natural number type, * `A`, and returns a boolean indicating whether `A` is an even number * - * @template {Number.Number} A - A natural number. + * @template {Number_.Number} A - A natural number. * @returns {boolean} */ export type _$isEven< - T extends Number.Number, + T extends Number_.Number, LIST extends DigitList.DigitList = NaturalNumber._$toList, RESULT = DigitList._$isEven > = RESULT @@ -17,11 +17,26 @@ export type _$isEven< * `IsEven` is a type-level function that takes in a natural number type, * `A`, and returns a boolean indicating whether `A` is an even number * - * @template {Number.Number} A - A natural number. + * @template {Number_.Number} A - A natural number. * @returns {boolean} */ export interface IsEven extends Kind.Kind { f( - x: Type._$cast - ): Number._$isNatural extends true ? _$isEven : never + x: Type._$cast + ): Number_._$isNatural extends true ? _$isEven : never } + +/** + * Given a natural number, return whether or not it is an even number. + * + * @param {Number_.Number} x - The natural number to check. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const T0 = NaturalNumber.isEven(42); // true + * ``` + */ +export const isEven = ((x: Number_.Number) => + Number(x) % 2 === 0) as Kind._$reify From b5cbdcb856c5dc44ba982251e54fe1de17bc3f5c Mon Sep 17 00:00:00 2001 From: poteat Date: Sun, 29 Dec 2024 17:01:16 -0800 Subject: [PATCH 07/27] feat: reify is-odd nat num util, export intermediary interfaces --- src/natural-number/compare.ts | 2 +- src/natural-number/difference.ts | 2 +- src/natural-number/divide.ts | 2 +- .../is-greater-than-or-equal.ts | 3 +- src/natural-number/is-greater-than.ts | 2 +- src/natural-number/is-less-than-or-equal.ts | 3 +- src/natural-number/is-less-than.ts | 2 +- .../{is-odd.spec.ts => is-odd.test.ts} | 8 ++++++ src/natural-number/is-odd.ts | 28 +++++++++++++++---- src/natural-number/modulo-by.ts | 2 +- src/natural-number/modulo.ts | 2 +- src/natural-number/multiply.ts | 2 +- src/natural-number/subtract-by.ts | 27 ++++++++++++++---- src/natural-number/subtract.ts | 2 +- 14 files changed, 65 insertions(+), 22 deletions(-) rename src/natural-number/{is-odd.spec.ts => is-odd.test.ts} (87%) diff --git a/src/natural-number/compare.ts b/src/natural-number/compare.ts index 1b354d44..e8f75bf6 100644 --- a/src/natural-number/compare.ts +++ b/src/natural-number/compare.ts @@ -40,7 +40,7 @@ export type _$compare< RESULT extends -1 | 0 | 1 = DigitList._$compare > = RESULT -interface Compare_T extends Kind.Kind { +export interface Compare_T extends Kind.Kind { f( x: Type._$cast ): Number._$isNatural extends true ? _$compare : never diff --git a/src/natural-number/difference.ts b/src/natural-number/difference.ts index f9b552c4..10a9a011 100644 --- a/src/natural-number/difference.ts +++ b/src/natural-number/difference.ts @@ -21,7 +21,7 @@ export type _$difference = ? NaturalNumber._$subtract : NaturalNumber._$subtract -interface Difference_T extends Kind.Kind { +export interface Difference_T extends Kind.Kind { f(x: Type._$cast): _$difference } diff --git a/src/natural-number/divide.ts b/src/natural-number/divide.ts index 150912b8..e69a6df7 100644 --- a/src/natural-number/divide.ts +++ b/src/natural-number/divide.ts @@ -40,7 +40,7 @@ export type _$divide< > > = QUOTIENT -interface Divide_T extends Kind.Kind { +export interface Divide_T extends Kind.Kind { f( x: Type._$cast ): Number_._$isNatural extends true ? _$divide : never diff --git a/src/natural-number/is-greater-than-or-equal.ts b/src/natural-number/is-greater-than-or-equal.ts index 1f987e18..b201510b 100644 --- a/src/natural-number/is-greater-than-or-equal.ts +++ b/src/natural-number/is-greater-than-or-equal.ts @@ -29,7 +29,8 @@ export type _$isGreaterThanOrEqual< : false > = RESULT -interface IsGreaterThanOrEqual_T extends Kind.Kind { +export interface IsGreaterThanOrEqual_T + extends Kind.Kind { f( x: Type._$cast ): _$isGreaterThanOrEqual diff --git a/src/natural-number/is-greater-than.ts b/src/natural-number/is-greater-than.ts index 4de4f2e7..49cee4a2 100644 --- a/src/natural-number/is-greater-than.ts +++ b/src/natural-number/is-greater-than.ts @@ -29,7 +29,7 @@ export type _$isGreaterThan< : false > = RESULT -interface IsGreaterThan_T extends Kind.Kind { +export interface IsGreaterThan_T extends Kind.Kind { f(x: Type._$cast): _$isGreaterThan } diff --git a/src/natural-number/is-less-than-or-equal.ts b/src/natural-number/is-less-than-or-equal.ts index c70868c3..351bc338 100644 --- a/src/natural-number/is-less-than-or-equal.ts +++ b/src/natural-number/is-less-than-or-equal.ts @@ -29,7 +29,8 @@ export type _$isLessThanOrEqual< : false > = RESULT -interface IsLessThanOrEqual_T extends Kind.Kind { +export interface IsLessThanOrEqual_T + extends Kind.Kind { f( x: Type._$cast ): _$isLessThanOrEqual diff --git a/src/natural-number/is-less-than.ts b/src/natural-number/is-less-than.ts index 68748373..9f558e9b 100644 --- a/src/natural-number/is-less-than.ts +++ b/src/natural-number/is-less-than.ts @@ -29,7 +29,7 @@ export type _$isLessThan< : false > = RESULT -interface IsLessThan_T extends Kind.Kind { +export interface IsLessThan_T extends Kind.Kind { f(x: Type._$cast): _$isLessThan } diff --git a/src/natural-number/is-odd.spec.ts b/src/natural-number/is-odd.test.ts similarity index 87% rename from src/natural-number/is-odd.spec.ts rename to src/natural-number/is-odd.test.ts index d7f6f79a..e968e811 100644 --- a/src/natural-number/is-odd.spec.ts +++ b/src/natural-number/is-odd.test.ts @@ -96,3 +96,11 @@ type IsOdd_Spec = [ */ Test.Expect<$, true> ] + +it('should return true if the input is an odd number', () => { + expect(NaturalNumber.isOdd(42)).toBe(false) +}) + +it('should return false if the input is an even number', () => { + expect(NaturalNumber.isOdd(43)).toBe(true) +}) diff --git a/src/natural-number/is-odd.ts b/src/natural-number/is-odd.ts index 5c80e9a8..ec28a753 100644 --- a/src/natural-number/is-odd.ts +++ b/src/natural-number/is-odd.ts @@ -1,14 +1,14 @@ -import { Type, Kind, DigitList, Number, NaturalNumber } from '..' +import { Type, Kind, DigitList, Number as Number_, NaturalNumber } from '..' /** * `_$isOdd` is a type-level function that takes in a natural number type, * `A`, and returns a boolean indicating whether `A` is an odd number * - * @template {Number.Number} A - A natural number. + * @template {Number_.Number} A - A natural number. * @returns {boolean} */ export type _$isOdd< - T extends Number.Number, + T extends Number_.Number, LIST extends DigitList.DigitList = NaturalNumber._$toList, RESULT = DigitList._$isOdd > = RESULT @@ -17,11 +17,27 @@ export type _$isOdd< * `IsOdd` is a type-level function that takes in a natural number type, * `A`, and returns a boolean indicating whether `A` is an odd number * - * @template {Number.Number} A - A natural number. + * @template {Number_.Number} A - A natural number. * @returns {boolean} */ export interface IsOdd extends Kind.Kind { f( - x: Type._$cast - ): Number._$isNatural extends true ? _$isOdd : never + x: Type._$cast + ): Number_._$isNatural extends true ? _$isOdd : never } + +/** + * Given a natural number, return whether or not it is an odd number. + * + * @param {Number_.Number} x - The natural number to check. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const T0 = NaturalNumber.isOdd(42); // false + * const T1 = NaturalNumber.isOdd(43); // true + * ``` + */ +export const isOdd = ((x: Number_.Number) => + Number(x) % 2 === 1) as Kind._$reify diff --git a/src/natural-number/modulo-by.ts b/src/natural-number/modulo-by.ts index 98fa1fc8..33f45778 100644 --- a/src/natural-number/modulo-by.ts +++ b/src/natural-number/modulo-by.ts @@ -21,7 +21,7 @@ export type _$moduloBy< B extends number > = NaturalNumber._$modulo -interface ModuloBy_T extends Kind.Kind { +export interface ModuloBy_T extends Kind.Kind { f( x: Type._$cast ): Number._$isNatural extends true ? _$moduloBy : never diff --git a/src/natural-number/modulo.ts b/src/natural-number/modulo.ts index f8248868..8afcc72c 100644 --- a/src/natural-number/modulo.ts +++ b/src/natural-number/modulo.ts @@ -16,7 +16,7 @@ export type _$modulo< MODULUS = DigitList._$toNumber> > = MODULUS -interface Modulo_T extends Kind.Kind { +export interface Modulo_T extends Kind.Kind { f( x: Type._$cast ): Number_._$isNatural extends true ? _$modulo : never diff --git a/src/natural-number/multiply.ts b/src/natural-number/multiply.ts index 2461b657..e8a4d266 100644 --- a/src/natural-number/multiply.ts +++ b/src/natural-number/multiply.ts @@ -45,7 +45,7 @@ export type _$multiply = : never : never -interface Multiply_T extends Kind.Kind { +export interface Multiply_T extends Kind.Kind { f(x: Type._$cast): _$multiply } diff --git a/src/natural-number/subtract-by.ts b/src/natural-number/subtract-by.ts index 4650aac4..2dad5357 100644 --- a/src/natural-number/subtract-by.ts +++ b/src/natural-number/subtract-by.ts @@ -1,11 +1,28 @@ import { Type, Number, Kind, NaturalNumber } from '..' -interface SubtractBy_T extends Kind.Kind { - f( - x: Type._$cast - ): Number._$isNatural extends true - ? NaturalNumber._$subtract +/** + * Given two natural numbers, subtract the second from the first, and return the + * result as a natural number. + * + * @param {Number.Number} A - The minuend. + * @param {Number.Number} B - The subtrahend. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * type T0 = NaturalNumber._$subtractBy<10, 42>; // 32 + * ``` + */ +export type _$subtractBy = + Number._$isNatural extends true + ? Number._$isNatural extends true + ? NaturalNumber._$subtract + : never : never + +export interface SubtractBy_T extends Kind.Kind { + f(x: Type._$cast): _$subtractBy } /** diff --git a/src/natural-number/subtract.ts b/src/natural-number/subtract.ts index d9117644..b9d50389 100644 --- a/src/natural-number/subtract.ts +++ b/src/natural-number/subtract.ts @@ -35,7 +35,7 @@ export type _$subtract< RESULT = DigitList._$toNumber > = RESULT -interface Subtract_T extends Kind.Kind { +export interface Subtract_T extends Kind.Kind { f( x: Type._$cast ): Number._$isNatural extends true ? _$subtract : never From 649254807b8b3afc23c720f17bc321e8c53cf97b Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 2 Jan 2025 14:47:27 -0800 Subject: [PATCH 08/27] feat: reify string first and last methods --- changelog.md | 11 +++++++++++ src/string/{first.spec.ts => first.test.ts} | 8 ++++++++ src/string/first.ts | 15 +++++++++++++++ src/string/{last.spec.ts => last.test.ts} | 8 ++++++++ src/string/last.ts | 15 +++++++++++++++ 5 files changed, 57 insertions(+) rename src/string/{first.spec.ts => first.test.ts} (83%) rename src/string/{last.spec.ts => last.test.ts} (83%) diff --git a/changelog.md b/changelog.md index de012b77..eaed88b4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## [~] + +- Add `Object.DeepEntries` to get all deep paths and values in an object. +- Add `Object.FromEntries` to create an object from a list of key-value pairs. +- Add `Object.IsObject` to check if a value is an object. +- Reify `NaturalNumber.DivideBy` to a value-level function. +- Reify `NaturalNumber.Divide` to a value-level function. +- Reify `NaturalNumber.IsEven` to a value-level function. +- Reify `NaturalNumber.IsOdd` to a value-level function. +- Reify `String.First` and `String.Last` to value-level functions. + ## [0.25.2] - Fix `List.Sort` to work with 2-ary comparators. diff --git a/src/string/first.spec.ts b/src/string/first.test.ts similarity index 83% rename from src/string/first.spec.ts rename to src/string/first.test.ts index 5f02dd3c..8a74beb0 100644 --- a/src/string/first.spec.ts +++ b/src/string/first.test.ts @@ -46,3 +46,11 @@ type First_Spec = [ */ Test.Expect<$, string> ] + +it('should return the first character of a string', () => { + expect(String.first('foo')).toBe('f') +}) + +it('should return the first character of an empty string', () => { + expect(String.first('')).toBe('') +}) diff --git a/src/string/first.ts b/src/string/first.ts index 8dd9bded..ffa47a0a 100644 --- a/src/string/first.ts +++ b/src/string/first.ts @@ -26,3 +26,18 @@ export type _$first = S extends `${infer Head}${string}` export interface First extends Kind.Kind { f(x: Type._$cast): _$first } + +/** + * Given a string, return the first character of the string. + * + * @param {string} x - The string to get the first character of. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String.first('foo'); // 'f' + * const T1 = String.first(''); // '' + * ``` + */ +export const first = ((x: string) => x[0] ?? '') as Kind._$reify diff --git a/src/string/last.spec.ts b/src/string/last.test.ts similarity index 83% rename from src/string/last.spec.ts rename to src/string/last.test.ts index 9e669a1f..eefbacca 100644 --- a/src/string/last.spec.ts +++ b/src/string/last.test.ts @@ -46,3 +46,11 @@ type Last_Spec = [ */ Test.Expect<$, string> ] + +it('should return the last character of a string', () => { + expect(String.last('foo')).toBe('o') +}) + +it('should return the last character of an empty string', () => { + expect(String.last('')).toBe('') +}) diff --git a/src/string/last.ts b/src/string/last.ts index 72fe8420..ab54ecbb 100644 --- a/src/string/last.ts +++ b/src/string/last.ts @@ -29,3 +29,18 @@ export type _$last = S extends `${string}${infer Tail}` export interface Last extends Kind.Kind { f(x: Type._$cast): _$last } + +/** + * Given a string, return the last character of the string. + * + * @param {string} x - The string to get the last character of. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String.last('foo'); // 'o' + * const T1 = String.last(''); // '' + * ``` + */ +export const last = ((x: string) => x[x.length - 1] ?? '') as Kind._$reify From 071854cda41f2d3c71bd03b3c0ca45ed6c31109d Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 2 Jan 2025 14:55:42 -0800 Subject: [PATCH 09/27] feat: add string rest util --- src/string/index.ts | 1 + src/string/rest.test.ts | 26 +++++++++++++++++++++ src/string/rest.ts | 50 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/string/rest.test.ts create mode 100644 src/string/rest.ts diff --git a/src/string/index.ts b/src/string/index.ts index ef03f0f9..bd94096a 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -27,6 +27,7 @@ export * from './pascal-case' export * from './prepend' export * from './repeat' export * from './replace' +export * from './rest' export * from './reverse' export * from './slice' export * from './snake-case' diff --git a/src/string/rest.test.ts b/src/string/rest.test.ts new file mode 100644 index 00000000..b03422ab --- /dev/null +++ b/src/string/rest.test.ts @@ -0,0 +1,26 @@ +import { $, String, Test } from '..' + +type Rest_Spec = [ + /** + * Can remove the first character from a string. + */ + Test.Expect<$, 'oo'>, + + /** + * Can remove the first character from an empty string. + */ + Test.Expect<$, ''>, + + /** + * Can remove the first character from a string. + */ + Test.Expect<$, `oo${string}`> +] + +it('should return the string with the first character removed', () => { + expect(String.rest('foo')).toBe('oo') +}) + +it('should return an empty string if the string is empty', () => { + expect(String.rest('')).toBe('') +}) diff --git a/src/string/rest.ts b/src/string/rest.ts new file mode 100644 index 00000000..93ac727f --- /dev/null +++ b/src/string/rest.ts @@ -0,0 +1,50 @@ +import { Type, Kind } from '..' + +/** + * Given a string, return the string with the first character removed. + * + * @template S - The string to remove the first character from. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type T0 = String._$rest<'foo'>; // 'oo' + * type T1 = String._$rest<''>; // '' + * ``` + */ +export type _$rest = S extends `${string}${infer Rest}` + ? Rest + : '' + +/** + * Given a string, return the string with the first character removed. + * + * @template S - The string to remove the first character from. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type T0 = $; // 'oo' + * type T1 = $; // '' + * ``` + */ +export interface Rest extends Kind.Kind { + f(x: Type._$cast): _$rest +} + +/** + * Given a string, return the string with the first character removed. + * + * @param {string} x - The string to remove the first character from. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String.rest('foo'); // 'oo' + * const T1 = String.rest(''); // '' + * ``` + */ +export const rest = ((x: string) => x.slice(1)) as Kind._$reify From f348a510ee60529aa42a4f3a3b6f94f0b0ee97c3 Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 2 Jan 2025 14:55:52 -0800 Subject: [PATCH 10/27] chore: add test case for to-upper util --- src/string/to-upper.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/string/to-upper.test.ts b/src/string/to-upper.test.ts index c9f17459..884fbb3e 100644 --- a/src/string/to-upper.test.ts +++ b/src/string/to-upper.test.ts @@ -53,3 +53,7 @@ type ToUpper_Spec = [ it('should convert a string to uppercase', () => { expect(String.toUpper('foo')).toBe('FOO') }) + +it('should convert an empty string to uppercase', () => { + expect(String.toUpper('')).toBe('') +}) From 1acfe1477e4b776936a752e6cba647650c1fc85e Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 2 Jan 2025 15:16:30 -0800 Subject: [PATCH 11/27] feat: reify string init and tail utils --- src/string/index.ts | 1 - src/string/{init.spec.ts => init.test.ts} | 8 ++++ src/string/init.ts | 5 +++ src/string/rest.test.ts | 26 ------------ src/string/rest.ts | 50 ----------------------- src/string/{tail.spec.ts => tail.test.ts} | 8 ++++ src/string/tail.ts | 15 +++++++ 7 files changed, 36 insertions(+), 77 deletions(-) rename src/string/{init.spec.ts => init.test.ts} (82%) delete mode 100644 src/string/rest.test.ts delete mode 100644 src/string/rest.ts rename src/string/{tail.spec.ts => tail.test.ts} (82%) diff --git a/src/string/index.ts b/src/string/index.ts index bd94096a..ef03f0f9 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -27,7 +27,6 @@ export * from './pascal-case' export * from './prepend' export * from './repeat' export * from './replace' -export * from './rest' export * from './reverse' export * from './slice' export * from './snake-case' diff --git a/src/string/init.spec.ts b/src/string/init.test.ts similarity index 82% rename from src/string/init.spec.ts rename to src/string/init.test.ts index ece14221..c8573c50 100644 --- a/src/string/init.spec.ts +++ b/src/string/init.test.ts @@ -45,3 +45,11 @@ type Init_Spec = [ */ Test.Expect<$, string> ] + +it('should return the string with the last character removed', () => { + expect(String.init('hello')).toBe('hell') +}) + +it('should return an empty string if the string is empty', () => { + expect(String.init('')).toBe('') +}) diff --git a/src/string/init.ts b/src/string/init.ts index 4d481b4f..16d262b1 100644 --- a/src/string/init.ts +++ b/src/string/init.ts @@ -32,3 +32,8 @@ export type _$init = string extends S ? string : _$init2 export interface Init extends Kind.Kind { f(x: Type._$cast): _$init } + +/** + * Given a string, return the string with the last character removed. + */ +export const init = ((x: string) => x.slice(0, -1)) as Kind._$reify diff --git a/src/string/rest.test.ts b/src/string/rest.test.ts deleted file mode 100644 index b03422ab..00000000 --- a/src/string/rest.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { $, String, Test } from '..' - -type Rest_Spec = [ - /** - * Can remove the first character from a string. - */ - Test.Expect<$, 'oo'>, - - /** - * Can remove the first character from an empty string. - */ - Test.Expect<$, ''>, - - /** - * Can remove the first character from a string. - */ - Test.Expect<$, `oo${string}`> -] - -it('should return the string with the first character removed', () => { - expect(String.rest('foo')).toBe('oo') -}) - -it('should return an empty string if the string is empty', () => { - expect(String.rest('')).toBe('') -}) diff --git a/src/string/rest.ts b/src/string/rest.ts deleted file mode 100644 index 93ac727f..00000000 --- a/src/string/rest.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Type, Kind } from '..' - -/** - * Given a string, return the string with the first character removed. - * - * @template S - The string to remove the first character from. - * - * @example - * ```ts - * import { String } from "hkt-toolbelt"; - * - * type T0 = String._$rest<'foo'>; // 'oo' - * type T1 = String._$rest<''>; // '' - * ``` - */ -export type _$rest = S extends `${string}${infer Rest}` - ? Rest - : '' - -/** - * Given a string, return the string with the first character removed. - * - * @template S - The string to remove the first character from. - * - * @example - * ```ts - * import { String } from "hkt-toolbelt"; - * - * type T0 = $; // 'oo' - * type T1 = $; // '' - * ``` - */ -export interface Rest extends Kind.Kind { - f(x: Type._$cast): _$rest -} - -/** - * Given a string, return the string with the first character removed. - * - * @param {string} x - The string to remove the first character from. - * - * @example - * ```ts - * import { String } from "hkt-toolbelt"; - * - * const T0 = String.rest('foo'); // 'oo' - * const T1 = String.rest(''); // '' - * ``` - */ -export const rest = ((x: string) => x.slice(1)) as Kind._$reify diff --git a/src/string/tail.spec.ts b/src/string/tail.test.ts similarity index 82% rename from src/string/tail.spec.ts rename to src/string/tail.test.ts index e293e4ca..e922e424 100644 --- a/src/string/tail.spec.ts +++ b/src/string/tail.test.ts @@ -46,3 +46,11 @@ type Tail_Spec = [ */ Test.Expect<$, string> ] + +it('should return the string with the first character removed', () => { + expect(String.tail('hello')).toBe('ello') +}) + +it('should return an empty string if the string is empty', () => { + expect(String.tail('')).toBe('') +}) diff --git a/src/string/tail.ts b/src/string/tail.ts index 0c09d853..1c0a606b 100644 --- a/src/string/tail.ts +++ b/src/string/tail.ts @@ -29,3 +29,18 @@ export type _$tail = S extends `${string}${infer Tail}` export interface Tail extends Kind.Kind { f(x: Type._$cast): _$tail } + +/** + * Given a string, return the string with the first character removed. + * + * @param {string} x - The string to remove the first character from. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String.tail('hello'); // 'ello' + * const T1 = String.tail(''); // '' + * ``` + */ +export const tail = ((x: string) => x.slice(1)) as Kind._$reify From 84a6379d9e8f16456ce5510267a42df52afb154b Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 2 Jan 2025 15:16:51 -0800 Subject: [PATCH 12/27] chore: add changelog entry for init / tail --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index eaed88b4..eff46b0c 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ - Reify `NaturalNumber.IsEven` to a value-level function. - Reify `NaturalNumber.IsOdd` to a value-level function. - Reify `String.First` and `String.Last` to value-level functions. +- Reify `String.Init` and `String.Tail` to value-level functions. ## [0.25.2] From 1b51f032fad0101ad10c1b78ff19916134a30a7a Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 2 Jan 2025 16:16:31 -0800 Subject: [PATCH 13/27] feat: add 'unwords' util for strings --- changelog.md | 1 + src/string/index.ts | 1 + src/string/unwords.test.ts | 21 +++++++++++++++ src/string/unwords.ts | 52 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 src/string/unwords.test.ts create mode 100644 src/string/unwords.ts diff --git a/changelog.md b/changelog.md index eff46b0c..5a437486 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - Reify `NaturalNumber.IsOdd` to a value-level function. - Reify `String.First` and `String.Last` to value-level functions. - Reify `String.Init` and `String.Tail` to value-level functions. +- Add `String.Unwords` to join a list of strings with a space delimiter. ## [0.25.2] diff --git a/src/string/index.ts b/src/string/index.ts index ef03f0f9..70582316 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -37,4 +37,5 @@ export * from './to-char-code' export * from './to-list' export * from './to-lower' export * from './to-upper' +export * from './unwords' export * from './words' diff --git a/src/string/unwords.test.ts b/src/string/unwords.test.ts new file mode 100644 index 00000000..fccaa293 --- /dev/null +++ b/src/string/unwords.test.ts @@ -0,0 +1,21 @@ +import { $, String, Test } from '..' + +type Unwords_Spec = [ + /** + * Can join a list of strings with a space delimiter. + */ + Test.Expect<$, 'foo bar baz'>, + + /** + * An empty list results in an empty string. + */ + Test.Expect<$, ''> +] + +it('should join a list of strings with a space delimiter', () => { + expect(String.unwords(['foo', 'bar', 'baz'])).toBe('foo bar baz') +}) + +it('should return an empty string if the list is empty', () => { + expect(String.unwords([])).toBe('') +}) diff --git a/src/string/unwords.ts b/src/string/unwords.ts new file mode 100644 index 00000000..47dff0d1 --- /dev/null +++ b/src/string/unwords.ts @@ -0,0 +1,52 @@ +import { Type, Kind } from '..' + +/** + * Given a list of words, combine them into a single string, separated by spaces. + * + * @param {string[]} x - The list of words to join. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String._$unwords<['foo', 'bar']> // 'foo bar' + * ``` + */ +export type _$unwords = S extends [ + infer Head extends string, + ...infer Tail extends string[] +] + ? Tail extends [] + ? Head + : `${Head} ${_$unwords}` + : '' + +/** + * Given a list of words, combine them into a single string, separated by spaces. + * + * @param {string[]} x - The list of words to join. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = $ // 'foo bar' + * ``` + */ +export interface Unwords extends Kind.Kind { + f(x: Type._$cast): _$unwords +} + +/** + * Given a list of words, combine them into a single string, separated by spaces. + * + * @param {string[]} x - The list of words to join. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = $<$ // 'foo bar' + * ``` + */ +export const unwords = ((x: string[]) => x.join(' ')) as Kind._$reify From c37de4f51a06f9cb3f3c72660fec74f429f99ec5 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:04:38 -0800 Subject: [PATCH 14/27] feat: add string repeat-by arg-swapped util --- src/string/repeat-by.test.ts | 30 ++++++++++++++++ src/string/repeat-by.ts | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/string/repeat-by.test.ts create mode 100644 src/string/repeat-by.ts diff --git a/src/string/repeat-by.test.ts b/src/string/repeat-by.test.ts new file mode 100644 index 00000000..b6a8c680 --- /dev/null +++ b/src/string/repeat-by.test.ts @@ -0,0 +1,30 @@ +import { $, String, Test } from '..' + +type RepeatBy_Spec = [ + /** + * Can repeat a string by a given number. + */ + Test.Expect<$<$, 'foo'>, 'foofoofoo'>, + + /** + * Can repeat an empty string by a given number. + */ + Test.Expect<$<$, ''>, ''>, + + /** + * Can repeat a string by zero, results in an empty string. + */ + Test.Expect<$<$, 'foo'>, ''> +] + +it('should repeat a string by a given number', () => { + expect(String.repeatBy(3)('foo')).toBe('foofoofoo') +}) + +it('should repeat an empty string by a given number', () => { + expect(String.repeatBy(3)('')).toBe('') +}) + +it('should return an empty string if the number is zero', () => { + expect(String.repeatBy(0)('foo')).toBe('') +}) diff --git a/src/string/repeat-by.ts b/src/string/repeat-by.ts new file mode 100644 index 00000000..dc138033 --- /dev/null +++ b/src/string/repeat-by.ts @@ -0,0 +1,68 @@ +import { Type, Kind, Number, String } from '..' + +/** + * Given a natural number and a string, return a string with the original string + * repeated that number of times. + * + * This is an argument swapped version of `String._$repeat`. + * + * @param {Number.Number} N - The number of times to repeat the string. + * @param {string} S - The string to repeat. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type T0 = String._$repeatBy<3, 'foo'>; // 'foofoofoo' + * type T1 = String._$repeatBy<0, 'foo'>; // '' + * ``` + */ +export type _$repeatBy< + N extends Number.Number, + S extends string +> = String._$repeat + +export interface RepeatBy_T extends Kind.Kind { + f(x: Type._$cast): _$repeatBy +} + +/** + * Given a natural number and a string, return a string with the original string + * repeated that number of times. + * + * This is an argument swapped version of `String._$repeat`. + * + * @param {Number.Number} N - The number of times to repeat the string. + * @param {string} S - The string to repeat. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type T0 = $<$, 'foo'>; // 'foofoofoo' + * type T1 = $<$, 'foo'>; // '' + * ``` + */ +export interface RepeatBy extends Kind.Kind { + f(x: Type._$cast): RepeatBy_T +} + +/** + * Given a natural number and a string, return a string with the original string + * repeated that number of times. + * + * This is an argument swapped version of `String._$repeat`. + * + * @param {Number.Number} N - The number of times to repeat the string. + * @param {string} S - The string to repeat. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String.repeatBy(3)('foo'); // 'foofoofoo' + * const T1 = String.repeatBy(0)('foo'); // '' + * ``` + */ +export const repeatBy = ((n: Number.Number) => (s: string) => + String.repeat(s)(n as string)) as Kind._$reify From 0842d6bdcbfd68c346442d6ad8fd249df7ae0e90 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:04:49 -0800 Subject: [PATCH 15/27] feat: add string repeat-by arg-swapped util --- src/string/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/string/index.ts b/src/string/index.ts index 70582316..4b026c33 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -25,6 +25,7 @@ export * from './pad-end' export * from './pad-start' export * from './pascal-case' export * from './prepend' +export * from './repeat-by' export * from './repeat' export * from './replace' export * from './reverse' From 744aa012847f12d8f9c7741a5e88d964414ae009 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:12:19 -0800 Subject: [PATCH 16/27] feat: create the 'iso' module for isomorphic bijections --- src/index.ts | 3 +++ src/iso.ts | 24 ++++++++++++++++++++++++ src/iso/index.ts | 14 ++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 src/iso.ts create mode 100644 src/iso/index.ts diff --git a/src/index.ts b/src/index.ts index 64a3a09a..70ef31bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * as Digit from './digit' export * as DigitList from './digit-list' export * as Function from './function' export * as Integer from './integer' +export * as Iso from './iso' export * as Kind from './kind' export * as List from './list' export * as Loop from './loop' @@ -29,6 +30,7 @@ import * as Digit from './digit' import * as DigitList from './digit-list' import * as Function from './function' import * as Integer from './integer' +import * as Iso from './iso' import * as Kind from './kind' import * as List from './list' import * as Loop from './loop' @@ -53,6 +55,7 @@ const _ = { Digit, Function, Integer, + Iso, Kind, List, Loop, diff --git a/src/iso.ts b/src/iso.ts new file mode 100644 index 00000000..b65bd824 --- /dev/null +++ b/src/iso.ts @@ -0,0 +1,24 @@ +export * from './iso/' + +/** + * The `Iso` module contains various isomorphisms. These isomorphisms are used + * to transform between different representations of the same type. + * + * Generally, isomorphisms consist of a mapping and an inverse mapping. + * The isomorphism takes in a kind, and returns a new kind where the input is + * transformed via the primary mapping, and the output is transformed via the + * inverse mapping. + * + * A very common operation is to perform a .split(""), do some operations, and + * then .join("") the result. This can be combined into a single step using + * the `Iso` module. + * + * @example + * ```ts + * import { String, Iso } from "hkt-toolbelt"; + * + * type T0 = $>; + * type T1 = $; // 'Foo Bar Baz' + * ``` + */ +declare module './iso' {} diff --git a/src/iso/index.ts b/src/iso/index.ts new file mode 100644 index 00000000..d139aaa8 --- /dev/null +++ b/src/iso/index.ts @@ -0,0 +1,14 @@ +export * as String from './string' + +import * as String from './string' + +const _ = { + /** + * Foo + */ + String: String +} + +type _ = typeof _ + +export default _ From 9bc9842ef193ce9243768d5101779dfb7ccefcc1 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:12:37 -0800 Subject: [PATCH 17/27] feat: add the iso.string nested module --- src/iso/string.ts | 16 ++++++++++++++++ src/iso/string/index.ts | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 src/iso/string.ts create mode 100644 src/iso/string/index.ts diff --git a/src/iso/string.ts b/src/iso/string.ts new file mode 100644 index 00000000..34e75788 --- /dev/null +++ b/src/iso/string.ts @@ -0,0 +1,16 @@ +export * from './string/' + +/** + * The `Iso.String` module contains various string-related isomorphisms. These + * isomorphisms are used to transform between different representations of + * strings. + * + * @example + * ```ts + * import { String, Iso } from "hkt-toolbelt"; + * + * type T0 = $>; + * type T1 = $; // 'Foo Bar Baz' + * ``` + */ +declare module './string' {} diff --git a/src/iso/string/index.ts b/src/iso/string/index.ts new file mode 100644 index 00000000..ab4864c0 --- /dev/null +++ b/src/iso/string/index.ts @@ -0,0 +1,2 @@ +export * from './chars' +export * from './words' From 49e19aee17ad4e25d95f73d86dc96c7a8c44751a Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:13:03 -0800 Subject: [PATCH 18/27] feat: add the chars string isomorphism --- src/iso/string/chars.test.ts | 15 ++++++++++ src/iso/string/chars.ts | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/iso/string/chars.test.ts create mode 100644 src/iso/string/chars.ts diff --git a/src/iso/string/chars.test.ts b/src/iso/string/chars.test.ts new file mode 100644 index 00000000..af230fc5 --- /dev/null +++ b/src/iso/string/chars.test.ts @@ -0,0 +1,15 @@ +import { $, Iso, String, Test, List } from '../..' + +type Chars_Spec = [ + /** + * Can repeat each character of a string by two. + */ + Test.Expect< + $<$>>, 'bar'>, + 'bbaarr' + > +] + +it('should repeat each character of a string by two', () => { + expect(Iso.String.chars(List.map(String.repeatBy(2)))('bar')).toBe('bbaarr') +}) diff --git a/src/iso/string/chars.ts b/src/iso/string/chars.ts new file mode 100644 index 00000000..508da220 --- /dev/null +++ b/src/iso/string/chars.ts @@ -0,0 +1,56 @@ +import { $, Type, Kind, String } from '../..' + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of characters, and the output is joined into a string with spaces as + * delimiters. + * + * @param {Kind.Kind} K - The kind to convert to a string. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type T0 = String._$chars<$>>; + * type T1 = $; // "bbaarr" + */ +export type _$chars = $< + Kind.Pipe, + [String.ToList, K, String.FromList] +> + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of characters, and the output is joined into a string with spaces as + * delimiters. + * + * @param {Kind.Kind} K - The kind to convert to a string. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type T0 = $>> + * type T1 = $; // "bbaarr" + */ +export interface Chars extends Kind.Kind { + f(x: Type._$cast): _$chars +} + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of characters, and the output is joined into a string with spaces as + * delimiters. + * + * @param {Kind.Kind} K - The kind to convert to a string. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const T0 = String.chars(List.map(String.repeatBy(2))); + * const T1 = T0("bar"); // "bbaarr" + */ +export const chars = ((f: Kind._$reify string[]>>) => + (x: string) => + String.fromList(f(String.toList(x)))) as Kind._$reify From 154f207fd2745457ad7b3d74aa78e814dd03dc8f Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:13:13 -0800 Subject: [PATCH 19/27] feat: add words isomorphism --- src/iso/string/words.test.ts | 31 +++++++++++++++++++ src/iso/string/words.ts | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/iso/string/words.test.ts create mode 100644 src/iso/string/words.ts diff --git a/src/iso/string/words.test.ts b/src/iso/string/words.test.ts new file mode 100644 index 00000000..a4f92998 --- /dev/null +++ b/src/iso/string/words.test.ts @@ -0,0 +1,31 @@ +import { $, Test, Iso, String, List } from '../..' + +type Words_Spec = [ + /** + * Can reverse each word in a string. + */ + Test.Expect< + $<$>, 'hello world'>, + 'olleh dlrow' + >, + + /** + * Can capitalize each word in a string. + */ + Test.Expect< + $<$>, 'hello world'>, + 'Hello World' + > +] + +it('should reverse each word in a string', () => { + expect(Iso.String.words(List.map(String.reverse))('hello world')).toBe( + 'olleh dlrow' + ) +}) + +it('should capitalize each word in a string', () => { + expect(Iso.String.words(List.map(String.capitalize))('hello world')).toBe( + 'Hello World' + ) +}) diff --git a/src/iso/string/words.ts b/src/iso/string/words.ts new file mode 100644 index 00000000..bb1b02c1 --- /dev/null +++ b/src/iso/string/words.ts @@ -0,0 +1,58 @@ +import { $, Type, Kind, String } from '../..' + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of words, and the output is converted from a list of words back to a + * composite string. + * + * @param {Kind.Kind} K - The kind to wrap. + * + * @example + * ```ts + * import { String, Iso } from "hkt-toolbelt"; + * + * type T0 = Iso.String._$words<$>; + * type T1 = $; // 'Foo Bar Baz' + * ``` + */ +export type _$words = $< + Kind.Pipe, + [String.Words, K, String.Unwords] +> + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of words, and the output is converted from a list of words back to a + * composite string. + * + * @param {Kind.Kind} K - The kind to wrap. + * + * @example + * ```ts + * import { String, Iso } from "hkt-toolbelt"; + * + * type T0 = $>; + * type T1 = $; // 'Foo Bar Baz' + * ``` + */ +export interface Words extends Kind.Kind { + f(x: Type._$cast): _$words +} + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of words, and the output is converted from a list of words back to a + * composite string. + * + * @param {Kind.Kind} K - The kind to wrap. + * + * @example + * ```ts + * import { String, Iso } from "hkt-toolbelt"; + * + * const T0 = Iso.string.words(List.map(String.capitalize)); + * ``` + */ +export const words = ((f: Kind._$reify string[]>>) => + (x: string) => + String.unwords(f(String.words(x)))) as Kind._$reify From 9bd39b2cb4a7bf6f28558cc0e48784ca017f24c6 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:15:35 -0800 Subject: [PATCH 20/27] chore: add changelog entries for iso module --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index 5a437486..bf41f38c 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,9 @@ ## [~] +- Introduce the `Iso` module for isomorphisms. + - Add `Iso.String.Chars` to wrap the process of iterating over chars. + - Add `Iso.String.Words` to wrap the process of iterating over words. - Add `Object.DeepEntries` to get all deep paths and values in an object. - Add `Object.FromEntries` to create an object from a list of key-value pairs. - Add `Object.IsObject` to check if a value is an object. @@ -12,6 +15,7 @@ - Reify `String.First` and `String.Last` to value-level functions. - Reify `String.Init` and `String.Tail` to value-level functions. - Add `String.Unwords` to join a list of strings with a space delimiter. +- Add `String.RepeatBy` to repeat a string by a given number (argument swapped). ## [0.25.2] From 83abbd906ba85fb7bc2933c1acd1531d1af7429c Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:31:09 -0800 Subject: [PATCH 21/27] feat: add inc/dec isomorphic bijection utils --- changelog.md | 4 +- src/iso/index.ts | 6 +-- src/iso/natural-number.ts | 16 +++++++ src/iso/natural-number/decrement.test.ts | 15 ++++++ src/iso/natural-number/decrement.ts | 58 ++++++++++++++++++++++++ src/iso/natural-number/increment.test.ts | 15 ++++++ src/iso/natural-number/increment.ts | 58 ++++++++++++++++++++++++ src/iso/natural-number/index.ts | 2 + 8 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/iso/natural-number.ts create mode 100644 src/iso/natural-number/decrement.test.ts create mode 100644 src/iso/natural-number/decrement.ts create mode 100644 src/iso/natural-number/increment.test.ts create mode 100644 src/iso/natural-number/increment.ts create mode 100644 src/iso/natural-number/index.ts diff --git a/changelog.md b/changelog.md index bf41f38c..ee374f5e 100644 --- a/changelog.md +++ b/changelog.md @@ -2,9 +2,11 @@ ## [~] -- Introduce the `Iso` module for isomorphisms. +- Introduce the `Iso` module for isomorphic bijections. - Add `Iso.String.Chars` to wrap the process of iterating over chars. - Add `Iso.String.Words` to wrap the process of iterating over words. + - Add `Iso.NaturalNumber.Increment` to wrap the process of incrementing a number. + - Add `Iso.NaturalNumber.Decrement` to wrap the process of decrementing a number. - Add `Object.DeepEntries` to get all deep paths and values in an object. - Add `Object.FromEntries` to create an object from a list of key-value pairs. - Add `Object.IsObject` to check if a value is an object. diff --git a/src/iso/index.ts b/src/iso/index.ts index d139aaa8..ed9d143c 100644 --- a/src/iso/index.ts +++ b/src/iso/index.ts @@ -1,11 +1,11 @@ +export * as NaturalNumber from './natural-number' export * as String from './string' +import * as NaturalNumber from './natural-number' import * as String from './string' const _ = { - /** - * Foo - */ + NaturalNumber: NaturalNumber, String: String } diff --git a/src/iso/natural-number.ts b/src/iso/natural-number.ts new file mode 100644 index 00000000..58761fab --- /dev/null +++ b/src/iso/natural-number.ts @@ -0,0 +1,16 @@ +export * from './natural-number/' + +/** + * The `Iso.NaturalNumber` module contains various number-related isomorphisms. + * These first compute some transformation on the input, and then apply the + * inverse transformation to the output. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = $>; + * type T1 = $; // (N + 1) * 2 - 1 = 21 + * ``` + */ +declare module './natural-number' {} diff --git a/src/iso/natural-number/decrement.test.ts b/src/iso/natural-number/decrement.test.ts new file mode 100644 index 00000000..a53b7f31 --- /dev/null +++ b/src/iso/natural-number/decrement.test.ts @@ -0,0 +1,15 @@ +import { $, Iso, NaturalNumber, Test } from '../..' + +type Decrement_Spec = [ + /** + * Can wrap a kind i.e. f(n - 1) + 1, for f(x) = x * 2 + */ + Test.Expect< + $<$>, 10>, + 19 + > +] + +it('should wrap a kind i.e. f(n - 1) + 1, for f(x) = x * 2', () => { + expect(Iso.NaturalNumber.decrement(NaturalNumber.multiply(2))(10)).toBe(19) +}) diff --git a/src/iso/natural-number/decrement.ts b/src/iso/natural-number/decrement.ts new file mode 100644 index 00000000..8a7b7601 --- /dev/null +++ b/src/iso/natural-number/decrement.ts @@ -0,0 +1,58 @@ +import { $, Type, Kind, NaturalNumber } from '../..' + +/** + * Given a kind, return an isomorphism such that the input is decremented by + * one, and the output is incremented by one. + * + * @param {Kind.Kind} K - The kind to wrap with an increment/decrement. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = Iso.NaturalNumber._$decrement<$>; + * type T1 = $; // (N - 1) * 2 + 1 = 19 + * ``` + */ +export type _$decrement = $< + Kind.Pipe, + [NaturalNumber.Decrement, K, NaturalNumber.Increment] +> + +/** + * Given a kind, return an isomorphism such that the input is decremented by + * one, and the output is incremented by one. + * + * @param {Kind.Kind} K - The kind to wrap with an increment/decrement. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = $>; + * type T1 = $; // (N - 1) * 2 + 1 = 19 + * ``` + */ +export interface Decrement extends Kind.Kind { + f(x: Type._$cast): _$decrement +} + +/** + * Given a kind, return an isomorphism such that the input is decremented by + * one, and the output is incremented by one. + * + * @param {Kind.Kind} K - The kind to wrap with an increment/decrement. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * const T0 = Iso.NaturalNumber.decrement(NaturalNumber.multiply(2)); + * const T1 = T0(10); // (N - 1) * 2 + 1 = 19 + * ``` + */ +export const decrement = ((f: Kind._$reify number>>) => + (x: number) => + NaturalNumber.increment( + f(NaturalNumber.decrement(x)) + )) as Kind._$reify diff --git a/src/iso/natural-number/increment.test.ts b/src/iso/natural-number/increment.test.ts new file mode 100644 index 00000000..309b887d --- /dev/null +++ b/src/iso/natural-number/increment.test.ts @@ -0,0 +1,15 @@ +import { $, Iso, NaturalNumber, Test } from '../..' + +type Increment_Spec = [ + /** + * Can wrap a kind i.e. f(n + 1) - 1, for f(x) = x * 2 + */ + Test.Expect< + $<$>, 10>, + 21 + > +] + +it('should wrap a kind i.e. f(n + 1) - 1, for f(x) = x * 2', () => { + expect(Iso.NaturalNumber.increment(NaturalNumber.multiply(2))(10)).toBe(21) +}) diff --git a/src/iso/natural-number/increment.ts b/src/iso/natural-number/increment.ts new file mode 100644 index 00000000..4a600938 --- /dev/null +++ b/src/iso/natural-number/increment.ts @@ -0,0 +1,58 @@ +import { $, Type, Kind, NaturalNumber } from '../..' + +/** + * Given a kind, return an isomorphism such that the input is incremented by + * one, and the output is decremented by one. + * + * @param {Kind.Kind} K - The kind to wrap with an increment/decrement. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = Iso.NaturalNumber._$increment<$>; + * type T1 = $; // (N + 1) * 2 - 1 = 21 + * ``` + */ +export type _$increment = $< + Kind.Pipe, + [NaturalNumber.Increment, K, NaturalNumber.Decrement] +> + +/** + * Given a kind, return an isomorphism such that the input is incremented by + * one, and the output is decremented by one. + * + * @param {Kind.Kind} K - The kind to wrap with an increment/decrement. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = $>; + * type T1 = $; // (N + 1) * 2 - 1 = 21 + * ``` + */ +export interface Increment extends Kind.Kind { + f(x: Type._$cast): _$increment +} + +/** + * Given a kind, return an isomorphism such that the input is incremented by + * one, and the output is decremented by one. + * + * @param {Kind.Kind} K - The kind to wrap with an increment/decrement. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * const T0 = Iso.NaturalNumber.increment(NaturalNumber.multiply(2)); + * const T1 = T0(10); // (N + 1) * 2 - 1 = 21 + * ``` + */ +export const increment = ((f: Kind._$reify number>>) => + (x: number) => + NaturalNumber.decrement( + f(NaturalNumber.increment(x)) + )) as Kind._$reify diff --git a/src/iso/natural-number/index.ts b/src/iso/natural-number/index.ts new file mode 100644 index 00000000..484ce352 --- /dev/null +++ b/src/iso/natural-number/index.ts @@ -0,0 +1,2 @@ +export * from './decrement' +export * from './increment' From fc62d9ea02a060b55a1ca0a95ff39208bf00d808 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:48:42 -0800 Subject: [PATCH 22/27] feat: add undigits nat num util --- changelog.md | 1 + src/natural-number/index.ts | 1 + src/natural-number/undigits.test.ts | 21 ++++++++++ src/natural-number/undigits.ts | 60 +++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/natural-number/undigits.test.ts create mode 100644 src/natural-number/undigits.ts diff --git a/changelog.md b/changelog.md index ee374f5e..fcd90f19 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ - Reify `NaturalNumber.Divide` to a value-level function. - Reify `NaturalNumber.IsEven` to a value-level function. - Reify `NaturalNumber.IsOdd` to a value-level function. +- Add `NaturalNumber.Undigits` to convert a list of digits to a natural number. - Reify `String.First` and `String.Last` to value-level functions. - Reify `String.Init` and `String.Tail` to value-level functions. - Add `String.Unwords` to join a list of strings with a space delimiter. diff --git a/src/natural-number/index.ts b/src/natural-number/index.ts index 72c9ac96..b051a2ed 100644 --- a/src/natural-number/index.ts +++ b/src/natural-number/index.ts @@ -21,3 +21,4 @@ export * from './subtract' export * from './subtract-by' export * from './to-hex' export * from './to-list' +export * from './undigits' diff --git a/src/natural-number/undigits.test.ts b/src/natural-number/undigits.test.ts new file mode 100644 index 00000000..2fc258d0 --- /dev/null +++ b/src/natural-number/undigits.test.ts @@ -0,0 +1,21 @@ +import { $, Test, NaturalNumber } from '..' + +type Undigits_Spec = [ + /** + * Can convert a list of digits to a natural number. + */ + Test.Expect<$, 123>, + + /** + * Can convert a list of digits to a natural number. + */ + Test.Expect<$, 456> +] + +it('should convert a list of digits to a natural number', () => { + expect(NaturalNumber.undigits([1, 2, 3])).toBe(123) +}) + +it('should convert a list of digits to a natural number', () => { + expect(NaturalNumber.undigits([4, 5, 6])).toBe(456) +}) diff --git a/src/natural-number/undigits.ts b/src/natural-number/undigits.ts new file mode 100644 index 00000000..a1e8c164 --- /dev/null +++ b/src/natural-number/undigits.ts @@ -0,0 +1,60 @@ +import { Type, Digit, Kind, String } from '..' + +type _$simpleJoin< + T extends (string | number)[], + O extends string = '' +> = T extends [ + infer Head extends string | number, + ...infer Tail extends (string | number)[] +] + ? _$simpleJoin + : O + +/** + * Given a list of digits, return a natural number. + * + * @param {Digit.Digit[]} T - The list of digits. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * type T0 = NaturalNumber._$undigits<[1, 2, 3]>; // 123 + * type T1 = NaturalNumber._$undigits<[4, 5, 6]>; // 456 + * ``` + */ +export type _$undigits = + _$simpleJoin extends `${infer N extends number}` ? N : never + +/** + * Given a list of digits, return a natural number. + * + * @param {Digit.Digit[]} T - The list of digits. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * type T0 = $; // 123 + * type T1 = $; // 456 + * ``` + */ +export interface Undigits extends Kind.Kind { + f(x: Type._$cast): _$undigits +} + +/** + * Given a list of digits, return a natural number. + * + * @param {Digit.Digit[]} T - The list of digits. + * + * @example + * ```ts + * import { NaturalNumber } from "hkt-toolbelt"; + * + * const T0 = NaturalNumber.undigits([1, 2, 3]); // 123 + * const T1 = NaturalNumber.undigits([4, 5, 6]); // 456 + * ``` + */ +export const undigits = ((x: (string | number)[]) => + Number(String.fromList(x as string[]))) as Kind._$reify From 1ee76f1d9472bc5cae823a0a0252743fd21bf311 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:55:03 -0800 Subject: [PATCH 23/27] feat: add 'digits' isomorphism to iso module --- changelog.md | 1 + src/iso/natural-number/digits.test.ts | 22 +++++++++ src/iso/natural-number/digits.ts | 65 +++++++++++++++++++++++++++ src/iso/natural-number/index.ts | 1 + 4 files changed, 89 insertions(+) create mode 100644 src/iso/natural-number/digits.test.ts create mode 100644 src/iso/natural-number/digits.ts diff --git a/changelog.md b/changelog.md index fcd90f19..6d0a23e6 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ - Add `Iso.String.Words` to wrap the process of iterating over words. - Add `Iso.NaturalNumber.Increment` to wrap the process of incrementing a number. - Add `Iso.NaturalNumber.Decrement` to wrap the process of decrementing a number. + - Add `Iso.NaturalNumber.Digits` to wrap the process of converting a natural number to a list of digits. - Add `Object.DeepEntries` to get all deep paths and values in an object. - Add `Object.FromEntries` to create an object from a list of key-value pairs. - Add `Object.IsObject` to check if a value is an object. diff --git a/src/iso/natural-number/digits.test.ts b/src/iso/natural-number/digits.test.ts new file mode 100644 index 00000000..eff47e40 --- /dev/null +++ b/src/iso/natural-number/digits.test.ts @@ -0,0 +1,22 @@ +import { $, Iso, NaturalNumber, Test, List } from '../..' + +type Digits_Spec = [ + /** + * Can convert a natural number to a list of digits. + */ + Test.Expect< + $< + $>>, + 99 + >, + 1818 + > +] + +it('should convert a natural number to a list of digits', () => { + const result = Iso.NaturalNumber.digits(List.map(NaturalNumber.multiply(2)))( + 99 + ) + + expect(result).toBe(1818) +}) diff --git a/src/iso/natural-number/digits.ts b/src/iso/natural-number/digits.ts new file mode 100644 index 00000000..a026e5de --- /dev/null +++ b/src/iso/natural-number/digits.ts @@ -0,0 +1,65 @@ +import { $, Type, Kind, NaturalNumber, Number } from '../..' + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of digits, and the output is converted from a list of digits back to a + * natural number. + * + * @param {Kind.Kind} K - The kind to convert to a natural number. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = Iso.NaturalNumber._$digits<$>>; + * type T1 = $; // 1818 + * ``` + */ +export type _$digits< + K extends Kind.Kind<(x: Number.Number[]) => Number.Number[]> +> = $ + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of digits, and the output is converted from a list of digits back to a + * natural number. + * + * @param {Kind.Kind} K - The kind to convert to a natural number. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * type T0 = $>> + * type T1 = $; // 1818 + * ``` + */ +export interface Digits extends Kind.Kind { + f( + x: Type._$cast< + this[Kind._], + Kind.Kind<(x: Number.Number[]) => Number.Number[]> + > + ): _$digits +} + +/** + * Given a kind, return an isomorphism such that the input is converted to a + * list of digits, and the output is converted from a list of digits back to a + * natural number. + * + * @param {Kind.Kind} K - The kind to convert to a natural number. + * + * @example + * ```ts + * import { Iso } from "hkt-toolbelt"; + * + * const T0 = Iso.NaturalNumber.digits(List.map(NaturalNumber.multiply(2))); + * const T1 = T0(99); // 1818 + * ``` + */ +export const digits = (( + f: Kind._$reify number[]>> + ) => + (x: number) => + NaturalNumber.undigits(f(NaturalNumber.digits(x)))) as Kind._$reify diff --git a/src/iso/natural-number/index.ts b/src/iso/natural-number/index.ts index 484ce352..0d185e10 100644 --- a/src/iso/natural-number/index.ts +++ b/src/iso/natural-number/index.ts @@ -1,2 +1,3 @@ export * from './decrement' +export * from './digits' export * from './increment' From 2bdf02d0fcba55b1284882d4a2d59095c21ead01 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:58:38 -0800 Subject: [PATCH 24/27] feat: add changelog for 0.26.0 --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 6d0a23e6..36f3e7d6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Changelog -## [~] +## [0.26.0] - Introduce the `Iso` module for isomorphic bijections. - Add `Iso.String.Chars` to wrap the process of iterating over chars. From 123fcb0c1c1d55e904e350e165a36effaee7ea0e Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 3 Jan 2025 10:59:21 -0800 Subject: [PATCH 25/27] chore: publish 0.26.0 --- pkg/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/package.json b/pkg/package.json index 16e6c99c..f019e999 100644 --- a/pkg/package.json +++ b/pkg/package.json @@ -1,6 +1,6 @@ { "name": "hkt-toolbelt", - "version": "0.25.2", + "version": "0.26.0", "description": "Functional and composable type utilities", "types": "./index.d.ts", "main": "./index.js", From bb45e04e99dc64bf5845aa5875f736d8ad44e6b5 Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 4 Jan 2025 13:29:40 -0800 Subject: [PATCH 26/27] chore: fix lint errors --- src/natural-number/undigits.ts | 2 +- src/object/deep-entries.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/natural-number/undigits.ts b/src/natural-number/undigits.ts index a1e8c164..55cf8b9f 100644 --- a/src/natural-number/undigits.ts +++ b/src/natural-number/undigits.ts @@ -1,4 +1,4 @@ -import { Type, Digit, Kind, String } from '..' +import { Type, Kind, String } from '..' type _$simpleJoin< T extends (string | number)[], diff --git a/src/object/deep-entries.ts b/src/object/deep-entries.ts index 9edc199c..603cb53b 100644 --- a/src/object/deep-entries.ts +++ b/src/object/deep-entries.ts @@ -85,7 +85,7 @@ export const deepEntries = (( // If value is an object (non-null) and not an array, recurse if (value !== null && typeof value === 'object' && !Array.isArray(value)) { - const nested = (deepEntries as Function)(value) + const nested = (deepEntries as Function)(value) as unknown[][] // Prepend the current key to each nested path for (const entry of nested) { results.push([key, ...entry]) From 203a337bfda3daf7a52bb0b3dbda1aa804ba943c Mon Sep 17 00:00:00 2001 From: poteat Date: Sat, 4 Jan 2025 13:34:22 -0800 Subject: [PATCH 27/27] fix: test case for deep entries util --- src/object/deep-entries.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/object/deep-entries.test.ts b/src/object/deep-entries.test.ts index cdeb9f39..f583cbde 100644 --- a/src/object/deep-entries.test.ts +++ b/src/object/deep-entries.test.ts @@ -29,8 +29,8 @@ type DeepEntries_Spec = [ * Mixed nesting (arrays are treated as values) */ Test.Expect< - $, - [['x', ['y', 'z']], ['w', 'v', 1]] + $[number], + [['x', ['y', 'z']], ['w', 'v', 1]][number] > ]