diff --git a/changelog.md b/changelog.md index de012b77e..36f3e7d67 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,26 @@ # Changelog +## [0.26.0] + +- 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 `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. +- 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. +- 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. +- Add `String.RepeatBy` to repeat a string by a given number (argument swapped). + ## [0.25.2] - Fix `List.Sort` to work with 2-ary comparators. diff --git a/pkg/package.json b/pkg/package.json index 16e6c99c0..f019e9996 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", diff --git a/src/index.ts b/src/index.ts index 64a3a09ac..70ef31bf1 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 000000000..b65bd8249 --- /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 000000000..ed9d143c2 --- /dev/null +++ b/src/iso/index.ts @@ -0,0 +1,14 @@ +export * as NaturalNumber from './natural-number' +export * as String from './string' + +import * as NaturalNumber from './natural-number' +import * as String from './string' + +const _ = { + NaturalNumber: NaturalNumber, + String: String +} + +type _ = typeof _ + +export default _ diff --git a/src/iso/natural-number.ts b/src/iso/natural-number.ts new file mode 100644 index 000000000..58761fab1 --- /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 000000000..a53b7f31f --- /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 000000000..8a7b7601c --- /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/digits.test.ts b/src/iso/natural-number/digits.test.ts new file mode 100644 index 000000000..eff47e407 --- /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 000000000..a026e5de3 --- /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/increment.test.ts b/src/iso/natural-number/increment.test.ts new file mode 100644 index 000000000..309b887d6 --- /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 000000000..4a6009388 --- /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 000000000..0d185e109 --- /dev/null +++ b/src/iso/natural-number/index.ts @@ -0,0 +1,3 @@ +export * from './decrement' +export * from './digits' +export * from './increment' diff --git a/src/iso/string.ts b/src/iso/string.ts new file mode 100644 index 000000000..34e757887 --- /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/chars.test.ts b/src/iso/string/chars.test.ts new file mode 100644 index 000000000..af230fc54 --- /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 000000000..508da2208 --- /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 diff --git a/src/iso/string/index.ts b/src/iso/string/index.ts new file mode 100644 index 000000000..ab4864c07 --- /dev/null +++ b/src/iso/string/index.ts @@ -0,0 +1,2 @@ +export * from './chars' +export * from './words' diff --git a/src/iso/string/words.test.ts b/src/iso/string/words.test.ts new file mode 100644 index 000000000..a4f929986 --- /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 000000000..bb1b02c13 --- /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 diff --git a/src/natural-number/compare.ts b/src/natural-number/compare.ts index 1b354d446..e8f75bf66 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 f9b552c4f..10a9a0110 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-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 f8a116c78..6b4e255b8 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 208de0e3e..e6e307b3d 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 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 9bde66e88..b2ed91cb0 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 ab4699724..e69a6df7f 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 { +export 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 diff --git a/src/natural-number/index.ts b/src/natural-number/index.ts index 72c9ac962..b051a2ed8 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/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 75df3b06e..1ae28c6ed 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 65b9e680d..d2da09ccc 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 diff --git a/src/natural-number/is-greater-than-or-equal.ts b/src/natural-number/is-greater-than-or-equal.ts index 1f987e18b..b201510bc 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 4de4f2e7a..49cee4a27 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 c70868c33..351bc338b 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 68748373a..9f558e9bc 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 d7f6f79a3..e968e811f 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 5c80e9a8f..ec28a7535 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 98fa1fc88..33f45778e 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 f82488682..8afcc72ce 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 2461b657c..e8a4d2660 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 4650aac40..2dad53575 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 d91176448..b9d50389d 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 diff --git a/src/natural-number/undigits.test.ts b/src/natural-number/undigits.test.ts new file mode 100644 index 000000000..2fc258d0c --- /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 000000000..55cf8b9f1 --- /dev/null +++ b/src/natural-number/undigits.ts @@ -0,0 +1,60 @@ +import { Type, 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 diff --git a/src/object/deep-entries.test.ts b/src/object/deep-entries.test.ts new file mode 100644 index 000000000..f583cbde1 --- /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< + $[number], + [['x', ['y', 'z']], ['w', 'v', 1]][number] + > +] + +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 000000000..603cb53bf --- /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) as unknown[][] + // 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/from-entries.test.ts b/src/object/from-entries.test.ts new file mode 100644 index 000000000..81d87ec8a --- /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 000000000..1a82f54ed --- /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 diff --git a/src/object/index.ts b/src/object/index.ts index 6535004ae..2580931b6 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' diff --git a/src/object/is-object.test.ts b/src/object/is-object.test.ts new file mode 100644 index 000000000..98d4a6330 --- /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 000000000..2eaa11676 --- /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 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 5f02dd3c7..8a74beb02 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 8dd9bded6..ffa47a0a9 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/index.ts b/src/string/index.ts index ef03f0f9f..4b026c333 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' @@ -37,4 +38,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/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 ece14221d..c8573c506 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 4d481b4f5..16d262b16 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/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 9e669a1f3..eefbacca1 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 72fe8420e..ab54ecbb8 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 diff --git a/src/string/repeat-by.test.ts b/src/string/repeat-by.test.ts new file mode 100644 index 000000000..b6a8c680f --- /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 000000000..dc1380332 --- /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 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 e293e4caf..e922e4245 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 0c09d8539..1c0a606b8 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 diff --git a/src/string/to-upper.test.ts b/src/string/to-upper.test.ts index c9f17459d..884fbb3e0 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('') +}) diff --git a/src/string/unwords.test.ts b/src/string/unwords.test.ts new file mode 100644 index 000000000..fccaa2934 --- /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 000000000..47dff0d11 --- /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