From bf9345ddb8a7232ece238bd95d1f22ff1631a803 Mon Sep 17 00:00:00 2001 From: Landon Yarrington <33426811+jly36963@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:07:26 -0600 Subject: [PATCH] refactor: split files (#56) * refactor: split files * Updates * Updates * split object files * split type tests * Update tsconfig.json Co-authored-by: Guga Guichard * Updates * Updates --------- Co-authored-by: Guga Guichard --- src/casing.test.ts | 198 ------- src/casing.ts | 197 ------- src/casing.types.test.ts | 56 -- src/chars.ts | 45 -- src/chars.types.test.ts | 32 -- src/deep-key-casing.test.ts | 120 ----- src/deep-key-casing.ts | 193 ------- src/deep-key-casing.types.test.ts | 63 --- src/index.ts | 209 ++++---- src/internal/fixtures.ts | 7 + src/{ => internal}/internals.test.ts | 21 + src/{ => internal}/internals.ts | 3 +- src/{ => internal}/math.ts | 4 +- src/{ => internal}/types.d.ts | 0 src/internals.types.test.ts | 23 - src/key-casing.test.ts | 106 ---- src/key-casing.ts | 163 ------ src/key-casing.types.test.ts | 63 --- src/native/char-at.test.ts | 14 + src/native/char-at.ts | 21 + src/native/concat.test.ts | 16 + src/native/concat.ts | 18 + src/native/ends-with.test.ts | 53 ++ src/native/ends-with.ts | 37 ++ src/native/includes.test.ts | 53 ++ src/native/includes.ts | 36 ++ src/native/join.test.ts | 22 + src/native/join.ts | 32 ++ src/native/length.test.ts | 14 + src/native/length.ts | 15 + src/native/pad-end.test.ts | 38 ++ src/native/pad-end.ts | 35 ++ src/native/pad-start.test.ts | 38 ++ src/native/pad-start.ts | 35 ++ src/native/repeat.test.ts | 24 + src/native/repeat.ts | 27 + src/native/replace-all.test.ts | 57 ++ src/native/replace-all.ts | 37 ++ src/native/replace.test.ts | 22 + src/native/replace.ts | 30 ++ src/native/slice.test.ts | 50 ++ src/native/slice.ts | 43 ++ src/native/split.test.ts | 27 + src/native/split.ts | 26 + src/native/starts-with.test.ts | 48 ++ src/native/starts-with.ts | 36 ++ src/native/to-lower-case.test.ts | 18 + src/native/to-lower-case.ts | 9 + src/native/to-upper-case.test.ts | 18 + src/native/to-upper-case.ts | 9 + src/native/trim-end.test.ts | 14 + src/native/trim-end.ts | 16 + src/native/trim-start.test.ts | 16 + src/native/trim-start.ts | 16 + src/native/trim.test.ts | 14 + src/native/trim.ts | 18 + src/primitives.test.ts | 426 --------------- src/primitives.ts | 497 ------------------ src/primitives.types.test.ts | 43 -- src/separators.test.ts | 10 - src/separators.types.test.ts | 15 - src/utils.test.ts | 101 ---- src/utils.types.test.ts | 39 -- src/utils/capitalize.test.ts | 18 + src/utils/capitalize.ts | 14 + src/utils/characters/letters.test.ts | 20 + src/utils/characters/letters.ts | 23 + src/utils/characters/numbers.test.ts | 10 + src/utils/characters/numbers.ts | 6 + src/utils/characters/separators.test.ts | 23 + src/{ => utils/characters}/separators.ts | 9 +- src/utils/characters/special.test.ts | 12 + src/utils/characters/special.ts | 15 + src/utils/object-keys/camel-keys.test.ts | 26 + src/utils/object-keys/camel-keys.ts | 19 + src/utils/object-keys/constant-keys.test.ts | 35 ++ src/utils/object-keys/constant-keys.ts | 22 + src/utils/object-keys/deep-camel-keys.test.ts | 40 ++ src/utils/object-keys/deep-camel-keys.ts | 23 + .../object-keys/deep-constant-keys.test.ts | 38 ++ src/utils/object-keys/deep-constant-keys.ts | 26 + .../object-keys/deep-delimiter-keys.test.ts | 35 ++ src/utils/object-keys/deep-delimiter-keys.ts | 36 ++ src/utils/object-keys/deep-kebab-keys.test.ts | 26 + src/utils/object-keys/deep-kebab-keys.ts | 23 + .../object-keys/deep-pascal-keys.test.ts | 26 + src/utils/object-keys/deep-pascal-keys.ts | 23 + src/utils/object-keys/deep-snake-keys.test.ts | 26 + src/utils/object-keys/deep-snake-keys.ts | 23 + .../object-keys/deep-transform-keys.test.ts | 18 + src/utils/object-keys/deep-transform-keys.ts | 26 + src/utils/object-keys/delimiter-keys.test.ts | 32 ++ src/utils/object-keys/delimiter-keys.ts | 27 + src/utils/object-keys/kebab-keys.test.ts | 26 + src/utils/object-keys/kebab-keys.ts | 21 + src/utils/object-keys/pascal-keys.test.ts | 26 + src/utils/object-keys/pascal-keys.ts | 19 + src/utils/object-keys/snake-keys.test.ts | 26 + src/utils/object-keys/snake-keys.ts | 19 + src/utils/object-keys/transform-keys.test.ts | 18 + src/utils/object-keys/transform-keys.ts | 20 + src/utils/truncate.test.ts | 47 ++ src/utils/truncate.ts | 39 ++ src/utils/uncapitalize.test.ts | 18 + src/utils/uncapitalize.ts | 14 + src/utils/word-case/lower-case.test.ts | 19 + src/utils/word-case/lower-case.ts | 14 + src/utils/word-case/to-camel-case.test.ts | 30 ++ src/utils/word-case/to-camel-case.ts | 17 + src/utils/word-case/to-constant-case.test.ts | 34 ++ src/utils/word-case/to-constant-case.ts | 16 + src/utils/word-case/to-delimiter-case.test.ts | 32 ++ src/utils/word-case/to-delimiter-case.ts | 25 + src/utils/word-case/to-kebab-case.test.ts | 32 ++ src/utils/word-case/to-kebab-case.ts | 16 + src/utils/word-case/to-pascal-case.test.ts | 30 ++ src/utils/word-case/to-pascal-case.ts | 19 + src/utils/word-case/to-snake-case.test.ts | 32 ++ src/utils/word-case/to-snake-case.ts | 16 + src/utils/word-case/to-title-case.test.ts | 32 ++ src/utils/word-case/to-title-case.ts | 16 + src/utils/word-case/upper-case.test.ts | 18 + src/utils/word-case/upper-case.ts | 14 + src/utils/words.test.ts | 86 +++ src/{utils.ts => utils/words.ts} | 53 +- tsconfig.json | 5 +- 126 files changed, 2674 insertions(+), 2561 deletions(-) delete mode 100644 src/casing.test.ts delete mode 100644 src/casing.ts delete mode 100644 src/casing.types.test.ts delete mode 100644 src/chars.ts delete mode 100644 src/chars.types.test.ts delete mode 100644 src/deep-key-casing.test.ts delete mode 100644 src/deep-key-casing.ts delete mode 100644 src/deep-key-casing.types.test.ts create mode 100644 src/internal/fixtures.ts rename src/{ => internal}/internals.test.ts (56%) rename src/{ => internal}/internals.ts (93%) rename src/{ => internal}/math.ts (84%) rename src/{ => internal}/types.d.ts (100%) delete mode 100644 src/internals.types.test.ts delete mode 100644 src/key-casing.test.ts delete mode 100644 src/key-casing.ts delete mode 100644 src/key-casing.types.test.ts create mode 100644 src/native/char-at.test.ts create mode 100644 src/native/char-at.ts create mode 100644 src/native/concat.test.ts create mode 100644 src/native/concat.ts create mode 100644 src/native/ends-with.test.ts create mode 100644 src/native/ends-with.ts create mode 100644 src/native/includes.test.ts create mode 100644 src/native/includes.ts create mode 100644 src/native/join.test.ts create mode 100644 src/native/join.ts create mode 100644 src/native/length.test.ts create mode 100644 src/native/length.ts create mode 100644 src/native/pad-end.test.ts create mode 100644 src/native/pad-end.ts create mode 100644 src/native/pad-start.test.ts create mode 100644 src/native/pad-start.ts create mode 100644 src/native/repeat.test.ts create mode 100644 src/native/repeat.ts create mode 100644 src/native/replace-all.test.ts create mode 100644 src/native/replace-all.ts create mode 100644 src/native/replace.test.ts create mode 100644 src/native/replace.ts create mode 100644 src/native/slice.test.ts create mode 100644 src/native/slice.ts create mode 100644 src/native/split.test.ts create mode 100644 src/native/split.ts create mode 100644 src/native/starts-with.test.ts create mode 100644 src/native/starts-with.ts create mode 100644 src/native/to-lower-case.test.ts create mode 100644 src/native/to-lower-case.ts create mode 100644 src/native/to-upper-case.test.ts create mode 100644 src/native/to-upper-case.ts create mode 100644 src/native/trim-end.test.ts create mode 100644 src/native/trim-end.ts create mode 100644 src/native/trim-start.test.ts create mode 100644 src/native/trim-start.ts create mode 100644 src/native/trim.test.ts create mode 100644 src/native/trim.ts delete mode 100644 src/primitives.test.ts delete mode 100644 src/primitives.ts delete mode 100644 src/primitives.types.test.ts delete mode 100644 src/separators.test.ts delete mode 100644 src/separators.types.test.ts delete mode 100644 src/utils.test.ts delete mode 100644 src/utils.types.test.ts create mode 100644 src/utils/capitalize.test.ts create mode 100644 src/utils/capitalize.ts create mode 100644 src/utils/characters/letters.test.ts create mode 100644 src/utils/characters/letters.ts create mode 100644 src/utils/characters/numbers.test.ts create mode 100644 src/utils/characters/numbers.ts create mode 100644 src/utils/characters/separators.test.ts rename src/{ => utils/characters}/separators.ts (71%) create mode 100644 src/utils/characters/special.test.ts create mode 100644 src/utils/characters/special.ts create mode 100644 src/utils/object-keys/camel-keys.test.ts create mode 100644 src/utils/object-keys/camel-keys.ts create mode 100644 src/utils/object-keys/constant-keys.test.ts create mode 100644 src/utils/object-keys/constant-keys.ts create mode 100644 src/utils/object-keys/deep-camel-keys.test.ts create mode 100644 src/utils/object-keys/deep-camel-keys.ts create mode 100644 src/utils/object-keys/deep-constant-keys.test.ts create mode 100644 src/utils/object-keys/deep-constant-keys.ts create mode 100644 src/utils/object-keys/deep-delimiter-keys.test.ts create mode 100644 src/utils/object-keys/deep-delimiter-keys.ts create mode 100644 src/utils/object-keys/deep-kebab-keys.test.ts create mode 100644 src/utils/object-keys/deep-kebab-keys.ts create mode 100644 src/utils/object-keys/deep-pascal-keys.test.ts create mode 100644 src/utils/object-keys/deep-pascal-keys.ts create mode 100644 src/utils/object-keys/deep-snake-keys.test.ts create mode 100644 src/utils/object-keys/deep-snake-keys.ts create mode 100644 src/utils/object-keys/deep-transform-keys.test.ts create mode 100644 src/utils/object-keys/deep-transform-keys.ts create mode 100644 src/utils/object-keys/delimiter-keys.test.ts create mode 100644 src/utils/object-keys/delimiter-keys.ts create mode 100644 src/utils/object-keys/kebab-keys.test.ts create mode 100644 src/utils/object-keys/kebab-keys.ts create mode 100644 src/utils/object-keys/pascal-keys.test.ts create mode 100644 src/utils/object-keys/pascal-keys.ts create mode 100644 src/utils/object-keys/snake-keys.test.ts create mode 100644 src/utils/object-keys/snake-keys.ts create mode 100644 src/utils/object-keys/transform-keys.test.ts create mode 100644 src/utils/object-keys/transform-keys.ts create mode 100644 src/utils/truncate.test.ts create mode 100644 src/utils/truncate.ts create mode 100644 src/utils/uncapitalize.test.ts create mode 100644 src/utils/uncapitalize.ts create mode 100644 src/utils/word-case/lower-case.test.ts create mode 100644 src/utils/word-case/lower-case.ts create mode 100644 src/utils/word-case/to-camel-case.test.ts create mode 100644 src/utils/word-case/to-camel-case.ts create mode 100644 src/utils/word-case/to-constant-case.test.ts create mode 100644 src/utils/word-case/to-constant-case.ts create mode 100644 src/utils/word-case/to-delimiter-case.test.ts create mode 100644 src/utils/word-case/to-delimiter-case.ts create mode 100644 src/utils/word-case/to-kebab-case.test.ts create mode 100644 src/utils/word-case/to-kebab-case.ts create mode 100644 src/utils/word-case/to-pascal-case.test.ts create mode 100644 src/utils/word-case/to-pascal-case.ts create mode 100644 src/utils/word-case/to-snake-case.test.ts create mode 100644 src/utils/word-case/to-snake-case.ts create mode 100644 src/utils/word-case/to-title-case.test.ts create mode 100644 src/utils/word-case/to-title-case.ts create mode 100644 src/utils/word-case/upper-case.test.ts create mode 100644 src/utils/word-case/upper-case.ts create mode 100644 src/utils/words.test.ts rename src/{utils.ts => utils/words.ts} (61%) diff --git a/src/casing.test.ts b/src/casing.test.ts deleted file mode 100644 index fd48077..0000000 --- a/src/casing.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import * as subject from './casing' - -export const weirdString = - ' someWeird-cased$*String1986Foo [Bar] W_FOR_WUMBO...' as const - -describe('capitalize', () => { - test('it does nothing with a string that has no char at the beginning', () => { - const expected = weirdString - const result = subject.capitalize(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('it capitalizes the first char of a string', () => { - const expected = 'SomeWeird-casedString' as const - const result = subject.capitalize('someWeird-casedString') - expect(result).toEqual(expected) - type test = Expect> - }) -}) - -describe('uncapitalize', () => { - test('it does nothing with a string that has no char at the beginning', () => { - const expected = weirdString - const result = subject.uncapitalize(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('it uncapitalizes the first char of a string', () => { - const expected = 'someWeird-casedString' as const - const result = subject.uncapitalize('SomeWeird-casedString') - expect(result).toEqual(expected) - type test = Expect> - }) -}) - -describe('casing functions', () => { - test('lowerCase', () => { - const expected = - 'some weird cased $* string 1986 foo bar w for wumbo' as const - const result = subject.lowerCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toUpperCase', () => { - const expected = - ' SOMEWEIRD-CASED$*STRING1986FOO [BAR] W_FOR_WUMBO...' as const - const result = subject.toUpperCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toLowerCase', () => { - const expected = - ' someweird-cased$*string1986foo [bar] w_for_wumbo...' as const - const result = subject.toLowerCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toDelimiterCase', () => { - const expected = - 'some@Weird@cased@$*@String@1986@Foo@Bar@W@FOR@WUMBO' as const - const result = subject.toDelimiterCase(weirdString, '@') - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toCamelCase', () => { - const expected = 'someWeirdCased$*String1986FooBarWForWumbo' as const - const result = subject.toCamelCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toPascalCase', () => { - const expected = 'SomeWeirdCased$*String1986FooBarWForWumbo' as const - const result = subject.toPascalCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toKebabCase', () => { - const expected = - 'some-weird-cased-$*-string-1986-foo-bar-w-for-wumbo' as const - const result = subject.toKebabCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toSnakeCase', () => { - const expected = - 'some_weird_cased_$*_string_1986_foo_bar_w_for_wumbo' as const - const result = subject.toSnakeCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toConstantCase', () => { - const expected = - 'SOME_WEIRD_CASED_$*_STRING_1986_FOO_BAR_W_FOR_WUMBO' as const - const result = subject.toConstantCase( - ' someWeird-cased$*String1986Foo Bar W_FOR_WUMBO', - ) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('toTitleCase', () => { - const expected = - 'Some Weird Cased $* String 1986 Foo Bar W For Wumbo' as const - const result = subject.toTitleCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('upperCase', () => { - const expected = - 'SOME WEIRD CASED $* STRING 1986 FOO BAR W FOR WUMBO' as const - const result = subject.upperCase(weirdString) - expect(result).toEqual(expected) - type test = Expect> - }) - - describe('with various separators', () => { - const text = - '[one] two-three/four.five(six){seven}|eight_nine\\ten' as const - - test('lowerCase', () => { - const result = subject.lowerCase(text) - const expected = 'one two three four five six seven eight nine ten' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toUpperCase', () => { - const result = subject.toUpperCase(text) - const expected = '[ONE] TWO-THREE/FOUR.FIVE(SIX){SEVEN}|EIGHT_NINE\\TEN' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toLowerCase', () => { - const result = subject.toLowerCase(text) - const expected = '[one] two-three/four.five(six){seven}|eight_nine\\ten' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toDelimiterCase', () => { - const result = subject.toDelimiterCase(text, '.') - const expected = 'one.two.three.four.five.six.seven.eight.nine.ten' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toCamelCase', () => { - const result = subject.toCamelCase(text) - const expected = 'oneTwoThreeFourFiveSixSevenEightNineTen' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toPascalCase', () => { - const result = subject.toPascalCase(text) - const expected = 'OneTwoThreeFourFiveSixSevenEightNineTen' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toKebabCase', () => { - const result = subject.toKebabCase(text) - const expected = 'one-two-three-four-five-six-seven-eight-nine-ten' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toSnakeCase', () => { - const result = subject.toSnakeCase(text) - const expected = 'one_two_three_four_five_six_seven_eight_nine_ten' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toConstantCase', () => { - const result = subject.toConstantCase(text) - const expected = 'ONE_TWO_THREE_FOUR_FIVE_SIX_SEVEN_EIGHT_NINE_TEN' - expect(result).toEqual(expected) - type test = Expect> - }) - test('toTitleCase', () => { - const result = subject.toTitleCase(text) - const expected = 'One Two Three Four Five Six Seven Eight Nine Ten' - expect(result).toEqual(expected) - type test = Expect> - }) - test('upperCase', () => { - const result = subject.upperCase(text) - const expected = 'ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE TEN' - expect(result).toEqual(expected) - type test = Expect> - }) - }) -}) diff --git a/src/casing.ts b/src/casing.ts deleted file mode 100644 index 98eb839..0000000 --- a/src/casing.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { pascalCaseAll, type PascalCaseAll } from './internals' -import type { Join } from './primitives' -import { charAt, join, slice } from './primitives' -import type { Words } from './utils' -import { words } from './utils' - -// CASING UTILITIES THAT ALREADY HAVE NATIVE TS TYPES - -/** - * Capitalizes the first letter of a string. This is a runtime counterpart of `Capitalize` from `src/types.d.ts`. - * @param str the string to capitalize. - * @returns the capitalized string. - * @example capitalize('hello world') // 'Hello world' - */ -function capitalize(str: T): Capitalize { - return join([toUpperCase(charAt(str, 0)), slice(str, 1)]) -} - -/** - * This function is a strongly-typed counterpart of String.prototype.toLowerCase. - * @param str the string to make lowercase. - * @returns the lowercased string. - * @example toLowerCase('HELLO WORLD') // 'hello world' - */ -function toLowerCase(str: T) { - return str.toLowerCase() as Lowercase -} - -/** - * This function is a strongly-typed counterpart of String.prototype.toUpperCase. - * @param str the string to make uppercase. - * @returns the uppercased string. - * @example toUpperCase('hello world') // 'HELLO WORLD' - */ -function toUpperCase(str: T) { - return str.toUpperCase() as Uppercase -} - -/** - * Uncapitalizes the first letter of a string. This is a runtime counterpart of `Uncapitalize` from `src/types.d.ts`. - * @param str the string to uncapitalize. - * @returns the uncapitalized string. - * @example uncapitalize('Hello world') // 'hello world' - */ -function uncapitalize(str: T): Uncapitalize { - return join([toLowerCase(charAt(str, 0)), slice(str, 1)]) -} - -// CASING UTILITIES - -/** - * Transforms a string with the specified separator (delimiter). - */ -type DelimiterCase = Join, D> -/** - * A function that transforms a string by splitting it into words and joining them with the specified delimiter. - * @param str the string to transform. - * @param delimiter the delimiter to use. - * @returns the transformed string. - * @example toDelimiterCase('hello world', '.') // 'hello.world' - */ -function toDelimiterCase( - str: T, - delimiter: D, -): DelimiterCase { - return join(words(str), delimiter) -} - -/** - * Transforms a string to camelCase. - */ -type CamelCase = Uncapitalize> - -/** - * A strongly typed version of `toCamelCase` that works in both runtime and type level. - * @param str the string to convert to camel case. - * @returns the camel cased string. - * @example toCamelCase('hello world') // 'helloWorld' - */ -function toCamelCase(str: T): CamelCase { - return uncapitalize(toPascalCase(str)) -} - -/** - * A strongly-typed version of `lowerCase` that works in both runtime and type level. - * @param str the string to convert to lower case. - * @returns the lowercased string. - * @example lowerCase('HELLO-WORLD') // 'hello world' - */ -function lowerCase(str: T): Lowercase> { - return toLowerCase(toDelimiterCase(str, ' ')) -} - -/** - * A strongly-typed version of `upperCase` that works in both runtime and type level. - * @param str the string to convert to upper case. - * @returns the uppercased string. - * @example upperCase('hello-world') // 'HELLO WORLD' - */ -function upperCase(str: T): Uppercase> { - return toUpperCase(toDelimiterCase(str, ' ')) -} - -/** - * Transforms a string to PascalCase. - */ -type PascalCase = Join>> -/** - * A strongly typed version of `toPascalCase` that works in both runtime and type level. - * @param str the string to convert to pascal case. - * @returns the pascal cased string. - * @example toPascalCase('hello world') // 'HelloWorld' - */ -function toPascalCase(str: T): PascalCase { - return join(pascalCaseAll(words(str))) -} - -/** - * Transforms a string to kebab-case. - */ -type KebabCase = Lowercase> -/** - * A strongly typed version of `toKebabCase` that works in both runtime and type level. - * @param str the string to convert to kebab case. - * @returns the kebab cased string. - * @example toKebabCase('hello world') // 'hello-world' - */ -function toKebabCase(str: T): KebabCase { - return toLowerCase(toDelimiterCase(str, '-')) -} - -/** - * Transforms a string to snake_case. - */ -type SnakeCase = Lowercase> -/** - * A strongly typed version of `toSnakeCase` that works in both runtime and type level. - * @param str the string to convert to snake case. - * @returns the snake cased string. - * @example toSnakeCase('hello world') // 'hello_world' - */ -function toSnakeCase(str: T): SnakeCase { - return toLowerCase(toDelimiterCase(str, '_')) -} - -/** - * Transforms a string to CONSTANT_CASE. - */ -type ConstantCase = Uppercase> -/** - * A strongly typed version of `toConstantCase` that works in both runtime and type level. - * @param str the string to convert to constant case. - * @returns the constant cased string. - * @example toConstantCase('hello world') // 'HELLO_WORLD' - */ -function toConstantCase(str: T): ConstantCase { - return toUpperCase(toDelimiterCase(str, '_')) -} - -/** - * Transforms a string to "Title Case". - */ -type TitleCase = DelimiterCase, ' '> -/** - * A strongly typed version of `toTitleCase` that works in both runtime and type level. - * @param str the string to convert to title case. - * @returns the title cased string. - * @example toTitleCase('hello world') // 'Hello World' - */ -function toTitleCase(str: T): TitleCase { - return toDelimiterCase(toPascalCase(str), ' ') -} - -export type { - CamelCase, - ConstantCase, - DelimiterCase, - KebabCase, - PascalCase, - SnakeCase, - TitleCase, -} -export { - capitalize, - lowerCase, - toCamelCase, - toConstantCase, - toDelimiterCase, - toKebabCase, - toLowerCase, - toPascalCase, - toSnakeCase, - toTitleCase, - toUpperCase, - uncapitalize, - upperCase, -} diff --git a/src/casing.types.test.ts b/src/casing.types.test.ts deleted file mode 100644 index e3b219b..0000000 --- a/src/casing.types.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type * as Subject from './casing' -import type { weirdString } from './casing.test' - -type WeirdString = typeof weirdString | 'dont.distribute unions' - -namespace TypeTransforms { - type test = Expect< - Equal< - Subject.DelimiterCase, - | 'some%Weird%cased%$*%String%1986%Foo%Bar%W%FOR%WUMBO' - | 'dont%distribute%unions' - > - > - type test1 = Expect< - Equal< - Subject.CamelCase, - 'someWeirdCased$*String1986FooBarWForWumbo' | 'dontDistributeUnions' - > - > - type test2 = Expect< - Equal< - Subject.PascalCase, - 'SomeWeirdCased$*String1986FooBarWForWumbo' | 'DontDistributeUnions' - > - > - type test3 = Expect< - Equal< - Subject.KebabCase, - | 'some-weird-cased-$*-string-1986-foo-bar-w-for-wumbo' - | 'dont-distribute-unions' - > - > - type test4 = Expect< - Equal< - Subject.SnakeCase, - | 'some_weird_cased_$*_string_1986_foo_bar_w_for_wumbo' - | 'dont_distribute_unions' - > - > - type test5 = Expect< - Equal< - Subject.ConstantCase, - | 'SOME_WEIRD_CASED_$*_STRING_1986_FOO_BAR_W_FOR_WUMBO' - | 'DONT_DISTRIBUTE_UNIONS' - > - > - type test6 = Expect< - Equal< - Subject.TitleCase, - | 'Some Weird Cased $* String 1986 Foo Bar W For Wumbo' - | 'Dont Distribute Unions' - > - > -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/chars.ts b/src/chars.ts deleted file mode 100644 index 1e8de0e..0000000 --- a/src/chars.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { IsSeparator } from './separators' - -type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' -// prettier-ignore -type UpperChars = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' -type LowerChars = Lowercase - -// UTILITIES FOR DETECTING CHARS -/** - * Checks if the given character is an upper case letter. - */ -type IsUpper = T extends UpperChars ? true : false - -/** - * Checks if the given character is a letter. - */ -type IsLetter = IsUpper extends true - ? true - : IsLower extends true - ? true - : false - -/** - * Checks if the given character is a lower case letter. - */ -type IsLower = T extends LowerChars ? true : false - -/** - * Checks if the given character is a number. - */ -type IsDigit = T extends Digit ? true : false - -/** - * Checks if the given character is a special character. - * E.g. not a letter, number, or separator. - */ -type IsSpecial = IsLetter extends true - ? false - : IsDigit extends true - ? false - : IsSeparator extends true - ? false - : true - -export type { Digit, IsDigit, IsLetter, IsLower, IsSpecial, IsUpper } diff --git a/src/chars.types.test.ts b/src/chars.types.test.ts deleted file mode 100644 index e0b5055..0000000 --- a/src/chars.types.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type * as Subject from './chars' - -namespace TypeChecks { - type test1 = Expect, true>> - type test2 = Expect, false>> - type test3 = Expect, false>> - type test4 = Expect, false>> - - type test5 = Expect, false>> - type test6 = Expect, true>> - type test7 = Expect, false>> - type test8 = Expect, false>> - - type test9 = Expect, false>> - type test10 = Expect, false>> - type test11 = Expect, true>> - type test12 = Expect, false>> - - type test13 = Expect, false>> - type test14 = Expect, true>> - type test15 = Expect, true>> - type test16 = Expect, false>> - - type test26 = Expect, false>> - type test27 = Expect, false>> - type test28 = Expect, false>> - type test29 = Expect, true>> - type test30 = Expect, false>> - type test31 = Expect, true>> - type test32 = Expect, false>> -} -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/deep-key-casing.test.ts b/src/deep-key-casing.test.ts deleted file mode 100644 index a8ae3b9..0000000 --- a/src/deep-key-casing.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as subject from './deep-key-casing' - -describe('deepTransformKeys', () => { - test('should deeply transform the keys of an object', () => { - const expected = { - SOME: { 'DEEP-NESTED': { VALUE: true } }, - 'OTHER-VALUE': true, - } - const result = subject.deepTransformKeys( - { - some: { 'deep-nested': { value: true } }, - 'other-value': true, - }, - (key) => key.toUpperCase(), - ) - expect(result).toEqual(expected) - }) - - test('should handle null properly', () => { - const expected = null - const result = subject.deepConstantKeys(null) - expect(result).toEqual(expected) - type test = Expect> - }) -}) - -describe('deepCamelKeys', () => { - test('should camelize the object', () => { - const expected = { - some: { deepNested: { value: true } }, - otherValue: true, - } - const result = subject.deepCamelKeys({ - some: { 'deep-nested': { value: true } }, - 'other-value': true, - }) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('should camelize from SCREAMING_SNAKE_CASE', () => { - const obj = { - NODE_ENV: 'development', - } - const expected = { - nodeEnv: 'development', - } - const result = subject.deepCamelKeys(obj) - expect(result).toEqual(expected) - type test = Expect> - }) -}) - -test('deepConstantKeys', () => { - const expected = { - SOME: { DEEP_NESTED: { VALUE: true } }, - OTHER_VALUE: true, - } - const result = subject.deepConstantKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('deepDelimiterKeys', () => { - const expected = { - some: { 'deep@nested': { value: true } }, - 'other@value': true, - } - const result = subject.deepDelimiterKeys( - { - some: { 'deep-nested': { value: true } }, - 'other-value': true, - }, - '@', - ) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('deepKebabKeys', () => { - const expected = { - some: { 'deep-nested': { value: true } }, - 'other-value': true, - } - const result = subject.deepKebabKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('deepPascalKeys', () => { - const expected = { - Some: { DeepNested: { Value: true } }, - OtherValue: true, - } - const result = subject.deepPascalKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('deepSnakeKeys', () => { - const expected = { - some: { deep_nested: { value: true } }, - other_value: true, - } - const result = subject.deepSnakeKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) diff --git a/src/deep-key-casing.ts b/src/deep-key-casing.ts deleted file mode 100644 index 40c2226..0000000 --- a/src/deep-key-casing.ts +++ /dev/null @@ -1,193 +0,0 @@ -import type { - CamelCase, - ConstantCase, - DelimiterCase, - KebabCase, - PascalCase, - SnakeCase, -} from './casing' -import { - toCamelCase, - toConstantCase, - toDelimiterCase, - toKebabCase, - toPascalCase, - toSnakeCase, -} from './casing' -import { typeOf } from './internals' - -/** - * This function is used to transform the keys of an object deeply. - * It will only be transformed at runtime, so it's not type safe. - * @param obj the object to transform. - * @param transform the function to transform the keys from string to string. - * @returns the transformed object. - * @example deepTransformKeys({ 'foo-bar': { 'fizz-buzz': true } }, toCamelCase) - * // { fooBar: { fizzBuzz: true } } - */ -function deepTransformKeys(obj: T, transform: (s: string) => string): T { - if (!['object', 'array'].includes(typeOf(obj))) return obj - - if (Array.isArray(obj)) { - return obj.map((x) => deepTransformKeys(x, transform)) as T - } - const res = {} as T - for (const key in obj) { - res[transform(key) as keyof T] = deepTransformKeys(obj[key], transform) - } - return res -} - -/** - * Recursively transforms the keys of an Record to camelCase. - * T: the type of the Record to transform. - */ -type DeepCamelKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepCamelKeys } - : T extends (infer V)[] - ? DeepCamelKeys[] - : { - [K in keyof T as CamelCase>]: DeepCamelKeys - } -/** - * A strongly typed function that recursively transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example deepCamelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { fizzBuzz: true } } - */ -function deepCamelKeys(obj: T): DeepCamelKeys { - return deepTransformKeys(obj, toCamelCase) as never -} - -/** - * Recursively transforms the keys of an Record to CONSTANT_CASE. - * T: the type of the Record to transform. - */ -type DeepConstantKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepConstantKeys } - : T extends (infer V)[] - ? DeepConstantKeys[] - : { - [K in keyof T as ConstantCase>]: DeepConstantKeys - } -/** - * A strongly typed function that recursively transforms the keys of an object to CONSTANT_CASE. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example deepConstantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { FIZZ_BUZZ: true } } - */ -function deepConstantKeys(obj: T): DeepConstantKeys { - return deepTransformKeys(obj, toConstantCase) as never -} - -/** - * Recursively transforms the keys of an Record to a custom delimiter case. - * T: the type of the Record to transform. - * D: the delimiter to use. - */ -type DeepDelimiterKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepDelimiterKeys } - : T extends (infer V)[] - ? DeepDelimiterKeys[] - : { - [K in keyof T as DelimiterCase, D>]: DeepDelimiterKeys< - T[K], - D - > - } -/** - * A strongly typed function that recursively transforms the keys of an object to a custom delimiter case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @param delimiter the delimiter to use. - * @returns the transformed object. - * @example deepDelimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } - */ -function deepDelimiterKeys( - obj: T, - delimiter: D, -): DeepDelimiterKeys { - return deepTransformKeys(obj, (str) => - toDelimiterCase(str, delimiter), - ) as never -} - -/** - * Recursively transforms the keys of an Record to kebab-case. - * T: the type of the Record to transform. - */ -type DeepKebabKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepKebabKeys } - : T extends (infer V)[] - ? DeepKebabKeys[] - : { - [K in keyof T as KebabCase>]: DeepKebabKeys - } -/** - * A strongly typed function that recursively transforms the keys of an object to kebab-case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example deepKebabKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo-bar': { 'fizz-buzz': true } } - */ -function deepKebabKeys(obj: T): DeepKebabKeys { - return deepTransformKeys(obj, toKebabCase) as never -} - -/** - * Recursively transforms the keys of an Record to PascalCase. - * T: the type of the Record to transform. - */ -type DeepPascalKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepPascalKeys } - : T extends (infer V)[] - ? DeepPascalKeys[] - : { - [K in keyof T as PascalCase>]: DeepPascalKeys - } -/** - * A strongly typed function that recursively transforms the keys of an object to pascal case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example deepPascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { FizzBuzz: true } } - */ -function deepPascalKeys(obj: T): DeepPascalKeys { - return deepTransformKeys(obj, toPascalCase) as never -} - -/** - * Recursively transforms the keys of an Record to snake_case. - * T: the type of the Record to transform. - */ -type DeepSnakeKeys = T extends [any, ...any] - ? { [I in keyof T]: DeepSnakeKeys } - : T extends (infer V)[] - ? DeepSnakeKeys[] - : { - [K in keyof T as SnakeCase>]: DeepSnakeKeys - } -/** - * A strongly typed function that recursively transforms the keys of an object to snake_case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example deepSnakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz_buzz': true } } - */ -function deepSnakeKeys(obj: T): DeepSnakeKeys { - return deepTransformKeys(obj, toSnakeCase) as never -} - -export type { - DeepCamelKeys, - DeepConstantKeys, - DeepDelimiterKeys, - DeepKebabKeys, - DeepPascalKeys, - DeepSnakeKeys, -} -export { - deepCamelKeys, - deepConstantKeys, - deepDelimiterKeys, - deepKebabKeys, - deepPascalKeys, - deepSnakeKeys, - deepTransformKeys, -} diff --git a/src/deep-key-casing.types.test.ts b/src/deep-key-casing.types.test.ts deleted file mode 100644 index e423e8e..0000000 --- a/src/deep-key-casing.types.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type * as Subject from './deep-key-casing' - -namespace TypeTransforms { - type test = Expect< - Equal< - Subject.DeepDelimiterKeys< - { - some: { 'deep-nested': { value: true } } - 'other-value': true - }, - '@' - >, - { some: { 'deep@nested': { value: true } }; 'other@value': true } - > - > - type test1 = Expect< - Equal< - Subject.DeepCamelKeys<{ - some: { 'deep-nested': { value: true } } - 'other-value': true - }>, - { some: { deepNested: { value: true } }; otherValue: true } - > - > - type test2 = Expect< - Equal< - Subject.DeepSnakeKeys<{ - some: { 'deep-nested': { value: true } } - 'other-value': true - }>, - { some: { deep_nested: { value: true } }; other_value: true } - > - > - type test3 = Expect< - Equal< - Subject.DeepKebabKeys<{ - some: { deepNested: { value: true } } - otherValue: true - }>, - { some: { 'deep-nested': { value: true } }; 'other-value': true } - > - > - type test4 = Expect< - Equal< - Subject.DeepPascalKeys<{ - some: { 'deep-nested': { value: true } } - 'other-value': true - }>, - { Some: { DeepNested: { Value: true } }; OtherValue: true } - > - > - type test5 = Expect< - Equal< - Subject.DeepConstantKeys<{ - some: { 'deep-nested': { value: true } } - 'other-value': true - }>, - { SOME: { DEEP_NESTED: { VALUE: true } }; OTHER_VALUE: true } - > - > -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/index.ts b/src/index.ts index a0d10f7..54a08f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,118 +1,101 @@ -// PRIMITIVES -export type { - CharAt, - Concat, - EndsWith, - Includes, - Join, - Length, - PadEnd, - PadStart, - Repeat, - Replace, - ReplaceAll, - Slice, - Split, - StartsWith, - TrimStart, - TrimEnd, - Trim, -} from './primitives' -export { - charAt, - concat, - endsWith, - includes, - join, - length, - padEnd, - padStart, - repeat, - replace, - replaceAll, - slice, - split, - startsWith, - trim, - trimStart, - trimEnd, -} from './primitives' +// Native +export type { CharAt } from './native/char-at.js' +export type { Concat } from './native/concat.js' +export type { EndsWith } from './native/ends-with.js' +export type { Includes } from './native/includes.js' +export type { Join } from './native/join.js' +export type { Length } from './native/length.js' +export type { PadEnd } from './native/pad-end.js' +export type { PadStart } from './native/pad-start.js' +export type { Repeat } from './native/repeat.js' +export type { Replace } from './native/replace.js' +export type { ReplaceAll } from './native/replace-all.js' +export type { Slice } from './native/slice.js' +export type { Split } from './native/split.js' +export type { StartsWith } from './native/starts-with.js' +export type { TrimStart } from './native/trim-start.js' +export type { TrimEnd } from './native/trim-end.js' +export type { Trim } from './native/trim.js' -// UTILS -export type { Truncate, Words } from './utils' -export { truncate, words } from './utils' +export { charAt } from './native/char-at.js' +export { concat } from './native/concat.js' +export { endsWith } from './native/ends-with.js' +export { includes } from './native/includes.js' +export { join } from './native/join.js' +export { length } from './native/length.js' +export { padEnd } from './native/pad-end.js' +export { padStart } from './native/pad-start.js' +export { repeat } from './native/repeat.js' +export { replace } from './native/replace.js' +export { replaceAll } from './native/replace-all.js' +export { slice } from './native/slice.js' +export { split } from './native/split.js' +export { startsWith } from './native/starts-with.js' +export { trimStart } from './native/trim-start.js' +export { trimEnd } from './native/trim-end.js' -// CHARACTERS -export type { - Digit, - IsDigit, - IsLetter, - IsLower, - IsSpecial, - IsUpper, -} from './chars' -// SEPARATORS -export type { Separator, IsSeparator } from './separators' +// Utils +export type { Truncate } from './utils/truncate.js' +export type { Words } from './utils/words.js' -// CASING -export type { - CamelCase, - ConstantCase, - DelimiterCase, - KebabCase, - PascalCase, - SnakeCase, - TitleCase, -} from './casing' -export { - capitalize, - lowerCase, - toCamelCase, - toConstantCase, - toDelimiterCase, - toKebabCase, - toLowerCase, - toPascalCase, - toSnakeCase, - toTitleCase, - toUpperCase, - uncapitalize, - upperCase, -} from './casing' +export { truncate } from './utils/truncate.js' +export { words } from './utils/words.js' -// KEY CASING -export type { - CamelKeys, - ConstantKeys, - DelimiterKeys, - KebabKeys, - PascalKeys, - SnakeKeys, -} from './key-casing' -export { - camelKeys, - constantKeys, - delimiterKeys, - kebabKeys, - pascalKeys, - snakeKeys, -} from './key-casing' +// Characters +export type { IsLetter, IsLower, IsUpper } from './utils/characters/letters.js' +export type { Digit, IsDigit } from './utils/characters/numbers.js' +export type { IsSpecial } from './utils/characters/special.js' +export type { Separator, IsSeparator } from './utils/characters/separators.js' -// DEEP KEY CASING -export type { - DeepCamelKeys, - DeepConstantKeys, - DeepDelimiterKeys, - DeepKebabKeys, - DeepPascalKeys, - DeepSnakeKeys, -} from './deep-key-casing' -export { - deepCamelKeys, - deepConstantKeys, - deepDelimiterKeys, - deepKebabKeys, - deepPascalKeys, - deepSnakeKeys, - deepTransformKeys, -} from './deep-key-casing' +// Word casing +export type { CamelCase } from './utils/word-case/to-camel-case.js' +export type { ConstantCase } from './utils/word-case/to-constant-case.js' +export type { DelimiterCase } from './utils/word-case/to-delimiter-case.js' +export type { KebabCase } from './utils/word-case/to-kebab-case.js' +export type { PascalCase } from './utils/word-case/to-pascal-case.js' +export type { SnakeCase } from './utils/word-case/to-snake-case.js' +export type { TitleCase } from './utils/word-case/to-title-case.js' + +export { capitalize } from './utils/capitalize.js' +export { lowerCase } from './utils/word-case/lower-case.js' +export { toCamelCase } from './utils/word-case/to-camel-case.js' +export { toConstantCase } from './utils/word-case/to-constant-case.js' +export { toDelimiterCase } from './utils/word-case/to-delimiter-case.js' +export { toKebabCase } from './utils/word-case/to-kebab-case.js' +export { toLowerCase } from './native/to-lower-case.js' +export { toPascalCase } from './utils/word-case/to-pascal-case.js' +export { toSnakeCase } from './utils/word-case/to-snake-case.js' +export { toTitleCase } from './utils/word-case/to-title-case.js' +export { toUpperCase } from './native/to-upper-case.js' +export { uncapitalize } from './utils/uncapitalize.js' +export { upperCase } from './utils/word-case/upper-case.js' + +// Object keys word casing +export type { CamelKeys } from './utils/object-keys/camel-keys.js' +export type { ConstantKeys } from './utils/object-keys/constant-keys.js' +export type { DelimiterKeys } from './utils/object-keys/delimiter-keys.js' +export type { KebabKeys } from './utils/object-keys/kebab-keys.js' +export type { PascalKeys } from './utils/object-keys/pascal-keys.js' +export type { SnakeKeys } from './utils/object-keys/snake-keys.js' + +export { camelKeys } from './utils/object-keys/camel-keys.js' +export { constantKeys } from './utils/object-keys/constant-keys.js' +export { delimiterKeys } from './utils/object-keys/delimiter-keys.js' +export { kebabKeys } from './utils/object-keys/kebab-keys.js' +export { pascalKeys } from './utils/object-keys/pascal-keys.js' +export { snakeKeys } from './utils/object-keys/snake-keys.js' + +// Object keys word casing (deep) +export type { DeepCamelKeys } from './utils/object-keys/deep-camel-keys.js' +export type { DeepConstantKeys } from './utils/object-keys/deep-constant-keys.js' +export type { DeepDelimiterKeys } from './utils/object-keys/deep-delimiter-keys.js' +export type { DeepKebabKeys } from './utils/object-keys/deep-kebab-keys.js' +export type { DeepPascalKeys } from './utils/object-keys/deep-pascal-keys.js' +export type { DeepSnakeKeys } from './utils/object-keys/deep-snake-keys.js' + +export { deepCamelKeys } from './utils/object-keys/deep-camel-keys.js' +export { deepConstantKeys } from './utils/object-keys/deep-constant-keys.js' +export { deepDelimiterKeys } from './utils/object-keys/deep-delimiter-keys.js' +export { deepKebabKeys } from './utils/object-keys/deep-kebab-keys.js' +export { deepPascalKeys } from './utils/object-keys/deep-pascal-keys.js' +export { deepSnakeKeys } from './utils/object-keys/deep-snake-keys.js' diff --git a/src/internal/fixtures.ts b/src/internal/fixtures.ts new file mode 100644 index 0000000..9b291b4 --- /dev/null +++ b/src/internal/fixtures.ts @@ -0,0 +1,7 @@ +export const SEPARATORS_TEXT = + '[one] two-three/four.five(six){seven}|eight_nine\\ten' as const + +export const WEIRD_TEXT = + ' someWeird-cased$*String1986Foo [Bar] W_FOR_WUMBO...' as const + +export type WeirdTextUnion = typeof WEIRD_TEXT | 'dont.distribute unions' diff --git a/src/internals.test.ts b/src/internal/internals.test.ts similarity index 56% rename from src/internals.test.ts rename to src/internal/internals.test.ts index be63e03..4bd3f72 100644 --- a/src/internals.test.ts +++ b/src/internal/internals.test.ts @@ -1,4 +1,25 @@ import * as subject from './internals' +import type * as Subject from './internals' + +namespace Internals { + type test = Expect< + Equal< + Subject.PascalCaseAll<['one', 'two', 'three']>, + ['One', 'Two', 'Three'] + > + > + + type test1 = Expect< + Equal< + Subject.Reject<['one', '', 'two', '', 'three'], ''>, + ['one', 'two', 'three'] + > + > + + type test2 = Expect, 'hello'>> + + type test3 = Expect, [' ', ' ', ' ']>> +} describe('typeOf', () => { test('null', () => { diff --git a/src/internals.ts b/src/internal/internals.ts similarity index 93% rename from src/internals.ts rename to src/internal/internals.ts index c71707b..522a307 100644 --- a/src/internals.ts +++ b/src/internal/internals.ts @@ -1,4 +1,5 @@ -import { capitalize, toLowerCase } from './casing' +import { capitalize } from '../utils/capitalize.js' +import { toLowerCase } from '../native/to-lower-case.js' /** * This is an enhanced version of the typeof operator to check the type of more complex values. diff --git a/src/math.ts b/src/internal/math.ts similarity index 84% rename from src/math.ts rename to src/internal/math.ts index 74c481c..ebb2c3c 100644 --- a/src/math.ts +++ b/src/internal/math.ts @@ -1,5 +1,5 @@ -import type { Length } from './primitives' -import type { TupleOf } from './internals' +import type { Length } from '../native/length.js' +import type { TupleOf } from './internals.js' namespace Math { export type Subtract< diff --git a/src/types.d.ts b/src/internal/types.d.ts similarity index 100% rename from src/types.d.ts rename to src/internal/types.d.ts diff --git a/src/internals.types.test.ts b/src/internals.types.test.ts deleted file mode 100644 index 10b04c1..0000000 --- a/src/internals.types.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type * as Subject from './internals' - -namespace Internals { - type test = Expect< - Equal< - Subject.PascalCaseAll<['one', 'two', 'three']>, - ['One', 'Two', 'Three'] - > - > - - type test1 = Expect< - Equal< - Subject.Reject<['one', '', 'two', '', 'three'], ''>, - ['one', 'two', 'three'] - > - > - - type test2 = Expect, 'hello'>> - - type test3 = Expect, [' ', ' ', ' ']>> -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/key-casing.test.ts b/src/key-casing.test.ts deleted file mode 100644 index e1bd3a4..0000000 --- a/src/key-casing.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as subject from './key-casing' - -describe('transformKeys', () => { - test('should shallowly transform the keys of an object', () => { - const expected = { - SOME: { 'deep-nested': { value: true } }, - 'OTHER-VALUE': true, - } - const result = subject.transformKeys( - { - some: { 'deep-nested': { value: true } }, - 'other-value': true, - }, - (key) => key.toUpperCase(), - ) - expect(result).toEqual(expected) - }) - - test('should handle null properly', () => { - const expected = null - const result = subject.constantKeys(null) - expect(result).toEqual(expected) - type test = Expect> - }) -}) - -test('camelKeys', () => { - const expected = { - some: { 'deep-nested': { value: true } }, - otherValue: true, - } - const result = subject.camelKeys({ - some: { 'deep-nested': { value: true } }, - 'other-value': true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('constantKeys', () => { - const expected = { - SOME: { deepNested: { value: true } }, - OTHER_VALUE: true, - } - const result = subject.constantKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('delimiterKeys', () => { - const expected = { - some: { 'deep-nested': { value: true } }, - 'other@value': true, - } - const result = subject.delimiterKeys( - { - some: { 'deep-nested': { value: true } }, - 'other-value': true, - }, - '@', - ) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('kebabKeys', () => { - const expected = { - some: { deepNested: { value: true } }, - 'other-value': true, - } - const result = subject.kebabKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('pascalKeys', () => { - const expected = { - Some: { deepNested: { value: true } }, - OtherValue: true, - } - const result = subject.pascalKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) - -test('snakeKeys', () => { - const expected = { - some: { deepNested: { value: true } }, - other_value: true, - } - const result = subject.snakeKeys({ - some: { deepNested: { value: true } }, - otherValue: true, - }) - expect(result).toEqual(expected) - type test = Expect> -}) diff --git a/src/key-casing.ts b/src/key-casing.ts deleted file mode 100644 index b8105ab..0000000 --- a/src/key-casing.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { - CamelCase, - ConstantCase, - DelimiterCase, - KebabCase, - PascalCase, - SnakeCase, -} from './casing' -import { - toCamelCase, - toConstantCase, - toDelimiterCase, - toKebabCase, - toPascalCase, - toSnakeCase, -} from './casing' -import { typeOf } from './internals' - -/** - * This function is used to shallowly transform the keys of an object. - * It will only be transformed at runtime, so it's not type safe. - * @param obj the object to transform. - * @param transform the function to transform the keys from string to string. - * @returns the transformed object. - * @example transformKeys({ 'foo-bar': { 'fizz-buzz': true } }, toCamelCase) - * // { fooBar: { 'fizz-buzz': true } } - */ -function transformKeys(obj: T, transform: (s: string) => string): T { - if (typeOf(obj) !== 'object') return obj - - const res = {} as T - for (const key in obj) { - res[transform(key) as keyof T] = obj[key] - } - return res -} - -/** - * Shallowly transforms the keys of an Record to camelCase. - * T: the type of the Record to transform. - */ -type CamelKeys = T extends [] - ? T - : { [K in keyof T as CamelCase>]: T[K] } -/** - * A strongly typed function that shallowly transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example camelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { 'fizz-buz': true } } - */ -function camelKeys(obj: T): CamelKeys { - return transformKeys(obj, toCamelCase) as never -} - -/** - * Shallowly transforms the keys of an Record to CONSTANT_CASE. - * T: the type of the Record to transform. - */ -type ConstantKeys = T extends [] - ? T - : { [K in keyof T as ConstantCase>]: T[K] } -/** - * A strongly typed function that shallowly transforms the keys of an object to CONSTANT_CASE. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example constantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { 'fizz-buzz': true } } - */ -function constantKeys(obj: T): ConstantKeys { - return transformKeys(obj, toConstantCase) as never -} - -/** - * Shallowly transforms the keys of an Record to a custom delimiter case. - * T: the type of the Record to transform. - * D: the delimiter to use. - */ -type DelimiterKeys = T extends [] - ? T - : { [K in keyof T as DelimiterCase, D>]: T[K] } -/** - * A strongly typed function that shallowly transforms the keys of an object to a custom delimiter case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @param delimiter the delimiter to use. - * @returns the transformed object. - * @example delimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } - */ -function delimiterKeys( - obj: T, - delimiter: D, -): DelimiterKeys { - return transformKeys(obj, (str) => toDelimiterCase(str, delimiter)) as never -} - -/** - * Shallowly transforms the keys of an Record to kebab-case. - * T: the type of the Record to transform. - */ -type KebabKeys = T extends [] - ? T - : { - [K in keyof T as KebabCase>]: T[K] - } -/** - * A strongly typed function that shallowly transforms the keys of an object to kebab-case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example kebabKeys({ fooBar: { fizzBuzz: true } }) // { 'foo-bar': { fizzBuzz: true } } - */ -function kebabKeys(obj: T): KebabKeys { - return transformKeys(obj, toKebabCase) as never -} - -/** - * Shallowly transforms the keys of an Record to PascalCase. - * T: the type of the Record to transform. - */ -type PascalKeys = T extends [] - ? T - : { [K in keyof T as PascalCase>]: T[K] } -/** - * A strongly typed function that shallowly transforms the keys of an object to pascal case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example pascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { 'fizz-buzz': true } } - */ -function pascalKeys(obj: T): PascalKeys { - return transformKeys(obj, toPascalCase) as never -} - -/** - * Shallowly transforms the keys of an Record to snake_case. - * T: the type of the Record to transform. - */ -type SnakeKeys = T extends [] - ? T - : { [K in keyof T as SnakeCase>]: T[K] } -/** - * A strongly typed function that shallowly the keys of an object to snake_case. The transformation is done both at runtime and type level. - * @param obj the object to transform. - * @returns the transformed object. - * @example snakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz-buzz': true } } - */ -function snakeKeys(obj: T): SnakeKeys { - return transformKeys(obj, toSnakeCase) as never -} - -export type { - CamelKeys, - ConstantKeys, - DelimiterKeys, - KebabKeys, - PascalKeys, - SnakeKeys, -} -export { - camelKeys, - constantKeys, - delimiterKeys, - kebabKeys, - pascalKeys, - snakeKeys, - transformKeys, -} diff --git a/src/key-casing.types.test.ts b/src/key-casing.types.test.ts deleted file mode 100644 index 9099422..0000000 --- a/src/key-casing.types.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type * as Subject from './key-casing' - -namespace TypeTransforms { - type test = Expect< - Equal< - Subject.DelimiterKeys< - { - 'some-value': { 'nested-value': true } - 'other-value': true - }, - '@' - >, - { 'some@value': { 'nested-value': true }; 'other@value': true } - > - > - type test1 = Expect< - Equal< - Subject.CamelKeys<{ - 'some-value': { 'deep-nested': true } - 'other-value': true - }>, - { someValue: { 'deep-nested': true }; otherValue: true } - > - > - type test2 = Expect< - Equal< - Subject.SnakeKeys<{ - 'some-value': { 'deep-nested': true } - 'other-value': true - }>, - { some_value: { 'deep-nested': true }; other_value: true } - > - > - type test3 = Expect< - Equal< - Subject.KebabKeys<{ - someValue: { deepNested: true } - otherValue: true - }>, - { 'some-value': { deepNested: true }; 'other-value': true } - > - > - type test4 = Expect< - Equal< - Subject.PascalKeys<{ - someValue: { deepNested: true } - otherValue: true - }>, - { SomeValue: { deepNested: true }; OtherValue: true } - > - > - type test5 = Expect< - Equal< - Subject.ConstantKeys<{ - someValue: { deepNested: true } - otherValue: true - }>, - { SOME_VALUE: { deepNested: true }; OTHER_VALUE: true } - > - > -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/native/char-at.test.ts b/src/native/char-at.test.ts new file mode 100644 index 0000000..5edaba6 --- /dev/null +++ b/src/native/char-at.test.ts @@ -0,0 +1,14 @@ +import { type CharAt, charAt } from './char-at.js' + +namespace TypeTests { + type test = Expect, 'n'>> +} + +describe('charAt', () => { + test('should get the character of a string at the given index in both type and runtime level', () => { + const data = 'some nice string' + const result = charAt(data, 5) + expect(result).toEqual('n') + type test = Expect> + }) +}) diff --git a/src/native/char-at.ts b/src/native/char-at.ts new file mode 100644 index 0000000..4b6e7e8 --- /dev/null +++ b/src/native/char-at.ts @@ -0,0 +1,21 @@ +import type { Split } from './split.js' + +/** + * Gets the character at the given index. + * T: The string to get the character from. + * index: The index of the character. + */ +export type CharAt = Split[index] +/** + * A strongly-typed version of `String.prototype.charAt`. + * @param str the string to get the character from. + * @param index the index of the character. + * @returns the character in both type level and runtime. + * @example charAt('hello world', 6) // 'w' + */ +export function charAt( + str: T, + index: I, +): CharAt { + return str.charAt(index) +} diff --git a/src/native/concat.test.ts b/src/native/concat.test.ts new file mode 100644 index 0000000..5adb5b2 --- /dev/null +++ b/src/native/concat.test.ts @@ -0,0 +1,16 @@ +import { type Concat, concat } from './concat.js' + +namespace TypeTests { + type test = Expect< + Equal, 'abcdef' | '123456'> + > +} + +describe('concat', () => { + test('concatenates', () => { + const result = concat('one', 'two', 'three') + const expected = 'onetwothree' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/native/concat.ts b/src/native/concat.ts new file mode 100644 index 0000000..510adfb --- /dev/null +++ b/src/native/concat.ts @@ -0,0 +1,18 @@ +import { join } from './join.js' +import type { Join } from './join.js' + +/** + * Concatenates a tuple of strings. + * T: The tuple of strings to concatenate. + */ +export type Concat = Join + +/** + * A strongly-typed version of `String.prototype.concat`. + * @param strings the tuple of strings to concatenate. + * @returns the concatenated string in both type level and runtime. + * @example concat('a', 'bc', 'def') // 'abcdef' + */ +export function concat(...strings: T): Concat { + return join(strings) +} diff --git a/src/native/ends-with.test.ts b/src/native/ends-with.test.ts new file mode 100644 index 0000000..1d2efaa --- /dev/null +++ b/src/native/ends-with.test.ts @@ -0,0 +1,53 @@ +import { type EndsWith, endsWith } from './ends-with.js' + +namespace TypeTests { + type test = Expect, true>> +} + +describe('endsWith', () => { + const text = 'abc' + + describe('without offset', () => { + test('should return true when text ends with search', () => { + const result = endsWith(text, 'c') + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when text does not end with search', () => { + const result = endsWith(text, 'b') + expect(result).toEqual(false) + type test = Expect> + }) + }) + + describe('with offset', () => { + test('should return true when offset text ends with search', () => { + const result = endsWith(text, 'b', 2) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return true when offset text ends with search (multi-char)', () => { + const result = endsWith(text, 'bc', 3) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when offset string does not end with search', () => { + const result = endsWith(text, 'c', 1) + expect(result).toEqual(false) + type test = Expect> + }) + }) + + describe('with bad offset', () => { + test('should return false when the offset is negative', () => { + const result = endsWith(text, 'a', -1) + expect(result).toEqual(false) + type test = Expect> + }) + test('should return true when the end matches and offset is greater than text length', () => { + const result = endsWith(text, 'c', 10) + expect(result).toEqual(true) + type test = Expect> + }) + }) +}) diff --git a/src/native/ends-with.ts b/src/native/ends-with.ts new file mode 100644 index 0000000..0e5a24a --- /dev/null +++ b/src/native/ends-with.ts @@ -0,0 +1,37 @@ +import type { Math } from '../internal/math.js' +import type { Length } from './length.js' +import type { Slice } from './slice.js' + +/** + * Checks if a string ends with another string. + * T: The string to check. + * S: The string to check against. + * P: The position the search should end. + */ +export type EndsWith< + T extends string, + S extends string, + P extends number = Length, +> = Math.IsNegative

extends false + ? P extends Length + ? S extends Slice, Length>, Length> + ? true + : false + : EndsWith, S, Length> // P !== T.length, slice + : false // P is negative, false + +/** + * A strongly-typed version of `String.prototype.endsWith`. + * @param text the string to search. + * @param search the string to search with. + * @param position the index the search should end at. + * @returns boolean, whether or not the text string ends with the search string. + * @example endsWith('abc', 'c') // true + */ +export function endsWith< + T extends string, + S extends string, + P extends number = Length, +>(text: T, search: S, position = text.length as P) { + return text.endsWith(search, position) as EndsWith +} diff --git a/src/native/includes.test.ts b/src/native/includes.test.ts new file mode 100644 index 0000000..ebc5bbe --- /dev/null +++ b/src/native/includes.test.ts @@ -0,0 +1,53 @@ +import { type Includes, includes } from './includes.js' + +namespace TypeTests { + type test = Expect, true>> +} + +describe('includes', () => { + const text = 'abcde' + + describe('without offset', () => { + test('should return true when text contains search', () => { + const result = includes(text, 'bcd') + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when text does not end with search', () => { + const result = includes(text, 'hello') + expect(result).toEqual(false) + type test = Expect> + }) + }) + + describe('with offset', () => { + test('should return true when offset text does contain search', () => { + const result = includes(text, 'c', 1) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return true when offset text does contain search (multi-char)', () => { + const result = includes(text, 'bcd', 1) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when offset string does not contain search', () => { + const result = includes(text, 'abc', 3) + expect(result).toEqual(false) + type test = Expect> + }) + }) + + describe('with bad offset', () => { + test('should ignore offset when the offset is negative', () => { + const result = includes(text, 'a', -100) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when text contains search but offset is greater than text length', () => { + const result = includes(text, 'c', 10) + expect(result).toEqual(false) + type test = Expect> + }) + }) +}) diff --git a/src/native/includes.ts b/src/native/includes.ts new file mode 100644 index 0000000..16527d2 --- /dev/null +++ b/src/native/includes.ts @@ -0,0 +1,36 @@ +import type { Math } from '../internal/math.js' +import type { Slice } from './slice.js' + +/** + * Checks if a string includes another string. + * T: The string to check. + * S: The string to check against. + * P: The position to start the search. + */ +export type Includes< + T extends string, + S extends string, + P extends number = 0, +> = Math.IsNegative

extends false + ? P extends 0 + ? T extends `${string}${S}${string}` + ? true + : false + : Includes, S, 0> // P is >0, slice + : Includes // P is negative, ignore it + +/** + * A strongly-typed version of `String.prototype.includes`. + * @param text the string to search + * @param search the string to search with + * @param position the index to start search at + * @returns boolean, whether or not the text contains the search string. + * @example includes('abcde', 'bcd') // true + */ +export function includes< + T extends string, + S extends string, + P extends number = 0, +>(text: T, search: S, position = 0 as P) { + return text.includes(search, position) as Includes +} diff --git a/src/native/join.test.ts b/src/native/join.test.ts new file mode 100644 index 0000000..fd7cf07 --- /dev/null +++ b/src/native/join.test.ts @@ -0,0 +1,22 @@ +import { type Join, join } from './join.js' + +namespace TypeTests { + type test = Expect< + Equal, 'some nice string'> + > +} + +describe('join', () => { + test('should join words in both type level and runtime level', () => { + const result = join(['a', 'b', 'c'], '-') + expect(result).toEqual('a-b-c') + type test = Expect> + }) + + test('should join only at runtime level when type is wide', () => { + const data = ['a', 'b', 'c'] + const result = join(data, '-') + expect(result).toEqual('a-b-c') + type test = Expect> + }) +}) diff --git a/src/native/join.ts b/src/native/join.ts new file mode 100644 index 0000000..01c6340 --- /dev/null +++ b/src/native/join.ts @@ -0,0 +1,32 @@ +/** + * Joins a tuple of strings with the given delimiter. + * T: The tuple of strings to join. + * delimiter: The delimiter. + */ +export type Join< + T extends readonly string[], + delimiter extends string = '', +> = string[] extends T + ? string // Avoid spending resources on a wide type + : T extends readonly [ + infer first extends string, + ...infer rest extends string[], + ] + ? rest extends [] + ? first + : `${first}${delimiter}${Join}` + : '' + +/** + * A strongly-typed version of `Array.prototype.join`. + * @param tuple the tuple of strings to join. + * @param delimiter the delimiter. + * @returns the joined string in both type level and runtime. + * @example join(['hello', 'world'], '-') // 'hello-world' + */ +export function join( + tuple: T, + delimiter: D = '' as D, +) { + return tuple.join(delimiter) as Join +} diff --git a/src/native/length.test.ts b/src/native/length.test.ts new file mode 100644 index 0000000..c4f980b --- /dev/null +++ b/src/native/length.test.ts @@ -0,0 +1,14 @@ +import { type Length, length } from './length.js' + +namespace TypeTests { + type test = Expect, 16>> +} + +describe('length', () => { + test('should return the lenght of a string at both type level and runtime level', () => { + const data = 'some nice string' + const result = length(data) + expect(result).toEqual(16) + type test = Expect> + }) +}) diff --git a/src/native/length.ts b/src/native/length.ts new file mode 100644 index 0000000..74b8eb6 --- /dev/null +++ b/src/native/length.ts @@ -0,0 +1,15 @@ +import type { Split } from './split.js' + +/** + * Gets the length of a string. + */ +export type Length = Split['length'] +/** + * A strongly-typed version of `String.prototype.length`. + * @param str the string to get the length from. + * @returns the length of the string in both type level and runtime. + * @example length('hello world') // 11 + */ +export function length(str: T) { + return str.length as Length +} diff --git a/src/native/pad-end.test.ts b/src/native/pad-end.test.ts new file mode 100644 index 0000000..d5aba53 --- /dev/null +++ b/src/native/pad-end.test.ts @@ -0,0 +1,38 @@ +import { padEnd } from './pad-end.js' + +describe('padEnd', () => { + test('should pad a string at the end', () => { + const data = 'hello' + const result = padEnd(data, 10) + expect(result).toEqual('hello ') + type test = Expect> + }) + + test('should pad with a given string', () => { + const data = 'hello' + const result = padEnd(data, 10, '=>') + expect(result).toEqual('hello=>=>=') + type test = Expect=>='>> + }) + + test('should not pad if no arguments are given', () => { + const data = 'hello' + const result = padEnd(data) + expect(result).toEqual('hello') + type test = Expect> + }) + + test('should not pad or truncate if length is shorter than string', () => { + const data = 'hello' + const result = padEnd(data, 3, '=') + expect(result).toEqual('hello') + type test = Expect> + }) + + test('should not pad for negative numbers', () => { + const data = 'hello' + const result = padEnd(data, -1, '=') + expect(result).toEqual('hello') + type test = Expect> + }) +}) diff --git a/src/native/pad-end.ts b/src/native/pad-end.ts new file mode 100644 index 0000000..fea79fd --- /dev/null +++ b/src/native/pad-end.ts @@ -0,0 +1,35 @@ +import type { Math } from '../internal/math.js' +import type { Slice } from './slice.js' +import type { Repeat } from './repeat.js' +import type { Length } from './length.js' + +/** + * Pads a string at the end with another string. + * T: The string to pad. + * times: The number of times to pad. + * pad: The string to pad with. + */ +export type PadEnd< + T extends string, + times extends number = 0, + pad extends string = ' ', +> = Math.IsNegative extends false + ? Math.Subtract> extends infer missing extends number + ? `${T}${Slice, 0, missing>}` + : never + : T +/** + * A strongly-typed version of `String.prototype.padEnd`. + * @param str the string to pad. + * @param length the length to pad. + * @param pad the string to pad with. + * @returns the padded string in both type level and runtime. + * @example padEnd('hello', 10, '=') // 'hello=====' + */ +export function padEnd< + T extends string, + N extends number = 0, + U extends string = ' ', +>(str: T, length: N = 0 as N, pad: U = ' ' as U) { + return str.padEnd(length, pad) as PadEnd +} diff --git a/src/native/pad-start.test.ts b/src/native/pad-start.test.ts new file mode 100644 index 0000000..b776cc1 --- /dev/null +++ b/src/native/pad-start.test.ts @@ -0,0 +1,38 @@ +import { padStart } from './pad-start.js' + +describe('padStart', () => { + test('should pad a string at the start', () => { + const data = 'hello' + const result = padStart(data, 10) + expect(result).toEqual(' hello') + type test = Expect> + }) + + test('should pad with a given string', () => { + const data = 'hello' + const result = padStart(data, 10, '=>') + expect(result).toEqual('=>=>=hello') + type test = Expect=>=hello'>> + }) + + test('should not pad if no arguments are given', () => { + const data = 'hello' + const result = padStart(data) + expect(result).toEqual('hello') + type test = Expect> + }) + + test('should not pad or truncate if length is shorter than string', () => { + const data = 'hello' + const result = padStart(data, 3, '=') + expect(result).toEqual('hello') + type test = Expect> + }) + + test('should not pad for negative numbers', () => { + const data = 'hello' + const result = padStart(data, -1, '=') + expect(result).toEqual('hello') + type test = Expect> + }) +}) diff --git a/src/native/pad-start.ts b/src/native/pad-start.ts new file mode 100644 index 0000000..7c71885 --- /dev/null +++ b/src/native/pad-start.ts @@ -0,0 +1,35 @@ +import type { Math } from '../internal/math.js' +import type { Slice } from './slice.js' +import type { Repeat } from './repeat.js' +import type { Length } from './length.js' + +/** + * Pads a string at the start with another string. + * T: The string to pad. + * times: The number of times to pad. + * pad: The string to pad with. + */ +export type PadStart< + T extends string, + times extends number = 0, + pad extends string = ' ', +> = Math.IsNegative extends false + ? Math.Subtract> extends infer missing extends number + ? `${Slice, 0, missing>}${T}` + : never + : T +/** + * A strongly-typed version of `String.prototype.padStart`. + * @param str the string to pad. + * @param length the length to pad. + * @param pad the string to pad with. + * @returns the padded string in both type level and runtime. + * @example padStart('hello', 10, '=') // '=====hello' + */ +export function padStart< + T extends string, + N extends number = 0, + U extends string = ' ', +>(str: T, length: N = 0 as N, pad: U = ' ' as U) { + return str.padStart(length, pad) as PadStart +} diff --git a/src/native/repeat.test.ts b/src/native/repeat.test.ts new file mode 100644 index 0000000..82a7749 --- /dev/null +++ b/src/native/repeat.test.ts @@ -0,0 +1,24 @@ +import type { Repeat } from './repeat.js' +import { repeat } from './repeat.js' + +describe('repeat', () => { + test('should repeat the string by a given number of times', () => { + const data = 'abc' + const result = repeat(data, 3) + expect(result).toEqual('abcabcabc') + type test = Expect> + }) + + test('should be empty when repeating 0 times', () => { + const data = 'abc' + const result = repeat(data) + expect(result).toEqual('') + type test = Expect> + }) + + test('should throw when trying to repeat with negative number', () => { + const data = 'abc' + expect(() => repeat(data, -1)).toThrow() + type test = Expect, never>> + }) +}) diff --git a/src/native/repeat.ts b/src/native/repeat.ts new file mode 100644 index 0000000..1cc1ed9 --- /dev/null +++ b/src/native/repeat.ts @@ -0,0 +1,27 @@ +import type { Math } from '../internal/math.js' +import type { Join } from './join.js' +import type { TupleOf } from '../internal/internals.js' + +/** + * Repeats a string N times. + * T: The string to repeat. + * N: The number of times to repeat. + */ +export type Repeat = times extends 0 + ? '' + : Math.IsNegative extends false + ? Join> + : never +/** + * A strongly-typed version of `String.prototype.repeat`. + * @param str the string to repeat. + * @param times the number of times to repeat. + * @returns the repeated string in both type level and runtime. + * @example repeat('hello', 3) // 'hellohellohello' + */ +export function repeat( + str: T, + times: N = 0 as N, +) { + return str.repeat(times) as Repeat +} diff --git a/src/native/replace-all.test.ts b/src/native/replace-all.test.ts new file mode 100644 index 0000000..041d857 --- /dev/null +++ b/src/native/replace-all.test.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { type ReplaceAll, replaceAll } from './replace-all.js' + +namespace TypeTests { + type test = Expect< + Equal, 'some-nice-string'> + > +} + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('replaceAll', () => { + test('should replace all chars in a string at both type level and runtime level once', () => { + const data = 'some nice string' + const result = replaceAll(data, ' ') + expect(result).toEqual('somenicestring') + type test = Expect> + }) + + test('accepts an argument for the replacement', () => { + const data = 'some nice string' + const result = replaceAll(data, ' ', '@') + expect(result).toEqual('some@nice@string') + type test = Expect> + }) + + test('should replace chars but not at type level when using RegExp', () => { + const data = 'some nice string' + const result = replaceAll(data, / /g, '-') + expect(result).toEqual('some-nice-string') + // Note: `string` instead of `some-nice-string` + type test = Expect> + }) +}) + +describe('replaceAll polyfill', () => { + const replaceAllPlaceholder = String.prototype.replaceAll + + beforeAll(() => { + // @ts-ignore + String.prototype.replaceAll = undefined + }) + + afterAll(() => { + String.prototype.replaceAll = replaceAllPlaceholder + }) + + test('it works through a polyfill', () => { + const spy = vi.spyOn(String.prototype, 'replace') + const data = 'some nice string' + const result = replaceAll(data, ' ', '@') + expect(result).toEqual('some@nice@string') + expect(spy).toHaveBeenCalledWith(/ /g, '@') + }) +}) diff --git a/src/native/replace-all.ts b/src/native/replace-all.ts new file mode 100644 index 0000000..b029fbf --- /dev/null +++ b/src/native/replace-all.ts @@ -0,0 +1,37 @@ +/** + * Replaces all the occurrences of a string with another string. + * sentence: The sentence to replace. + * lookup: The lookup string to be replaced. + * replacement: The replacement string. + */ +export type ReplaceAll< + sentence extends string, + lookup extends string | RegExp, + replacement extends string = '', +> = lookup extends string + ? sentence extends `${infer rest}${lookup}${infer rest2}` + ? `${rest}${replacement}${ReplaceAll}` + : sentence + : string + +/** + * A strongly-typed version of `String.prototype.replaceAll`. + * @param sentence the sentence to replace. + * @param lookup the lookup string to be replaced. + * @param replacement the replacement string. + * @returns the replaced string in both type level and runtime. + * @example replaceAll('hello world', 'l', '1') // 'he11o wor1d' + */ +export function replaceAll< + T extends string, + S extends string | RegExp, + R extends string = '', +>(sentence: T, lookup: S, replacement: R = '' as R) { + // Only supported in ES2021+ + if (typeof sentence.replaceAll === 'function') { + return sentence.replaceAll(lookup, replacement) as ReplaceAll + } + + const regex = new RegExp(lookup, 'g') + return sentence.replace(regex, replacement) as ReplaceAll +} diff --git a/src/native/replace.test.ts b/src/native/replace.test.ts new file mode 100644 index 0000000..d1f82c0 --- /dev/null +++ b/src/native/replace.test.ts @@ -0,0 +1,22 @@ +import { type Replace, replace } from './replace.js' + +namespace TypeTests { + type test = Expect< + Equal, 'some-nice string'> + > +} + +describe('replace', () => { + test('should replace chars in a string at both type level and runtime level once', () => { + const data = 'some nice string' + const result = replace(data, ' ') + expect(result).toEqual('somenice string') + type test = Expect> + }) + test('should replace chars but not at type level when using RegExp', () => { + const data = 'some nice string' + const result = replace(data, /nice /) + expect(result).toEqual('some string') + type test = Expect> + }) +}) diff --git a/src/native/replace.ts b/src/native/replace.ts new file mode 100644 index 0000000..eebf4c6 --- /dev/null +++ b/src/native/replace.ts @@ -0,0 +1,30 @@ +/** + * Replaces the first occurrence of a string with another string. + * sentence: The sentence to replace. + * lookup: The lookup string to be replaced. + * replacement: The replacement string. + */ +export type Replace< + sentence extends string, + lookup extends string | RegExp, + replacement extends string = '', +> = lookup extends string + ? sentence extends `${infer rest}${lookup}${infer rest2}` + ? `${rest}${replacement}${rest2}` + : sentence + : string +/** + * A strongly-typed version of `String.prototype.replace`. + * @param sentence the sentence to replace. + * @param lookup the lookup string to be replaced. + * @param replacement the replacement string. + * @returns the replaced string in both type level and runtime. + * @example replace('hello world', 'l', '1') // 'he1lo world' + */ +export function replace< + T extends string, + S extends string | RegExp, + R extends string = '', +>(sentence: T, lookup: S, replacement: R = '' as R) { + return sentence.replace(lookup, replacement) as Replace +} diff --git a/src/native/slice.test.ts b/src/native/slice.test.ts new file mode 100644 index 0000000..76f521c --- /dev/null +++ b/src/native/slice.test.ts @@ -0,0 +1,50 @@ +import { type Slice, slice } from './slice.js' + +namespace TypeTests { + type test = Expect, 'nice string'>> +} + +describe('slice', () => { + const str = 'The quick brown fox jumps over the lazy dog.' + test('should slice a string from a startIndex position', () => { + const result = slice(str, 31) + expect(result).toEqual('the lazy dog.') + type test = Expect> + }) + + test('should slice a string from a startIndex to an endIndex position', () => { + const result = slice(str, 4, 19) + expect(result).toEqual('quick brown fox') + type test = Expect> + }) + + test('should slice a string from the end with a negative startIndex', () => { + const result = slice(str, -4) + expect(result).toEqual('dog.') + type test = Expect> + }) + + test('should allow a negative endIndex', () => { + const result = slice(str, 0, -5) + expect(result).toEqual('The quick brown fox jumps over the lazy') + type test = Expect< + Equal + > + }) + + test('should slice a string from the end with a negative startIndex to a negative endIndex', () => { + const result = slice(str, -9, -5) + expect(result).toEqual('lazy') + type test = Expect> + }) + + test('should return an empty string if endIndex is lower than startIndex', () => { + const result = slice(str, -9, -10) + expect(result).toEqual('') + type test = Expect> + + const result2 = slice(str, 9, 1) + expect(result2).toEqual('') + type test2 = Expect> + }) +}) diff --git a/src/native/slice.ts b/src/native/slice.ts new file mode 100644 index 0000000..c8874da --- /dev/null +++ b/src/native/slice.ts @@ -0,0 +1,43 @@ +import type { Math } from '../internal/math.js' +import type { Length } from './length.js' + +/** + * Slices a string from a startIndex to an endIndex. + * T: The string to slice. + * startIndex: The start index. + * endIndex: The end index. + */ +export type Slice< + T extends string, + startIndex extends number = 0, + endIndex extends number = Length, +> = T extends `${infer head}${infer rest}` + ? startIndex extends 0 + ? endIndex extends 0 + ? '' + : `${head}${Slice< + rest, + Math.Subtract, 1>, + Math.Subtract, 1> + >}` + : `${Slice< + rest, + Math.Subtract, 1>, + Math.Subtract, 1> + >}` + : '' +/** + * A strongly-typed version of `String.prototype.slice`. + * @param str the string to slice. + * @param start the start index. + * @param end the end index. + * @returns the sliced string in both type level and runtime. + * @example slice('hello world', 6) // 'world' + */ +export function slice< + T extends string, + S extends number = 0, + E extends number = Length, +>(str: T, start: S = 0 as S, end: E = str.length as E) { + return str.slice(start, end) as Slice +} diff --git a/src/native/split.test.ts b/src/native/split.test.ts new file mode 100644 index 0000000..9c8efdb --- /dev/null +++ b/src/native/split.test.ts @@ -0,0 +1,27 @@ +import { type Split, split } from './split.js' + +namespace TypeTests { + type test = Expect< + Equal, ['some', 'nice', 'string']> + > +} + +describe('split', () => { + test('should split a string by a delimiter into an array of substrings', () => { + const data = 'some nice string' + const result = split(data, ' ') + expect(result).toEqual(['some', 'nice', 'string']) + type test = Expect> + }) + + test('should no add extra characters when splitting by empty string', () => { + const data = 'hello' + const result = split(data, '') + expect(result).toEqual(['h', 'e', 'l', 'l', 'o']) + type test = Expect> + + const result2 = split('', '') + expect(result2).toEqual([]) + type test2 = Expect> + }) +}) diff --git a/src/native/split.ts b/src/native/split.ts new file mode 100644 index 0000000..d894cde --- /dev/null +++ b/src/native/split.ts @@ -0,0 +1,26 @@ +/** + * Splits a string into an array of substrings. + * T: The string to split. + * delimiter: The delimiter. + */ +export type Split< + T, + delimiter extends string = '', +> = T extends `${infer first}${delimiter}${infer rest}` + ? [first, ...Split] + : T extends '' + ? [] + : [T] +/** + * A strongly-typed version of `String.prototype.split`. + * @param str the string to split. + * @param delimiter the delimiter. + * @returns the splitted string in both type level and runtime. + * @example split('hello world', ' ') // ['hello', 'world'] + */ +export function split( + str: T, + delimiter: D = '' as D, +) { + return str.split(delimiter) as Split +} diff --git a/src/native/starts-with.test.ts b/src/native/starts-with.test.ts new file mode 100644 index 0000000..fb2f218 --- /dev/null +++ b/src/native/starts-with.test.ts @@ -0,0 +1,48 @@ +import { type StartsWith, startsWith } from './starts-with.js' + +namespace TypeTests { + type test = Expect, true>> +} + +describe('startsWith', () => { + const text = 'abc' + + describe('without offset', () => { + test('should return true when text starts with search', () => { + const result = startsWith(text, 'a') + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when text does not start with search', () => { + const result = startsWith(text, 'b') + expect(result).toEqual(false) + type test = Expect> + }) + }) + + describe('with offset', () => { + test('should return true when offset text starts with search', () => { + const result = startsWith(text, 'b', 1) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when offset string does not start with search', () => { + const result = startsWith(text, 'a', 1) + expect(result).toEqual(false) + type test = Expect> + }) + }) + + describe('with bad offset', () => { + test('should return true when text starts with search and offset is negative', () => { + const result = startsWith(text, 'a', -1) + expect(result).toEqual(true) + type test = Expect> + }) + test('should return false when offset is greater than text length', () => { + const result = startsWith(text, 'a', 10) + expect(result).toEqual(false) + type test = Expect> + }) + }) +}) diff --git a/src/native/starts-with.ts b/src/native/starts-with.ts new file mode 100644 index 0000000..90d283a --- /dev/null +++ b/src/native/starts-with.ts @@ -0,0 +1,36 @@ +import type { Math } from '../internal/math.js' +import type { Slice } from './slice.js' + +/** + * Checks if a string starts with another string. + * T: The string to check. + * S: The string to check against. + * P: The position to start the search. + */ +export type StartsWith< + T extends string, + S extends string, + P extends number = 0, +> = Math.IsNegative

extends false + ? P extends 0 + ? T extends `${S}${string}` + ? true + : false + : StartsWith, S, 0> // P is >0, slice + : StartsWith // P is negative, ignore it + +/** + * A strongly-typed version of `String.prototype.startsWith`. + * @param text the string to search. + * @param search the string to search with. + * @param position the index to start search at. + * @returns boolean, whether or not the text string starts with the search string. + * @example startsWith('abc', 'a') // true + */ +export function startsWith< + T extends string, + S extends string, + P extends number = 0, +>(text: T, search: S, position = 0 as P) { + return text.startsWith(search, position) as StartsWith +} diff --git a/src/native/to-lower-case.test.ts b/src/native/to-lower-case.test.ts new file mode 100644 index 0000000..1497028 --- /dev/null +++ b/src/native/to-lower-case.test.ts @@ -0,0 +1,18 @@ +import { WEIRD_TEXT, SEPARATORS_TEXT } from '../internal/fixtures.js' +import { toLowerCase } from './to-lower-case.js' + +describe('toLowerCase', () => { + test('casing functions', () => { + const expected = + ' someweird-cased$*string1986foo [bar] w_for_wumbo...' as const + const result = toLowerCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toLowerCase(SEPARATORS_TEXT) + const expected = '[one] two-three/four.five(six){seven}|eight_nine\\ten' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/native/to-lower-case.ts b/src/native/to-lower-case.ts new file mode 100644 index 0000000..288c3a8 --- /dev/null +++ b/src/native/to-lower-case.ts @@ -0,0 +1,9 @@ +/** + * This function is a strongly-typed counterpart of String.prototype.toLowerCase. + * @param str the string to make lowercase. + * @returns the lowercased string. + * @example toLowerCase('HELLO WORLD') // 'hello world' + */ +export function toLowerCase(str: T) { + return str.toLowerCase() as Lowercase +} diff --git a/src/native/to-upper-case.test.ts b/src/native/to-upper-case.test.ts new file mode 100644 index 0000000..038436c --- /dev/null +++ b/src/native/to-upper-case.test.ts @@ -0,0 +1,18 @@ +import { WEIRD_TEXT, SEPARATORS_TEXT } from '../internal/fixtures.js' +import { toUpperCase } from './to-upper-case.js' + +describe('toUpperCase', () => { + test('casing functions', () => { + const expected = + ' SOMEWEIRD-CASED$*STRING1986FOO [BAR] W_FOR_WUMBO...' as const + const result = toUpperCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toUpperCase(SEPARATORS_TEXT) + const expected = '[ONE] TWO-THREE/FOUR.FIVE(SIX){SEVEN}|EIGHT_NINE\\TEN' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/native/to-upper-case.ts b/src/native/to-upper-case.ts new file mode 100644 index 0000000..8426f98 --- /dev/null +++ b/src/native/to-upper-case.ts @@ -0,0 +1,9 @@ +/** + * This function is a strongly-typed counterpart of String.prototype.toUpperCase. + * @param str the string to make uppercase. + * @returns the uppercased string. + * @example toUpperCase('hello world') // 'HELLO WORLD' + */ +export function toUpperCase(str: T) { + return str.toUpperCase() as Uppercase +} diff --git a/src/native/trim-end.test.ts b/src/native/trim-end.test.ts new file mode 100644 index 0000000..a360fa8 --- /dev/null +++ b/src/native/trim-end.test.ts @@ -0,0 +1,14 @@ +import { type TrimEnd, trimEnd } from './trim-end.js' + +namespace TypeTests { + type test = Expect, ' some nice string'>> +} + +describe('trimEnd', () => { + test('should trim the end of a string at both type level and runtime level', () => { + const data = ' some nice string ' + const result = trimEnd(data) + expect(result).toEqual(' some nice string') + type test = Expect> + }) +}) diff --git a/src/native/trim-end.ts b/src/native/trim-end.ts new file mode 100644 index 0000000..2db153a --- /dev/null +++ b/src/native/trim-end.ts @@ -0,0 +1,16 @@ +/** + * Trims all whitespaces at the end of a string. + * T: The string to trim. + */ +export type TrimEnd = T extends `${infer rest} ` + ? TrimEnd + : T +/** + * A strongly-typed version of `String.prototype.trimEnd`. + * @param str the string to trim. + * @returns the trimmed string in both type level and runtime. + * @example trimEnd(' hello world ') // ' hello world' + */ +export function trimEnd(str: T) { + return str.trimEnd() as TrimEnd +} diff --git a/src/native/trim-start.test.ts b/src/native/trim-start.test.ts new file mode 100644 index 0000000..ed75e69 --- /dev/null +++ b/src/native/trim-start.test.ts @@ -0,0 +1,16 @@ +import { type TrimStart, trimStart } from './trim-start.js' + +namespace TypeTests { + type test = Expect< + Equal, 'some nice string '> + > +} + +describe('trimStart', () => { + test('should trim the start of a string at both type level and runtime level', () => { + const data = ' some nice string ' + const result = trimStart(data) + expect(result).toEqual('some nice string ') + type test = Expect> + }) +}) diff --git a/src/native/trim-start.ts b/src/native/trim-start.ts new file mode 100644 index 0000000..2d7f6a3 --- /dev/null +++ b/src/native/trim-start.ts @@ -0,0 +1,16 @@ +/** + * Trims all whitespaces at the start of a string. + * T: The string to trim. + */ +export type TrimStart = T extends ` ${infer rest}` + ? TrimStart + : T +/** + * A strongly-typed version of `String.prototype.trimStart`. + * @param str the string to trim. + * @returns the trimmed string in both type level and runtime. + * @example trimStart(' hello world ') // 'hello world ' + */ +export function trimStart(str: T) { + return str.trimStart() as TrimStart +} diff --git a/src/native/trim.test.ts b/src/native/trim.test.ts new file mode 100644 index 0000000..ac0a568 --- /dev/null +++ b/src/native/trim.test.ts @@ -0,0 +1,14 @@ +import { type Trim, trim } from './trim.js' + +namespace TypeTests { + type test = Expect, 'some nice string'>> +} + +describe('trim', () => { + test('should trim a string at both type level and runtime level', () => { + const data = ' some nice string ' + const result = trim(data) + expect(result).toEqual('some nice string') + type test = Expect> + }) +}) diff --git a/src/native/trim.ts b/src/native/trim.ts new file mode 100644 index 0000000..d9d5b5e --- /dev/null +++ b/src/native/trim.ts @@ -0,0 +1,18 @@ +import type { TrimEnd } from './trim-end.js' +import type { TrimStart } from './trim-start.js' + +/** + * Trims all whitespaces at the start and end of a string. + * T: The string to trim. + */ +export type Trim = TrimEnd> + +/** + * A strongly-typed version of `String.prototype.trim`. + * @param str the string to trim. + * @returns the trimmed string in both type level and runtime. + * @example trim(' hello world ') // 'hello world' + */ +export function trim(str: T) { + return str.trim() as Trim +} diff --git a/src/primitives.test.ts b/src/primitives.test.ts deleted file mode 100644 index d2982df..0000000 --- a/src/primitives.test.ts +++ /dev/null @@ -1,426 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import * as subject from './primitives' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('primitives', () => { - describe('charAt', () => { - test('should get the character of a string at the given index in both type and runtime level', () => { - const data = 'some nice string' - const result = subject.charAt(data, 5) - expect(result).toEqual('n') - type test = Expect> - }) - }) - - describe('endsWith', () => { - const text = 'abc' - - describe('without offset', () => { - test('should return true when text ends with search', () => { - const result = subject.endsWith(text, 'c') - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when text does not end with search', () => { - const result = subject.endsWith(text, 'b') - expect(result).toEqual(false) - type test = Expect> - }) - }) - - describe('with offset', () => { - test('should return true when offset text ends with search', () => { - const result = subject.endsWith(text, 'b', 2) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return true when offset text ends with search (multi-char)', () => { - const result = subject.endsWith(text, 'bc', 3) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when offset string does not end with search', () => { - const result = subject.endsWith(text, 'c', 1) - expect(result).toEqual(false) - type test = Expect> - }) - }) - - describe('with bad offset', () => { - test('should return false when the offset is negative', () => { - const result = subject.endsWith(text, 'a', -1) - expect(result).toEqual(false) - type test = Expect> - }) - test('should return true when the end matches and offset is greater than text length', () => { - const result = subject.endsWith(text, 'c', 10) - expect(result).toEqual(true) - type test = Expect> - }) - }) - }) - - describe('includes', () => { - const text = 'abcde' - - describe('without offset', () => { - test('should return true when text contains search', () => { - const result = subject.includes(text, 'bcd') - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when text does not end with search', () => { - const result = subject.includes(text, 'hello') - expect(result).toEqual(false) - type test = Expect> - }) - }) - - describe('with offset', () => { - test('should return true when offset text does contain search', () => { - const result = subject.includes(text, 'c', 1) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return true when offset text does contain search (multi-char)', () => { - const result = subject.includes(text, 'bcd', 1) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when offset string does not contain search', () => { - const result = subject.includes(text, 'abc', 3) - expect(result).toEqual(false) - type test = Expect> - }) - }) - - describe('with bad offset', () => { - test('should ignore offset when the offset is negative', () => { - const result = subject.includes(text, 'a', -100) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when text contains search but offset is greater than text length', () => { - const result = subject.includes(text, 'c', 10) - expect(result).toEqual(false) - type test = Expect> - }) - }) - }) - - describe('join', () => { - test('should join words in both type level and runtime level', () => { - const result = subject.join(['a', 'b', 'c'], '-') - expect(result).toEqual('a-b-c') - type test = Expect> - }) - - test('should join only at runtime level when type is wide', () => { - const data = ['a', 'b', 'c'] - const result = subject.join(data, '-') - expect(result).toEqual('a-b-c') - type test = Expect> - }) - }) - - describe('length', () => { - test('should return the lenght of a string at both type level and runtime level', () => { - const data = 'some nice string' - const result = subject.length(data) - expect(result).toEqual(16) - type test = Expect> - }) - }) - - describe('padEnd', () => { - test('should pad a string at the end', () => { - const data = 'hello' - const result = subject.padEnd(data, 10) - expect(result).toEqual('hello ') - type test = Expect> - }) - - test('should pad with a given string', () => { - const data = 'hello' - const result = subject.padEnd(data, 10, '=>') - expect(result).toEqual('hello=>=>=') - type test = Expect=>='>> - }) - - test('should not pad if no arguments are given', () => { - const data = 'hello' - const result = subject.padEnd(data) - expect(result).toEqual('hello') - type test = Expect> - }) - - test('should not pad or truncate if length is shorter than string', () => { - const data = 'hello' - const result = subject.padEnd(data, 3, '=') - expect(result).toEqual('hello') - type test = Expect> - }) - - test('should not pad for negative numbers', () => { - const data = 'hello' - const result = subject.padEnd(data, -1, '=') - expect(result).toEqual('hello') - type test = Expect> - }) - }) - - describe('padStart', () => { - test('should pad a string at the start', () => { - const data = 'hello' - const result = subject.padStart(data, 10) - expect(result).toEqual(' hello') - type test = Expect> - }) - - test('should pad with a given string', () => { - const data = 'hello' - const result = subject.padStart(data, 10, '=>') - expect(result).toEqual('=>=>=hello') - type test = Expect=>=hello'>> - }) - - test('should not pad if no arguments are given', () => { - const data = 'hello' - const result = subject.padStart(data) - expect(result).toEqual('hello') - type test = Expect> - }) - - test('should not pad or truncate if length is shorter than string', () => { - const data = 'hello' - const result = subject.padStart(data, 3, '=') - expect(result).toEqual('hello') - type test = Expect> - }) - - test('should not pad for negative numbers', () => { - const data = 'hello' - const result = subject.padStart(data, -1, '=') - expect(result).toEqual('hello') - type test = Expect> - }) - }) - - describe('repeat', () => { - test('should repeat the string by a given number of times', () => { - const data = 'abc' - const result = subject.repeat(data, 3) - expect(result).toEqual('abcabcabc') - type test = Expect> - }) - - test('should be empty when repeating 0 times', () => { - const data = 'abc' - const result = subject.repeat(data) - expect(result).toEqual('') - type test = Expect> - }) - - test('should throw when trying to repeat with negative number', () => { - const data = 'abc' - expect(() => subject.repeat(data, -1)).toThrow() - type test = Expect, never>> - }) - }) - - describe('replace', () => { - test('should replace chars in a string at both type level and runtime level once', () => { - const data = 'some nice string' - const result = subject.replace(data, ' ') - expect(result).toEqual('somenice string') - type test = Expect> - }) - test('should replace chars but not at type level when using RegExp', () => { - const data = 'some nice string' - const result = subject.replace(data, /nice /) - expect(result).toEqual('some string') - type test = Expect> - }) - }) - - describe('replaceAll', () => { - test('should replace all chars in a string at both type level and runtime level once', () => { - const data = 'some nice string' - const result = subject.replaceAll(data, ' ') - expect(result).toEqual('somenicestring') - type test = Expect> - }) - - test('accepts an argument for the replacement', () => { - const data = 'some nice string' - const result = subject.replaceAll(data, ' ', '@') - expect(result).toEqual('some@nice@string') - type test = Expect> - }) - - test('should replace chars but not at type level when using RegExp', () => { - const data = 'some nice string' - const result = subject.replaceAll(data, / /g, '-') - expect(result).toEqual('some-nice-string') - // Note: `string` instead of `some-nice-string` - type test = Expect> - }) - }) - - describe('replaceAll polyfill', () => { - const replaceAll = String.prototype.replaceAll - beforeAll(() => { - // @ts-ignore - String.prototype.replaceAll = undefined - }) - - afterAll(() => { - String.prototype.replaceAll = replaceAll - }) - test('it works through a polyfill', () => { - const spy = vi.spyOn(String.prototype, 'replace') - const data = 'some nice string' - const result = subject.replaceAll(data, ' ', '@') - expect(result).toEqual('some@nice@string') - expect(spy).toHaveBeenCalledWith(/ /g, '@') - }) - }) - - describe('slice', () => { - const str = 'The quick brown fox jumps over the lazy dog.' - test('should slice a string from a startIndex position', () => { - const result = subject.slice(str, 31) - expect(result).toEqual('the lazy dog.') - type test = Expect> - }) - - test('should slice a string from a startIndex to an endIndex position', () => { - const result = subject.slice(str, 4, 19) - expect(result).toEqual('quick brown fox') - type test = Expect> - }) - - test('should slice a string from the end with a negative startIndex', () => { - const result = subject.slice(str, -4) - expect(result).toEqual('dog.') - type test = Expect> - }) - - test('should allow a negative endIndex', () => { - const result = subject.slice(str, 0, -5) - expect(result).toEqual('The quick brown fox jumps over the lazy') - type test = Expect< - Equal - > - }) - - test('should slice a string from the end with a negative startIndex to a negative endIndex', () => { - const result = subject.slice(str, -9, -5) - expect(result).toEqual('lazy') - type test = Expect> - }) - - test('should return an empty string if endIndex is lower than startIndex', () => { - const result = subject.slice(str, -9, -10) - expect(result).toEqual('') - type test = Expect> - - const result2 = subject.slice(str, 9, 1) - expect(result2).toEqual('') - type test2 = Expect> - }) - }) - - describe('split', () => { - test('should split a string by a delimiter into an array of substrings', () => { - const data = 'some nice string' - const result = subject.split(data, ' ') - expect(result).toEqual(['some', 'nice', 'string']) - type test = Expect> - }) - - test('should no add extra characters when splitting by empty string', () => { - const data = 'hello' - const result = subject.split(data, '') - expect(result).toEqual(['h', 'e', 'l', 'l', 'o']) - type test = Expect> - - const result2 = subject.split('', '') - expect(result2).toEqual([]) - type test2 = Expect> - }) - }) - - describe('startsWith', () => { - const text = 'abc' - - describe('without offset', () => { - test('should return true when text starts with search', () => { - const result = subject.startsWith(text, 'a') - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when text does not start with search', () => { - const result = subject.startsWith(text, 'b') - expect(result).toEqual(false) - type test = Expect> - }) - }) - - describe('with offset', () => { - test('should return true when offset text starts with search', () => { - const result = subject.startsWith(text, 'b', 1) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when offset string does not start with search', () => { - const result = subject.startsWith(text, 'a', 1) - expect(result).toEqual(false) - type test = Expect> - }) - }) - - describe('with bad offset', () => { - test('should return true when text starts with search and offset is negative', () => { - const result = subject.startsWith(text, 'a', -1) - expect(result).toEqual(true) - type test = Expect> - }) - test('should return false when offset is greater than text length', () => { - const result = subject.startsWith(text, 'a', 10) - expect(result).toEqual(false) - type test = Expect> - }) - }) - }) - - describe('trimStart', () => { - test('should trim the start of a string at both type level and runtime level', () => { - const data = ' some nice string ' - const result = subject.trimStart(data) - expect(result).toEqual('some nice string ') - type test = Expect> - }) - }) - - describe('trimEnd', () => { - test('should trim the end of a string at both type level and runtime level', () => { - const data = ' some nice string ' - const result = subject.trimEnd(data) - expect(result).toEqual(' some nice string') - type test = Expect> - }) - }) - - describe('trim', () => { - test('should trim a string at both type level and runtime level', () => { - const data = ' some nice string ' - const result = subject.trim(data) - expect(result).toEqual('some nice string') - type test = Expect> - }) - }) -}) diff --git a/src/primitives.ts b/src/primitives.ts deleted file mode 100644 index 9094a4c..0000000 --- a/src/primitives.ts +++ /dev/null @@ -1,497 +0,0 @@ -import type { Math } from './math' -import type { TupleOf } from './internals' - -/** - * Gets the character at the given index. - * T: The string to get the character from. - * index: The index of the character. - */ -type CharAt = Split[index] -/** - * A strongly-typed version of `String.prototype.charAt`. - * @param str the string to get the character from. - * @param index the index of the character. - * @returns the character in both type level and runtime. - * @example charAt('hello world', 6) // 'w' - */ -function charAt( - str: T, - index: I, -): CharAt { - return str.charAt(index) -} - -/** - * Concatenates a tuple of strings. - * T: The tuple of strings to concatenate. - */ -type Concat = Join - -/** - * A strongly-typed version of `String.prototype.concat`. - * @param strings the tuple of strings to concatenate. - * @returns the concatenated string in both type level and runtime. - * @example concat('a', 'bc', 'def') // 'abcdef' - */ -function concat(...strings: T): Concat { - return join(strings) -} - -/** - * Checks if a string ends with another string. - * T: The string to check. - * S: The string to check against. - * P: The position the search should end. - */ -type EndsWith< - T extends string, - S extends string, - P extends number = Length, -> = Math.IsNegative

extends false - ? P extends Length - ? S extends Slice, Length>, Length> - ? true - : false - : EndsWith, S, Length> // P !== T.length, slice - : false // P is negative, false - -/** - * A strongly-typed version of `String.prototype.endsWith`. - * @param text the string to search. - * @param search the string to search with. - * @param position the index the search should end at. - * @returns boolean, whether or not the text string ends with the search string. - * @example endsWith('abc', 'c') // true - */ -function endsWith< - T extends string, - S extends string, - P extends number = Length, ->(text: T, search: S, position = text.length as P) { - return text.endsWith(search, position) as EndsWith -} - -/** - * Joins a tuple of strings with the given delimiter. - * T: The tuple of strings to join. - * delimiter: The delimiter. - */ -type Join< - T extends readonly string[], - delimiter extends string = '', -> = string[] extends T - ? string // Avoid spending resources on a wide type - : T extends readonly [ - infer first extends string, - ...infer rest extends string[], - ] - ? rest extends [] - ? first - : `${first}${delimiter}${Join}` - : '' - -/** - * A strongly-typed version of `Array.prototype.join`. - * @param tuple the tuple of strings to join. - * @param delimiter the delimiter. - * @returns the joined string in both type level and runtime. - * @example join(['hello', 'world'], '-') // 'hello-world' - */ -function join( - tuple: T, - delimiter: D = '' as D, -) { - return tuple.join(delimiter) as Join -} - -/** - * Gets the length of a string. - */ -type Length = Split['length'] -/** - * A strongly-typed version of `String.prototype.length`. - * @param str the string to get the length from. - * @returns the length of the string in both type level and runtime. - * @example length('hello world') // 11 - */ -function length(str: T) { - return str.length as Length -} - -/** - * Pads a string at the end with another string. - * T: The string to pad. - * times: The number of times to pad. - * pad: The string to pad with. - */ -type PadEnd< - T extends string, - times extends number = 0, - pad extends string = ' ', -> = Math.IsNegative extends false - ? Math.Subtract> extends infer missing extends number - ? `${T}${Slice, 0, missing>}` - : never - : T -/** - * A strongly-typed version of `String.prototype.padEnd`. - * @param str the string to pad. - * @param length the length to pad. - * @param pad the string to pad with. - * @returns the padded string in both type level and runtime. - * @example padEnd('hello', 10, '=') // 'hello=====' - */ -function padEnd( - str: T, - length: N = 0 as N, - pad: U = ' ' as U, -) { - return str.padEnd(length, pad) as PadEnd -} - -/** - * Pads a string at the start with another string. - * T: The string to pad. - * times: The number of times to pad. - * pad: The string to pad with. - */ -type PadStart< - T extends string, - times extends number = 0, - pad extends string = ' ', -> = Math.IsNegative extends false - ? Math.Subtract> extends infer missing extends number - ? `${Slice, 0, missing>}${T}` - : never - : T -/** - * A strongly-typed version of `String.prototype.padStart`. - * @param str the string to pad. - * @param length the length to pad. - * @param pad the string to pad with. - * @returns the padded string in both type level and runtime. - * @example padStart('hello', 10, '=') // '=====hello' - */ -function padStart< - T extends string, - N extends number = 0, - U extends string = ' ', ->(str: T, length: N = 0 as N, pad: U = ' ' as U) { - return str.padStart(length, pad) as PadStart -} - -/** - * Repeats a string N times. - * T: The string to repeat. - * N: The number of times to repeat. - */ -type Repeat = times extends 0 - ? '' - : Math.IsNegative extends false - ? Join> - : never -/** - * A strongly-typed version of `String.prototype.repeat`. - * @param str the string to repeat. - * @param times the number of times to repeat. - * @returns the repeated string in both type level and runtime. - * @example repeat('hello', 3) // 'hellohellohello' - */ -function repeat( - str: T, - times: N = 0 as N, -) { - return str.repeat(times) as Repeat -} - -/** - * Replaces the first occurrence of a string with another string. - * sentence: The sentence to replace. - * lookup: The lookup string to be replaced. - * replacement: The replacement string. - */ -type Replace< - sentence extends string, - lookup extends string | RegExp, - replacement extends string = '', -> = lookup extends string - ? sentence extends `${infer rest}${lookup}${infer rest2}` - ? `${rest}${replacement}${rest2}` - : sentence - : string -/** - * A strongly-typed version of `String.prototype.replace`. - * @param sentence the sentence to replace. - * @param lookup the lookup string to be replaced. - * @param replacement the replacement string. - * @returns the replaced string in both type level and runtime. - * @example replace('hello world', 'l', '1') // 'he1lo world' - */ -function replace< - T extends string, - S extends string | RegExp, - R extends string = '', ->(sentence: T, lookup: S, replacement: R = '' as R) { - return sentence.replace(lookup, replacement) as Replace -} - -/** - * Replaces all the occurrences of a string with another string. - * sentence: The sentence to replace. - * lookup: The lookup string to be replaced. - * replacement: The replacement string. - */ -type ReplaceAll< - sentence extends string, - lookup extends string | RegExp, - replacement extends string = '', -> = lookup extends string - ? sentence extends `${infer rest}${lookup}${infer rest2}` - ? `${rest}${replacement}${ReplaceAll}` - : sentence - : string - -/** - * A strongly-typed version of `String.prototype.replaceAll`. - * @param sentence the sentence to replace. - * @param lookup the lookup string to be replaced. - * @param replacement the replacement string. - * @returns the replaced string in both type level and runtime. - * @example replaceAll('hello world', 'l', '1') // 'he11o wor1d' - */ -function replaceAll< - T extends string, - S extends string | RegExp, - R extends string = '', ->(sentence: T, lookup: S, replacement: R = '' as R) { - // Only supported in ES2021+ - if (typeof sentence.replaceAll === 'function') { - return sentence.replaceAll(lookup, replacement) as ReplaceAll - } - - const regex = new RegExp(lookup, 'g') - return sentence.replace(regex, replacement) as ReplaceAll -} - -/** - * Slices a string from a startIndex to an endIndex. - * T: The string to slice. - * startIndex: The start index. - * endIndex: The end index. - */ -type Slice< - T extends string, - startIndex extends number = 0, - endIndex extends number = Length, -> = T extends `${infer head}${infer rest}` - ? startIndex extends 0 - ? endIndex extends 0 - ? '' - : `${head}${Slice< - rest, - Math.Subtract, 1>, - Math.Subtract, 1> - >}` - : `${Slice< - rest, - Math.Subtract, 1>, - Math.Subtract, 1> - >}` - : '' -/** - * A strongly-typed version of `String.prototype.slice`. - * @param str the string to slice. - * @param start the start index. - * @param end the end index. - * @returns the sliced string in both type level and runtime. - * @example slice('hello world', 6) // 'world' - */ -function slice< - T extends string, - S extends number = 0, - E extends number = Length, ->(str: T, start: S = 0 as S, end: E = str.length as E) { - return str.slice(start, end) as Slice -} - -/** - * Splits a string into an array of substrings. - * T: The string to split. - * delimiter: The delimiter. - */ -type Split< - T, - delimiter extends string = '', -> = T extends `${infer first}${delimiter}${infer rest}` - ? [first, ...Split] - : T extends '' - ? [] - : [T] -/** - * A strongly-typed version of `String.prototype.split`. - * @param str the string to split. - * @param delimiter the delimiter. - * @returns the splitted string in both type level and runtime. - * @example split('hello world', ' ') // ['hello', 'world'] - */ -function split( - str: T, - delimiter: D = '' as D, -) { - return str.split(delimiter) as Split -} - -/** - * Checks if a string starts with another string. - * T: The string to check. - * S: The string to check against. - * P: The position to start the search. - */ -type StartsWith< - T extends string, - S extends string, - P extends number = 0, -> = Math.IsNegative

extends false - ? P extends 0 - ? T extends `${S}${string}` - ? true - : false - : StartsWith, S, 0> // P is >0, slice - : StartsWith // P is negative, ignore it - -/** - * A strongly-typed version of `String.prototype.startsWith`. - * @param text the string to search. - * @param search the string to search with. - * @param position the index to start search at. - * @returns boolean, whether or not the text string starts with the search string. - * @example startsWith('abc', 'a') // true - */ -function startsWith( - text: T, - search: S, - position = 0 as P, -) { - return text.startsWith(search, position) as StartsWith -} - -/** - * Checks if a string includes another string. - * T: The string to check. - * S: The string to check against. - * P: The position to start the search. - */ -type Includes< - T extends string, - S extends string, - P extends number = 0, -> = Math.IsNegative

extends false - ? P extends 0 - ? T extends `${string}${S}${string}` - ? true - : false - : Includes, S, 0> // P is >0, slice - : Includes // P is negative, ignore it - -/** - * A strongly-typed version of `String.prototype.includes`. - * @param text the string to search - * @param search the string to search with - * @param position the index to start search at - * @returns boolean, whether or not the text contains the search string. - * @example includes('abcde', 'bcd') // true - */ -function includes( - text: T, - search: S, - position = 0 as P, -) { - return text.includes(search, position) as Includes -} - -/** - * Trims all whitespaces at the start of a string. - * T: The string to trim. - */ -type TrimStart = T extends ` ${infer rest}` - ? TrimStart - : T -/** - * A strongly-typed version of `String.prototype.trimStart`. - * @param str the string to trim. - * @returns the trimmed string in both type level and runtime. - * @example trimStart(' hello world ') // 'hello world ' - */ -function trimStart(str: T) { - return str.trimStart() as TrimStart -} - -/** - * Trims all whitespaces at the end of a string. - * T: The string to trim. - */ -type TrimEnd = T extends `${infer rest} ` ? TrimEnd : T -/** - * A strongly-typed version of `String.prototype.trimEnd`. - * @param str the string to trim. - * @returns the trimmed string in both type level and runtime. - * @example trimEnd(' hello world ') // ' hello world' - */ -function trimEnd(str: T) { - return str.trimEnd() as TrimEnd -} - -/** - * Trims all whitespaces at the start and end of a string. - * T: The string to trim. - */ -type Trim = TrimEnd> - -/** - * A strongly-typed version of `String.prototype.trim`. - * @param str the string to trim. - * @returns the trimmed string in both type level and runtime. - * @example trim(' hello world ') // 'hello world' - */ -function trim(str: T) { - return str.trim() as Trim -} - -export type { - CharAt, - Concat, - EndsWith, - Includes, - Join, - Length, - PadEnd, - PadStart, - Repeat, - Replace, - ReplaceAll, - Slice, - Split, - StartsWith, - TrimStart, - TrimEnd, - Trim, -} -export { - charAt, - concat, - endsWith, - includes, - join, - length, - padEnd, - padStart, - repeat, - replace, - replaceAll, - slice, - split, - startsWith, - trim, - trimStart, - trimEnd, -} diff --git a/src/primitives.types.test.ts b/src/primitives.types.test.ts deleted file mode 100644 index eebbe15..0000000 --- a/src/primitives.types.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type * as Subject from './primitives' - -namespace TypeTests { - type test1 = Expect< - Equal, 'some nice string'> - > - type test2 = Expect< - Equal, 'some-nice string'> - > - type test3 = Expect< - Equal, 'some-nice-string'> - > - type test4 = Expect< - Equal, 'some nice string '> - > - type test5 = Expect< - Equal, ' some nice string'> - > - type test6 = Expect< - Equal, 'some nice string'> - > - type test7 = Expect< - Equal, ['some', 'nice', 'string']> - > - type test8 = Expect, 'n'>> - type test9 = Expect< - Equal, 'nice string'> - > - type test10 = Expect, 16>> - - type test11 = Expect< - Equal< - Subject.Concat<['a', 'bc', 'def'] | ['1', '23', '456']>, - 'abcdef' | '123456' - > - > - - type test12 = Expect, true>> - type test13 = Expect, true>> - type test14 = Expect, true>> -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/separators.test.ts b/src/separators.test.ts deleted file mode 100644 index dceefab..0000000 --- a/src/separators.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as subject from './separators' - -describe('SEPARATOR_REGEX', () => { - test('dummy regex test', () => { - expect(subject.SEPARATOR_REGEX.test('[test]')).toEqual(true) - expect(subject.SEPARATOR_REGEX.test('te.st')).toEqual(true) - expect(subject.SEPARATOR_REGEX.test('te$st')).toEqual(false) - expect(subject.SEPARATOR_REGEX.test('test')).toEqual(false) - }) -}) diff --git a/src/separators.types.test.ts b/src/separators.types.test.ts deleted file mode 100644 index 0e9dd24..0000000 --- a/src/separators.types.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type * as Subject from './separators' - -namespace TypeChecks { - type test1 = Expect, false>> - type test2 = Expect, false>> - type test3 = Expect, false>> - type test4 = Expect, false>> - type test5 = Expect, true>> - type test6 = Expect, true>> - type test7 = Expect, true>> - type test8 = Expect, true>> - type test9 = Expect, true>> -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/utils.test.ts b/src/utils.test.ts deleted file mode 100644 index 67e9e0b..0000000 --- a/src/utils.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as subject from './utils' - -type Mutable = { - -readonly [Key in keyof Type]: Type[Key] -} - -describe('words', () => { - test('it splits words at separators', () => { - const expected = [ - 'one', - 'two', - 'three', - 'four', - 'five', - 'six', - 'seven', - 'eight', - 'nine', - 'ten', - ] as const - const result = subject.words( - '[one] two-three/four.five(six){seven}|eight_nine\\ten', - ) - expect(result).toEqual(expected) - type test = Expect>> - }) - - test('it splits words at digits', () => { - const expected = ['2', 'Weird', 'Cased', '1986', 'Foo'] as const - const result = subject.words('2WeirdCased1986Foo') - expect(result).toEqual(expected) - type test = Expect>> - }) - - test('it splits words at special chars', () => { - const expected = ['$', '2', 'Weird', 'Cased', '@@', 'Foo'] as const - const result = subject.words('$2WeirdCased@@Foo') - expect(result).toEqual(expected) - type test = Expect>> - }) - - test('it splits words at casing', () => { - const expected = ['some', 'Weird', 'Cased', 'STRING', 'Foo'] as const - const result = subject.words('someWeirdCasedSTRINGFoo') - expect(result).toEqual(expected) - type test = Expect>> - }) - - test('it combines all of the rules above and trims the word', () => { - const expected = [ - 'some', - 'Weird', - 'cased', - '$*', - 'String', - '1986', - 'Foo', - 'Bar', - ] as const - const result = subject.words(' someWeird-cased$*String1986Foo Bar ') - expect(result).toEqual(expected) - type test = Expect>> - }) -}) - -describe('truncate', () => { - test('truncate small sentence does nothing', () => { - const expected = 'Hello' as const - const result = subject.truncate('Hello', 9) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('truncate big sentence truncate', () => { - const expected = 'Hello ...' as const - const result = subject.truncate('Hello world', 9) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('truncate with negative integer does truncate', () => { - const expected = '...' as const - const result = subject.truncate('Hello world', -1) - expect(result).toEqual(expected) - type test = Expect> - }) - - test('truncate big sentence with specified omission', () => { - const expected = 'Hello[...]' as const - const result = subject.truncate('Hello world', 10, '[...]') - expect(result).toEqual(expected) - type test = Expect> - }) - - test('truncate small sentence with specified omission', () => { - const expected = 'Hello' as const - const result = subject.truncate('Hello', 10, '[...]') - expect(result).toEqual(expected) - type test = Expect> - }) -}) diff --git a/src/utils.types.test.ts b/src/utils.types.test.ts deleted file mode 100644 index dde8f76..0000000 --- a/src/utils.types.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type * as Subject from './utils' - -namespace WordsTests { - type test1 = Expect< - Equal< - Subject.Words<' someWeird-cased$*String1986Foo Bar obj.items[0]'>, - [ - 'some', - 'Weird', - 'cased', - '$*', - 'String', - '1986', - 'Foo', - 'Bar', - 'obj', - 'items', - '0', - ] - > - > -} - -namespace TruncateTests { - type test1 = Expect, 'Hello,...'>> - type test2 = Expect< - Equal, 'Hello, world'> - > - type test3 = Expect, '...'>> - type test4 = Expect< - Equal, 'Hell[...]'> - > - type test5 = Expect, '...'>> - type test6 = Expect< - Equal, '[...]'> - > -} - -test('dummy test', () => expect(true).toBe(true)) diff --git a/src/utils/capitalize.test.ts b/src/utils/capitalize.test.ts new file mode 100644 index 0000000..7c7cdaf --- /dev/null +++ b/src/utils/capitalize.test.ts @@ -0,0 +1,18 @@ +import { capitalize } from './capitalize.js' +import { WEIRD_TEXT } from '../internal/fixtures.js' + +describe('capitalize', () => { + test('it does nothing with a string that has no char at the beginning', () => { + const expected = WEIRD_TEXT + const result = capitalize(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('it capitalizes the first char of a string', () => { + const expected = 'SomeWeird-casedString' as const + const result = capitalize('someWeird-casedString') + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/capitalize.ts b/src/utils/capitalize.ts new file mode 100644 index 0000000..9f6dac6 --- /dev/null +++ b/src/utils/capitalize.ts @@ -0,0 +1,14 @@ +import { charAt } from '../native/char-at.js' +import { join } from '../native/join.js' +import { slice } from '../native/slice.js' +import { toUpperCase } from '../native/to-upper-case.js' + +/** + * Capitalizes the first letter of a string. This is a runtime counterpart of `Capitalize` from `src/types.d.ts`. + * @param str the string to capitalize. + * @returns the capitalized string. + * @example capitalize('hello world') // 'Hello world' + */ +export function capitalize(str: T): Capitalize { + return join([toUpperCase(charAt(str, 0)), slice(str, 1)]) +} diff --git a/src/utils/characters/letters.test.ts b/src/utils/characters/letters.test.ts new file mode 100644 index 0000000..2a377ef --- /dev/null +++ b/src/utils/characters/letters.test.ts @@ -0,0 +1,20 @@ +import type { IsLower, IsUpper, IsLetter } from './letters.js' + +namespace TypeChecks { + type test1 = Expect, false>> + type test2 = Expect, true>> + type test3 = Expect, false>> + type test4 = Expect, false>> + + type test5 = Expect, false>> + type test6 = Expect, false>> + type test7 = Expect, true>> + type test8 = Expect, false>> + + type test9 = Expect, false>> + type test10 = Expect, true>> + type test11 = Expect, true>> + type test12 = Expect, false>> +} + +test('dummy test', () => expect(true).toBe(true)) diff --git a/src/utils/characters/letters.ts b/src/utils/characters/letters.ts new file mode 100644 index 0000000..e57461a --- /dev/null +++ b/src/utils/characters/letters.ts @@ -0,0 +1,23 @@ +// prettier-ignore +type UpperChars = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' +type LowerChars = Lowercase + +// UTILITIES FOR DETECTING CHARS +/** + * Checks if the given character is an upper case letter. + */ +export type IsUpper = T extends UpperChars ? true : false + +/** + * Checks if the given character is a lower case letter. + */ +export type IsLower = T extends LowerChars ? true : false + +/** + * Checks if the given character is a letter. + */ +export type IsLetter = IsUpper extends true + ? true + : IsLower extends true + ? true + : false diff --git a/src/utils/characters/numbers.test.ts b/src/utils/characters/numbers.test.ts new file mode 100644 index 0000000..2067481 --- /dev/null +++ b/src/utils/characters/numbers.test.ts @@ -0,0 +1,10 @@ +import type { IsDigit } from './numbers.js' + +namespace TypeChecks { + type test1 = Expect, true>> + type test2 = Expect, false>> + type test3 = Expect, false>> + type test4 = Expect, false>> +} + +test('dummy test', () => expect(true).toBe(true)) diff --git a/src/utils/characters/numbers.ts b/src/utils/characters/numbers.ts new file mode 100644 index 0000000..55c9c75 --- /dev/null +++ b/src/utils/characters/numbers.ts @@ -0,0 +1,6 @@ +export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + +/** + * Checks if the given character is a number. + */ +export type IsDigit = T extends Digit ? true : false diff --git a/src/utils/characters/separators.test.ts b/src/utils/characters/separators.test.ts new file mode 100644 index 0000000..2d83fe9 --- /dev/null +++ b/src/utils/characters/separators.test.ts @@ -0,0 +1,23 @@ +import { SEPARATOR_REGEX } from './separators.js' +import type { IsSeparator } from './separators.js' + +namespace TypeChecks { + type test1 = Expect, false>> + type test2 = Expect, false>> + type test3 = Expect, false>> + type test4 = Expect, false>> + type test5 = Expect, true>> + type test6 = Expect, true>> + type test7 = Expect, true>> + type test8 = Expect, true>> + type test9 = Expect, true>> +} + +describe('SEPARATOR_REGEX', () => { + test('dummy regex test', () => { + expect(SEPARATOR_REGEX.test('[test]')).toEqual(true) + expect(SEPARATOR_REGEX.test('te.st')).toEqual(true) + expect(SEPARATOR_REGEX.test('te$st')).toEqual(false) + expect(SEPARATOR_REGEX.test('test')).toEqual(false) + }) +}) diff --git a/src/separators.ts b/src/utils/characters/separators.ts similarity index 71% rename from src/separators.ts rename to src/utils/characters/separators.ts index f1d1704..15f81a0 100644 --- a/src/separators.ts +++ b/src/utils/characters/separators.ts @@ -19,18 +19,15 @@ function escapeChar(char: string): string { : char } -const SEPARATOR_REGEX = new RegExp( +export const SEPARATOR_REGEX = new RegExp( `[${SEPARATORS.map(escapeChar).join('')}]`, 'g', ) -type Separator = (typeof SEPARATORS)[number] +export type Separator = (typeof SEPARATORS)[number] /** * Checks if the given character is a separator. * E.g. space, underscore, dash, dot, slash. */ -type IsSeparator = T extends Separator ? true : false - -export type { IsSeparator, Separator } -export { SEPARATOR_REGEX } +export type IsSeparator = T extends Separator ? true : false diff --git a/src/utils/characters/special.test.ts b/src/utils/characters/special.test.ts new file mode 100644 index 0000000..1921ca5 --- /dev/null +++ b/src/utils/characters/special.test.ts @@ -0,0 +1,12 @@ +import type * as Subject from './special.js' + +namespace TypeChecks { + type test1 = Expect, false>> + type test2 = Expect, false>> + type test3 = Expect, false>> + type test4 = Expect, true>> + type test5 = Expect, false>> + type test6 = Expect, true>> + type test7 = Expect, false>> +} +test('dummy test', () => expect(true).toBe(true)) diff --git a/src/utils/characters/special.ts b/src/utils/characters/special.ts new file mode 100644 index 0000000..2ae7db8 --- /dev/null +++ b/src/utils/characters/special.ts @@ -0,0 +1,15 @@ +import type { IsSeparator } from './separators.js' +import type { IsLetter } from './letters.js' +import type { IsDigit } from './numbers.js' + +/** + * Checks if the given character is a special character. + * E.g. not a letter, number, or separator. + */ +export type IsSpecial = IsLetter extends true + ? false + : IsDigit extends true + ? false + : IsSeparator extends true + ? false + : true diff --git a/src/utils/object-keys/camel-keys.test.ts b/src/utils/object-keys/camel-keys.test.ts new file mode 100644 index 0000000..a835a00 --- /dev/null +++ b/src/utils/object-keys/camel-keys.test.ts @@ -0,0 +1,26 @@ +import { type CamelKeys, camelKeys } from './camel-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + CamelKeys<{ + 'some-value': { 'deep-nested': true } + 'other-value': true + }>, + { someValue: { 'deep-nested': true }; otherValue: true } + > + > +} + +test('camelKeys', () => { + const expected = { + some: { 'deep-nested': { value: true } }, + otherValue: true, + } + const result = camelKeys({ + some: { 'deep-nested': { value: true } }, + 'other-value': true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/camel-keys.ts b/src/utils/object-keys/camel-keys.ts new file mode 100644 index 0000000..9b3dd7f --- /dev/null +++ b/src/utils/object-keys/camel-keys.ts @@ -0,0 +1,19 @@ +import { transformKeys } from './transform-keys.js' +import { type CamelCase, toCamelCase } from '../word-case/to-camel-case.js' + +/** + * Shallowly transforms the keys of an Record to camelCase. + * T: the type of the Record to transform. + */ +export type CamelKeys = T extends [] + ? T + : { [K in keyof T as CamelCase>]: T[K] } +/** + * A strongly typed function that shallowly transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example camelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { 'fizz-buz': true } } + */ +export function camelKeys(obj: T): CamelKeys { + return transformKeys(obj, toCamelCase) as never +} diff --git a/src/utils/object-keys/constant-keys.test.ts b/src/utils/object-keys/constant-keys.test.ts new file mode 100644 index 0000000..66c3cd3 --- /dev/null +++ b/src/utils/object-keys/constant-keys.test.ts @@ -0,0 +1,35 @@ +import { type ConstantKeys, constantKeys } from './constant-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + ConstantKeys<{ + someValue: { deepNested: true } + otherValue: true + }>, + { SOME_VALUE: { deepNested: true }; OTHER_VALUE: true } + > + > +} + +describe('constantKeys', () => { + test('should shollowly transform object keys to constant case', () => { + const expected = { + SOME: { deepNested: { value: true } }, + OTHER_VALUE: true, + } + const result = constantKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('should handle null properly', () => { + const expected = null + const result = constantKeys(null) + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/object-keys/constant-keys.ts b/src/utils/object-keys/constant-keys.ts new file mode 100644 index 0000000..bb7ffa1 --- /dev/null +++ b/src/utils/object-keys/constant-keys.ts @@ -0,0 +1,22 @@ +import { transformKeys } from './transform-keys.js' +import { + type ConstantCase, + toConstantCase, +} from '../word-case/to-constant-case.js' + +/** + * Shallowly transforms the keys of an Record to CONSTANT_CASE. + * T: the type of the Record to transform. + */ +export type ConstantKeys = T extends [] + ? T + : { [K in keyof T as ConstantCase>]: T[K] } +/** + * A strongly typed function that shallowly transforms the keys of an object to CONSTANT_CASE. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example constantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { 'fizz-buzz': true } } + */ +export function constantKeys(obj: T): ConstantKeys { + return transformKeys(obj, toConstantCase) as never +} diff --git a/src/utils/object-keys/deep-camel-keys.test.ts b/src/utils/object-keys/deep-camel-keys.test.ts new file mode 100644 index 0000000..c65e79f --- /dev/null +++ b/src/utils/object-keys/deep-camel-keys.test.ts @@ -0,0 +1,40 @@ +import { type DeepCamelKeys, deepCamelKeys } from './deep-camel-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DeepCamelKeys<{ + some: { 'deep-nested': { value: true } } + 'other-value': true + }>, + { some: { deepNested: { value: true } }; otherValue: true } + > + > +} + +describe('deepCamelKeys', () => { + test('should camelize the object', () => { + const expected = { + some: { deepNested: { value: true } }, + otherValue: true, + } + const result = deepCamelKeys({ + some: { 'deep-nested': { value: true } }, + 'other-value': true, + }) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('should camelize from SCREAMING_SNAKE_CASE', () => { + const obj = { + NODE_ENV: 'development', + } + const expected = { + nodeEnv: 'development', + } + const result = deepCamelKeys(obj) + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/object-keys/deep-camel-keys.ts b/src/utils/object-keys/deep-camel-keys.ts new file mode 100644 index 0000000..ab911c7 --- /dev/null +++ b/src/utils/object-keys/deep-camel-keys.ts @@ -0,0 +1,23 @@ +import { type CamelCase, toCamelCase } from '../word-case/to-camel-case.js' +import { deepTransformKeys } from './deep-transform-keys.js' + +/** + * Recursively transforms the keys of an Record to camelCase. + * T: the type of the Record to transform. + */ +export type DeepCamelKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepCamelKeys } + : T extends (infer V)[] + ? DeepCamelKeys[] + : { + [K in keyof T as CamelCase>]: DeepCamelKeys + } +/** + * A strongly typed function that recursively transforms the keys of an object to camelCase. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example deepCamelKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { fooBar: { fizzBuzz: true } } + */ +export function deepCamelKeys(obj: T): DeepCamelKeys { + return deepTransformKeys(obj, toCamelCase) as never +} diff --git a/src/utils/object-keys/deep-constant-keys.test.ts b/src/utils/object-keys/deep-constant-keys.test.ts new file mode 100644 index 0000000..a666cb3 --- /dev/null +++ b/src/utils/object-keys/deep-constant-keys.test.ts @@ -0,0 +1,38 @@ +import { + type DeepConstantKeys, + deepConstantKeys, +} from './deep-constant-keys.js' + +namespace TypeTransforms { + type test5 = Expect< + Equal< + DeepConstantKeys<{ + some: { 'deep-nested': { value: true } } + 'other-value': true + }>, + { SOME: { DEEP_NESTED: { VALUE: true } }; OTHER_VALUE: true } + > + > +} + +describe('deepConstantKeys', () => { + test('should deeply transform object keys to constant case', () => { + const expected = { + SOME: { DEEP_NESTED: { VALUE: true } }, + OTHER_VALUE: true, + } + const result = deepConstantKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('should handle null properly', () => { + const expected = null + const result = deepConstantKeys(null) + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/object-keys/deep-constant-keys.ts b/src/utils/object-keys/deep-constant-keys.ts new file mode 100644 index 0000000..c45d49c --- /dev/null +++ b/src/utils/object-keys/deep-constant-keys.ts @@ -0,0 +1,26 @@ +import { + type ConstantCase, + toConstantCase, +} from '../word-case/to-constant-case.js' +import { deepTransformKeys } from './deep-transform-keys.js' + +/** + * Recursively transforms the keys of an Record to CONSTANT_CASE. + * T: the type of the Record to transform. + */ +export type DeepConstantKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepConstantKeys } + : T extends (infer V)[] + ? DeepConstantKeys[] + : { + [K in keyof T as ConstantCase>]: DeepConstantKeys + } +/** + * A strongly typed function that recursively transforms the keys of an object to CONSTANT_CASE. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example deepConstantKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FOO_BAR: { FIZZ_BUZZ: true } } + */ +export function deepConstantKeys(obj: T): DeepConstantKeys { + return deepTransformKeys(obj, toConstantCase) as never +} diff --git a/src/utils/object-keys/deep-delimiter-keys.test.ts b/src/utils/object-keys/deep-delimiter-keys.test.ts new file mode 100644 index 0000000..cacab1c --- /dev/null +++ b/src/utils/object-keys/deep-delimiter-keys.test.ts @@ -0,0 +1,35 @@ +import { + type DeepDelimiterKeys, + deepDelimiterKeys, +} from './deep-delimiter-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DeepDelimiterKeys< + { + some: { 'deep-nested': { value: true } } + 'other-value': true + }, + '@' + >, + { some: { 'deep@nested': { value: true } }; 'other@value': true } + > + > +} + +test('deepDelimiterKeys', () => { + const expected = { + some: { 'deep@nested': { value: true } }, + 'other@value': true, + } + const result = deepDelimiterKeys( + { + some: { 'deep-nested': { value: true } }, + 'other-value': true, + }, + '@', + ) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/deep-delimiter-keys.ts b/src/utils/object-keys/deep-delimiter-keys.ts new file mode 100644 index 0000000..2f99c07 --- /dev/null +++ b/src/utils/object-keys/deep-delimiter-keys.ts @@ -0,0 +1,36 @@ +import { + type DelimiterCase, + toDelimiterCase, +} from '../word-case/to-delimiter-case.js' +import { deepTransformKeys } from './deep-transform-keys.js' + +/** + * Recursively transforms the keys of an Record to a custom delimiter case. + * T: the type of the Record to transform. + * D: the delimiter to use. + */ +export type DeepDelimiterKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepDelimiterKeys } + : T extends (infer V)[] + ? DeepDelimiterKeys[] + : { + [K in keyof T as DelimiterCase, D>]: DeepDelimiterKeys< + T[K], + D + > + } +/** + * A strongly typed function that recursively transforms the keys of an object to a custom delimiter case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @param delimiter the delimiter to use. + * @returns the transformed object. + * @example deepDelimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } + */ +export function deepDelimiterKeys( + obj: T, + delimiter: D, +): DeepDelimiterKeys { + return deepTransformKeys(obj, (str) => + toDelimiterCase(str, delimiter), + ) as never +} diff --git a/src/utils/object-keys/deep-kebab-keys.test.ts b/src/utils/object-keys/deep-kebab-keys.test.ts new file mode 100644 index 0000000..fdd5862 --- /dev/null +++ b/src/utils/object-keys/deep-kebab-keys.test.ts @@ -0,0 +1,26 @@ +import { type DeepKebabKeys, deepKebabKeys } from './deep-kebab-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DeepKebabKeys<{ + some: { deepNested: { value: true } } + otherValue: true + }>, + { some: { 'deep-nested': { value: true } }; 'other-value': true } + > + > +} + +test('deepKebabKeys', () => { + const expected = { + some: { 'deep-nested': { value: true } }, + 'other-value': true, + } + const result = deepKebabKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/deep-kebab-keys.ts b/src/utils/object-keys/deep-kebab-keys.ts new file mode 100644 index 0000000..790524a --- /dev/null +++ b/src/utils/object-keys/deep-kebab-keys.ts @@ -0,0 +1,23 @@ +import { type KebabCase, toKebabCase } from '../word-case/to-kebab-case.js' +import { deepTransformKeys } from './deep-transform-keys.js' + +/** + * Recursively transforms the keys of an Record to kebab-case. + * T: the type of the Record to transform. + */ +export type DeepKebabKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepKebabKeys } + : T extends (infer V)[] + ? DeepKebabKeys[] + : { + [K in keyof T as KebabCase>]: DeepKebabKeys + } +/** + * A strongly typed function that recursively transforms the keys of an object to kebab-case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example deepKebabKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo-bar': { 'fizz-buzz': true } } + */ +export function deepKebabKeys(obj: T): DeepKebabKeys { + return deepTransformKeys(obj, toKebabCase) as never +} diff --git a/src/utils/object-keys/deep-pascal-keys.test.ts b/src/utils/object-keys/deep-pascal-keys.test.ts new file mode 100644 index 0000000..40def99 --- /dev/null +++ b/src/utils/object-keys/deep-pascal-keys.test.ts @@ -0,0 +1,26 @@ +import { type DeepPascalKeys, deepPascalKeys } from './deep-pascal-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DeepPascalKeys<{ + some: { 'deep-nested': { value: true } } + 'other-value': true + }>, + { Some: { DeepNested: { Value: true } }; OtherValue: true } + > + > +} + +test('deepPascalKeys', () => { + const expected = { + Some: { DeepNested: { Value: true } }, + OtherValue: true, + } + const result = deepPascalKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/deep-pascal-keys.ts b/src/utils/object-keys/deep-pascal-keys.ts new file mode 100644 index 0000000..12f3c50 --- /dev/null +++ b/src/utils/object-keys/deep-pascal-keys.ts @@ -0,0 +1,23 @@ +import { type PascalCase, toPascalCase } from '../word-case/to-pascal-case.js' +import { deepTransformKeys } from './deep-transform-keys.js' + +/** + * Recursively transforms the keys of an Record to PascalCase. + * T: the type of the Record to transform. + */ +export type DeepPascalKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepPascalKeys } + : T extends (infer V)[] + ? DeepPascalKeys[] + : { + [K in keyof T as PascalCase>]: DeepPascalKeys + } +/** + * A strongly typed function that recursively transforms the keys of an object to pascal case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example deepPascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { FizzBuzz: true } } + */ +export function deepPascalKeys(obj: T): DeepPascalKeys { + return deepTransformKeys(obj, toPascalCase) as never +} diff --git a/src/utils/object-keys/deep-snake-keys.test.ts b/src/utils/object-keys/deep-snake-keys.test.ts new file mode 100644 index 0000000..658a1e8 --- /dev/null +++ b/src/utils/object-keys/deep-snake-keys.test.ts @@ -0,0 +1,26 @@ +import { type DeepSnakeKeys, deepSnakeKeys } from './deep-snake-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DeepSnakeKeys<{ + some: { 'deep-nested': { value: true } } + 'other-value': true + }>, + { some: { deep_nested: { value: true } }; other_value: true } + > + > +} + +test('deepSnakeKeys', () => { + const expected = { + some: { deep_nested: { value: true } }, + other_value: true, + } + const result = deepSnakeKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/deep-snake-keys.ts b/src/utils/object-keys/deep-snake-keys.ts new file mode 100644 index 0000000..91ee0db --- /dev/null +++ b/src/utils/object-keys/deep-snake-keys.ts @@ -0,0 +1,23 @@ +import { type SnakeCase, toSnakeCase } from '../word-case/to-snake-case.js' +import { deepTransformKeys } from './deep-transform-keys.js' + +/** + * Recursively transforms the keys of an Record to snake_case. + * T: the type of the Record to transform. + */ +export type DeepSnakeKeys = T extends [any, ...any] + ? { [I in keyof T]: DeepSnakeKeys } + : T extends (infer V)[] + ? DeepSnakeKeys[] + : { + [K in keyof T as SnakeCase>]: DeepSnakeKeys + } +/** + * A strongly typed function that recursively transforms the keys of an object to snake_case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example deepSnakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz_buzz': true } } + */ +export function deepSnakeKeys(obj: T): DeepSnakeKeys { + return deepTransformKeys(obj, toSnakeCase) as never +} diff --git a/src/utils/object-keys/deep-transform-keys.test.ts b/src/utils/object-keys/deep-transform-keys.test.ts new file mode 100644 index 0000000..136744e --- /dev/null +++ b/src/utils/object-keys/deep-transform-keys.test.ts @@ -0,0 +1,18 @@ +import { deepTransformKeys } from './deep-transform-keys.js' + +describe('deepTransformKeys', () => { + test('should deeply transform the keys of an object', () => { + const expected = { + SOME: { 'DEEP-NESTED': { VALUE: true } }, + 'OTHER-VALUE': true, + } + const result = deepTransformKeys( + { + some: { 'deep-nested': { value: true } }, + 'other-value': true, + }, + (key) => key.toUpperCase(), + ) + expect(result).toEqual(expected) + }) +}) diff --git a/src/utils/object-keys/deep-transform-keys.ts b/src/utils/object-keys/deep-transform-keys.ts new file mode 100644 index 0000000..8bda552 --- /dev/null +++ b/src/utils/object-keys/deep-transform-keys.ts @@ -0,0 +1,26 @@ +import { typeOf } from '../../internal/internals.js' + +/** + * This function is used to transform the keys of an object deeply. + * It will only be transformed at runtime, so it's not type safe. + * @param obj the object to transform. + * @param transform the function to transform the keys from string to string. + * @returns the transformed object. + * @example deepTransformKeys({ 'foo-bar': { 'fizz-buzz': true } }, toCamelCase) + * // { fooBar: { fizzBuzz: true } } + */ +export function deepTransformKeys( + obj: T, + transform: (s: string) => string, +): T { + if (!['object', 'array'].includes(typeOf(obj))) return obj + + if (Array.isArray(obj)) { + return obj.map((x) => deepTransformKeys(x, transform)) as T + } + const res = {} as T + for (const key in obj) { + res[transform(key) as keyof T] = deepTransformKeys(obj[key], transform) + } + return res +} diff --git a/src/utils/object-keys/delimiter-keys.test.ts b/src/utils/object-keys/delimiter-keys.test.ts new file mode 100644 index 0000000..5c7991c --- /dev/null +++ b/src/utils/object-keys/delimiter-keys.test.ts @@ -0,0 +1,32 @@ +import { type DelimiterKeys, delimiterKeys } from './delimiter-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DelimiterKeys< + { + 'some-value': { 'nested-value': true } + 'other-value': true + }, + '@' + >, + { 'some@value': { 'nested-value': true }; 'other@value': true } + > + > +} + +test('delimiterKeys', () => { + const expected = { + some: { 'deep-nested': { value: true } }, + 'other@value': true, + } + const result = delimiterKeys( + { + some: { 'deep-nested': { value: true } }, + 'other-value': true, + }, + '@', + ) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/delimiter-keys.ts b/src/utils/object-keys/delimiter-keys.ts new file mode 100644 index 0000000..bc374c6 --- /dev/null +++ b/src/utils/object-keys/delimiter-keys.ts @@ -0,0 +1,27 @@ +import { transformKeys } from './transform-keys.js' +import { + type DelimiterCase, + toDelimiterCase, +} from '../word-case/to-delimiter-case.js' + +/** + * Shallowly transforms the keys of an Record to a custom delimiter case. + * T: the type of the Record to transform. + * D: the delimiter to use. + */ +export type DelimiterKeys = T extends [] + ? T + : { [K in keyof T as DelimiterCase, D>]: T[K] } +/** + * A strongly typed function that shallowly transforms the keys of an object to a custom delimiter case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @param delimiter the delimiter to use. + * @returns the transformed object. + * @example delimiterKeys({ 'foo-bar': { 'fizz-buzz': true } }, '.') // { 'foo.bar': { 'fizz.buzz': true } } + */ +export function delimiterKeys( + obj: T, + delimiter: D, +): DelimiterKeys { + return transformKeys(obj, (str) => toDelimiterCase(str, delimiter)) as never +} diff --git a/src/utils/object-keys/kebab-keys.test.ts b/src/utils/object-keys/kebab-keys.test.ts new file mode 100644 index 0000000..ee9b8dc --- /dev/null +++ b/src/utils/object-keys/kebab-keys.test.ts @@ -0,0 +1,26 @@ +import { type KebabKeys, kebabKeys } from './kebab-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + KebabKeys<{ + someValue: { deepNested: true } + otherValue: true + }>, + { 'some-value': { deepNested: true }; 'other-value': true } + > + > +} + +test('kebabKeys', () => { + const expected = { + some: { deepNested: { value: true } }, + 'other-value': true, + } + const result = kebabKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/kebab-keys.ts b/src/utils/object-keys/kebab-keys.ts new file mode 100644 index 0000000..d041c93 --- /dev/null +++ b/src/utils/object-keys/kebab-keys.ts @@ -0,0 +1,21 @@ +import { type KebabCase, toKebabCase } from '../word-case/to-kebab-case.js' +import { transformKeys } from './transform-keys.js' + +/** + * Shallowly transforms the keys of an Record to kebab-case. + * T: the type of the Record to transform. + */ +export type KebabKeys = T extends [] + ? T + : { + [K in keyof T as KebabCase>]: T[K] + } +/** + * A strongly typed function that shallowly transforms the keys of an object to kebab-case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example kebabKeys({ fooBar: { fizzBuzz: true } }) // { 'foo-bar': { fizzBuzz: true } } + */ +export function kebabKeys(obj: T): KebabKeys { + return transformKeys(obj, toKebabCase) as never +} diff --git a/src/utils/object-keys/pascal-keys.test.ts b/src/utils/object-keys/pascal-keys.test.ts new file mode 100644 index 0000000..a674036 --- /dev/null +++ b/src/utils/object-keys/pascal-keys.test.ts @@ -0,0 +1,26 @@ +import { type PascalKeys, pascalKeys } from './pascal-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + PascalKeys<{ + someValue: { deepNested: true } + otherValue: true + }>, + { SomeValue: { deepNested: true }; OtherValue: true } + > + > +} + +test('pascalKeys', () => { + const expected = { + Some: { deepNested: { value: true } }, + OtherValue: true, + } + const result = pascalKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/pascal-keys.ts b/src/utils/object-keys/pascal-keys.ts new file mode 100644 index 0000000..fb92907 --- /dev/null +++ b/src/utils/object-keys/pascal-keys.ts @@ -0,0 +1,19 @@ +import { type PascalCase, toPascalCase } from '../word-case/to-pascal-case.js' +import { transformKeys } from './transform-keys.js' + +/** + * Shallowly transforms the keys of an Record to PascalCase. + * T: the type of the Record to transform. + */ +export type PascalKeys = T extends [] + ? T + : { [K in keyof T as PascalCase>]: T[K] } +/** + * A strongly typed function that shallowly transforms the keys of an object to pascal case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example pascalKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { FooBar: { 'fizz-buzz': true } } + */ +export function pascalKeys(obj: T): PascalKeys { + return transformKeys(obj, toPascalCase) as never +} diff --git a/src/utils/object-keys/snake-keys.test.ts b/src/utils/object-keys/snake-keys.test.ts new file mode 100644 index 0000000..cdc8ce3 --- /dev/null +++ b/src/utils/object-keys/snake-keys.test.ts @@ -0,0 +1,26 @@ +import { type SnakeKeys, snakeKeys } from './snake-keys.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + SnakeKeys<{ + 'some-value': { 'deep-nested': true } + 'other-value': true + }>, + { some_value: { 'deep-nested': true }; other_value: true } + > + > +} + +test('snakeKeys', () => { + const expected = { + some: { deepNested: { value: true } }, + other_value: true, + } + const result = snakeKeys({ + some: { deepNested: { value: true } }, + otherValue: true, + }) + expect(result).toEqual(expected) + type test = Expect> +}) diff --git a/src/utils/object-keys/snake-keys.ts b/src/utils/object-keys/snake-keys.ts new file mode 100644 index 0000000..a5d2641 --- /dev/null +++ b/src/utils/object-keys/snake-keys.ts @@ -0,0 +1,19 @@ +import { transformKeys } from './transform-keys.js' +import { type SnakeCase, toSnakeCase } from '../word-case/to-snake-case.js' + +/** + * Shallowly transforms the keys of an Record to snake_case. + * T: the type of the Record to transform. + */ +export type SnakeKeys = T extends [] + ? T + : { [K in keyof T as SnakeCase>]: T[K] } +/** + * A strongly typed function that shallowly the keys of an object to snake_case. The transformation is done both at runtime and type level. + * @param obj the object to transform. + * @returns the transformed object. + * @example snakeKeys({ 'foo-bar': { 'fizz-buzz': true } }) // { 'foo_bar': { 'fizz-buzz': true } } + */ +export function snakeKeys(obj: T): SnakeKeys { + return transformKeys(obj, toSnakeCase) as never +} diff --git a/src/utils/object-keys/transform-keys.test.ts b/src/utils/object-keys/transform-keys.test.ts new file mode 100644 index 0000000..bdcafb2 --- /dev/null +++ b/src/utils/object-keys/transform-keys.test.ts @@ -0,0 +1,18 @@ +import { transformKeys } from './transform-keys.js' + +describe('transformKeys', () => { + test('should shallowly transform the keys of an object', () => { + const expected = { + SOME: { 'deep-nested': { value: true } }, + 'OTHER-VALUE': true, + } + const result = transformKeys( + { + some: { 'deep-nested': { value: true } }, + 'other-value': true, + }, + (key) => key.toUpperCase(), + ) + expect(result).toEqual(expected) + }) +}) diff --git a/src/utils/object-keys/transform-keys.ts b/src/utils/object-keys/transform-keys.ts new file mode 100644 index 0000000..986ac11 --- /dev/null +++ b/src/utils/object-keys/transform-keys.ts @@ -0,0 +1,20 @@ +import { typeOf } from '../../internal/internals.js' + +/** + * This function is used to shallowly transform the keys of an object. + * It will only be transformed at runtime, so it's not type safe. + * @param obj the object to transform. + * @param transform the function to transform the keys from string to string. + * @returns the transformed object. + * @example transformKeys({ 'foo-bar': { 'fizz-buzz': true } }, toCamelCase) + * // { fooBar: { 'fizz-buzz': true } } + */ +export function transformKeys(obj: T, transform: (s: string) => string): T { + if (typeOf(obj) !== 'object') return obj + + const res = {} as T + for (const key in obj) { + res[transform(key) as keyof T] = obj[key] + } + return res +} diff --git a/src/utils/truncate.test.ts b/src/utils/truncate.test.ts new file mode 100644 index 0000000..c629118 --- /dev/null +++ b/src/utils/truncate.test.ts @@ -0,0 +1,47 @@ +import { type Truncate, truncate } from './truncate.js' + +namespace TruncateTests { + type test1 = Expect, 'Hello,...'>> + type test2 = Expect, 'Hello, world'>> + type test3 = Expect, '...'>> + type test4 = Expect, 'Hell[...]'>> + type test5 = Expect, '...'>> + type test6 = Expect, '[...]'>> +} + +describe('truncate', () => { + test('truncate small sentence does nothing', () => { + const expected = 'Hello' as const + const result = truncate('Hello', 9) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('truncate big sentence truncate', () => { + const expected = 'Hello ...' as const + const result = truncate('Hello world', 9) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('truncate with negative integer does truncate', () => { + const expected = '...' as const + const result = truncate('Hello world', -1) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('truncate big sentence with specified omission', () => { + const expected = 'Hello[...]' as const + const result = truncate('Hello world', 10, '[...]') + expect(result).toEqual(expected) + type test = Expect> + }) + + test('truncate small sentence with specified omission', () => { + const expected = 'Hello' as const + const result = truncate('Hello', 10, '[...]') + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/truncate.ts b/src/utils/truncate.ts new file mode 100644 index 0000000..30d62e7 --- /dev/null +++ b/src/utils/truncate.ts @@ -0,0 +1,39 @@ +import type { Math } from '../internal/math.js' +import { join, type Join } from '../native/join.js' +import { type Length } from '../native/length.js' +import { type Slice } from '../native/slice.js' + +// STRING FUNCTIONS + +/** + * Truncate a string if it's longer than the given maximum length. + * The last characters of the truncated string are replaced with the omission string which defaults to "...". + */ +export type Truncate< + T extends string, + Size extends number, + Omission extends string = '...', +> = Math.IsNegative extends true + ? Omission + : Math.Subtract, Size> extends 0 + ? T + : Join<[Slice>>, Omission]> + +/** + * A strongly typed function to truncate a string if it's longer than the given maximum string length. + * The last characters of the truncated string are replaced with the omission string which defaults to "...". + * @param sentence the sentence to extract the words from. + * @param length the maximum length of the string. + * @param omission the string to append to the end of the truncated string. + * @returns the truncated string + * @example truncate('Hello, World', 8) // 'Hello...' + */ +export function truncate< + T extends string, + S extends number, + P extends string = '...', +>(sentence: T, length: S, omission = '...' as P): Truncate { + if (length < 0) return omission as Truncate + if (sentence.length <= length) return sentence as Truncate + return join([sentence.slice(0, length - omission.length), omission]) +} diff --git a/src/utils/uncapitalize.test.ts b/src/utils/uncapitalize.test.ts new file mode 100644 index 0000000..c677617 --- /dev/null +++ b/src/utils/uncapitalize.test.ts @@ -0,0 +1,18 @@ +import { uncapitalize } from './uncapitalize.js' +import { WEIRD_TEXT } from '../internal/fixtures.js' + +describe('uncapitalize', () => { + test('it does nothing with a string that has no char at the beginning', () => { + const expected = WEIRD_TEXT + const result = uncapitalize(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('it uncapitalizes the first char of a string', () => { + const expected = 'someWeird-casedString' as const + const result = uncapitalize('SomeWeird-casedString') + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/uncapitalize.ts b/src/utils/uncapitalize.ts new file mode 100644 index 0000000..9082978 --- /dev/null +++ b/src/utils/uncapitalize.ts @@ -0,0 +1,14 @@ +import { charAt } from '../native/char-at.js' +import { join } from '../native/join.js' +import { slice } from '../native/slice.js' +import { toLowerCase } from '../native/to-lower-case.js' + +/** + * Uncapitalizes the first letter of a string. This is a runtime counterpart of `Uncapitalize` from `src/types.d.ts`. + * @param str the string to uncapitalize. + * @returns the uncapitalized string. + * @example uncapitalize('Hello world') // 'hello world' + */ +export function uncapitalize(str: T): Uncapitalize { + return join([toLowerCase(charAt(str, 0)), slice(str, 1)]) +} diff --git a/src/utils/word-case/lower-case.test.ts b/src/utils/word-case/lower-case.test.ts new file mode 100644 index 0000000..333bca0 --- /dev/null +++ b/src/utils/word-case/lower-case.test.ts @@ -0,0 +1,19 @@ +import { WEIRD_TEXT, SEPARATORS_TEXT } from '../../internal/fixtures.js' +import { lowerCase } from './lower-case.js' + +describe('lowerCase', () => { + test('casing functions', () => { + const expected = + 'some weird cased $* string 1986 foo bar w for wumbo' as const + const result = lowerCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('lowerCase', () => { + const result = lowerCase(SEPARATORS_TEXT) + const expected = 'one two three four five six seven eight nine ten' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/lower-case.ts b/src/utils/word-case/lower-case.ts new file mode 100644 index 0000000..c4d8e9b --- /dev/null +++ b/src/utils/word-case/lower-case.ts @@ -0,0 +1,14 @@ +import { toLowerCase } from '../../native/to-lower-case.js' +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' + +/** + * A strongly-typed version of `lowerCase` that works in both runtime and type level. + * @param str the string to convert to lower case. + * @returns the lowercased string. + * @example lowerCase('HELLO-WORLD') // 'hello world' + */ +export function lowerCase( + str: T, +): Lowercase> { + return toLowerCase(toDelimiterCase(str, ' ')) +} diff --git a/src/utils/word-case/to-camel-case.test.ts b/src/utils/word-case/to-camel-case.test.ts new file mode 100644 index 0000000..916ef36 --- /dev/null +++ b/src/utils/word-case/to-camel-case.test.ts @@ -0,0 +1,30 @@ +import { + type WeirdTextUnion, + WEIRD_TEXT, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type CamelCase, toCamelCase } from './to-camel-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + CamelCase, + 'someWeirdCased$*String1986FooBarWForWumbo' | 'dontDistributeUnions' + > + > +} + +describe('toCamelCase', () => { + test('casing functions', () => { + const expected = 'someWeirdCased$*String1986FooBarWForWumbo' as const + const result = toCamelCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toCamelCase(SEPARATORS_TEXT) + const expected = 'oneTwoThreeFourFiveSixSevenEightNineTen' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-camel-case.ts b/src/utils/word-case/to-camel-case.ts new file mode 100644 index 0000000..bf815ca --- /dev/null +++ b/src/utils/word-case/to-camel-case.ts @@ -0,0 +1,17 @@ +import { type PascalCase, toPascalCase } from './to-pascal-case.js' +import { uncapitalize } from '../uncapitalize.js' + +/** + * Transforms a string to camelCase. + */ +export type CamelCase = Uncapitalize> + +/** + * A strongly typed version of `toCamelCase` that works in both runtime and type level. + * @param str the string to convert to camel case. + * @returns the camel cased string. + * @example toCamelCase('hello world') // 'helloWorld' + */ +export function toCamelCase(str: T): CamelCase { + return uncapitalize(toPascalCase(str)) +} diff --git a/src/utils/word-case/to-constant-case.test.ts b/src/utils/word-case/to-constant-case.test.ts new file mode 100644 index 0000000..830ccba --- /dev/null +++ b/src/utils/word-case/to-constant-case.test.ts @@ -0,0 +1,34 @@ +import { + type WeirdTextUnion, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type ConstantCase, toConstantCase } from './to-constant-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + ConstantCase, + | 'SOME_WEIRD_CASED_$*_STRING_1986_FOO_BAR_W_FOR_WUMBO' + | 'DONT_DISTRIBUTE_UNIONS' + > + > +} + +describe('toConstantCase', () => { + test('casing functions', () => { + const expected = + 'SOME_WEIRD_CASED_$*_STRING_1986_FOO_BAR_W_FOR_WUMBO' as const + const result = toConstantCase( + ' someWeird-cased$*String1986Foo Bar W_FOR_WUMBO', + ) + expect(result).toEqual(expected) + type test = Expect> + }) + + test('with various separators', () => { + const result = toConstantCase(SEPARATORS_TEXT) + const expected = 'ONE_TWO_THREE_FOUR_FIVE_SIX_SEVEN_EIGHT_NINE_TEN' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-constant-case.ts b/src/utils/word-case/to-constant-case.ts new file mode 100644 index 0000000..a0393c7 --- /dev/null +++ b/src/utils/word-case/to-constant-case.ts @@ -0,0 +1,16 @@ +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' +import { toUpperCase } from '../../native/to-upper-case.js' + +/** + * Transforms a string to CONSTANT_CASE. + */ +export type ConstantCase = Uppercase> +/** + * A strongly typed version of `toConstantCase` that works in both runtime and type level. + * @param str the string to convert to constant case. + * @returns the constant cased string. + * @example toConstantCase('hello world') // 'HELLO_WORLD' + */ +export function toConstantCase(str: T): ConstantCase { + return toUpperCase(toDelimiterCase(str, '_')) +} diff --git a/src/utils/word-case/to-delimiter-case.test.ts b/src/utils/word-case/to-delimiter-case.test.ts new file mode 100644 index 0000000..95b4e4e --- /dev/null +++ b/src/utils/word-case/to-delimiter-case.test.ts @@ -0,0 +1,32 @@ +import { + type WeirdTextUnion, + WEIRD_TEXT, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + DelimiterCase, + | 'some%Weird%cased%$*%String%1986%Foo%Bar%W%FOR%WUMBO' + | 'dont%distribute%unions' + > + > +} + +describe('toDelimiterCase', () => { + test('casing functions', () => { + const expected = + 'some@Weird@cased@$*@String@1986@Foo@Bar@W@FOR@WUMBO' as const + const result = toDelimiterCase(WEIRD_TEXT, '@') + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toDelimiterCase(SEPARATORS_TEXT, '.') + const expected = 'one.two.three.four.five.six.seven.eight.nine.ten' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-delimiter-case.ts b/src/utils/word-case/to-delimiter-case.ts new file mode 100644 index 0000000..21f4df0 --- /dev/null +++ b/src/utils/word-case/to-delimiter-case.ts @@ -0,0 +1,25 @@ +import { join } from '../../native/join.js' +import type { Join } from '../../native/join.js' +import { words } from '../words.js' +import type { Words } from '../words.js' + +/** + * Transforms a string with the specified separator (delimiter). + */ +export type DelimiterCase = Join< + Words, + D +> +/** + * A function that transforms a string by splitting it into words and joining them with the specified delimiter. + * @param str the string to transform. + * @param delimiter the delimiter to use. + * @returns the transformed string. + * @example toDelimiterCase('hello world', '.') // 'hello.world' + */ +export function toDelimiterCase( + str: T, + delimiter: D, +): DelimiterCase { + return join(words(str), delimiter) +} diff --git a/src/utils/word-case/to-kebab-case.test.ts b/src/utils/word-case/to-kebab-case.test.ts new file mode 100644 index 0000000..a2597e7 --- /dev/null +++ b/src/utils/word-case/to-kebab-case.test.ts @@ -0,0 +1,32 @@ +import { + type WeirdTextUnion, + WEIRD_TEXT, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type KebabCase, toKebabCase } from './to-kebab-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + KebabCase, + | 'some-weird-cased-$*-string-1986-foo-bar-w-for-wumbo' + | 'dont-distribute-unions' + > + > +} + +describe('toKebabCase', () => { + test('casing functions', () => { + const expected = + 'some-weird-cased-$*-string-1986-foo-bar-w-for-wumbo' as const + const result = toKebabCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toKebabCase(SEPARATORS_TEXT) + const expected = 'one-two-three-four-five-six-seven-eight-nine-ten' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-kebab-case.ts b/src/utils/word-case/to-kebab-case.ts new file mode 100644 index 0000000..5f61ea7 --- /dev/null +++ b/src/utils/word-case/to-kebab-case.ts @@ -0,0 +1,16 @@ +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' +import { toLowerCase } from '../../native/to-lower-case.js' + +/** + * Transforms a string to kebab-case. + */ +export type KebabCase = Lowercase> +/** + * A strongly typed version of `toKebabCase` that works in both runtime and type level. + * @param str the string to convert to kebab case. + * @returns the kebab cased string. + * @example toKebabCase('hello world') // 'hello-world' + */ +export function toKebabCase(str: T): KebabCase { + return toLowerCase(toDelimiterCase(str, '-')) +} diff --git a/src/utils/word-case/to-pascal-case.test.ts b/src/utils/word-case/to-pascal-case.test.ts new file mode 100644 index 0000000..71b1dfe --- /dev/null +++ b/src/utils/word-case/to-pascal-case.test.ts @@ -0,0 +1,30 @@ +import { + type WeirdTextUnion, + WEIRD_TEXT, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type PascalCase, toPascalCase } from './to-pascal-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + PascalCase, + 'SomeWeirdCased$*String1986FooBarWForWumbo' | 'DontDistributeUnions' + > + > +} + +describe('toPascelCase', () => { + test('casing functions', () => { + const expected = 'SomeWeirdCased$*String1986FooBarWForWumbo' as const + const result = toPascalCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toPascalCase(SEPARATORS_TEXT) + const expected = 'OneTwoThreeFourFiveSixSevenEightNineTen' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-pascal-case.ts b/src/utils/word-case/to-pascal-case.ts new file mode 100644 index 0000000..5571a00 --- /dev/null +++ b/src/utils/word-case/to-pascal-case.ts @@ -0,0 +1,19 @@ +import { pascalCaseAll, type PascalCaseAll } from '../../internal/internals.js' +import type { Join } from '../../native/join.js' +import { join } from '../../native/join.js' +import type { Words } from '../words.js' +import { words } from '../words.js' + +/** + * Transforms a string to PascalCase. + */ +export type PascalCase = Join>> +/** + * A strongly typed version of `toPascalCase` that works in both runtime and type level. + * @param str the string to convert to pascal case. + * @returns the pascal cased string. + * @example toPascalCase('hello world') // 'HelloWorld' + */ +export function toPascalCase(str: T): PascalCase { + return join(pascalCaseAll(words(str))) +} diff --git a/src/utils/word-case/to-snake-case.test.ts b/src/utils/word-case/to-snake-case.test.ts new file mode 100644 index 0000000..5c6f345 --- /dev/null +++ b/src/utils/word-case/to-snake-case.test.ts @@ -0,0 +1,32 @@ +import { + type WeirdTextUnion, + WEIRD_TEXT, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type SnakeCase, toSnakeCase } from './to-snake-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + SnakeCase, + | 'some_weird_cased_$*_string_1986_foo_bar_w_for_wumbo' + | 'dont_distribute_unions' + > + > +} + +describe('toSnakeCase', () => { + test('casing functions', () => { + const expected = + 'some_weird_cased_$*_string_1986_foo_bar_w_for_wumbo' as const + const result = toSnakeCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toSnakeCase(SEPARATORS_TEXT) + const expected = 'one_two_three_four_five_six_seven_eight_nine_ten' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-snake-case.ts b/src/utils/word-case/to-snake-case.ts new file mode 100644 index 0000000..2ac6950 --- /dev/null +++ b/src/utils/word-case/to-snake-case.ts @@ -0,0 +1,16 @@ +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' +import { toLowerCase } from '../../native/to-lower-case.js' + +/** + * Transforms a string to snake_case. + */ +export type SnakeCase = Lowercase> +/** + * A strongly typed version of `toSnakeCase` that works in both runtime and type level. + * @param str the string to convert to snake case. + * @returns the snake cased string. + * @example toSnakeCase('hello world') // 'hello_world' + */ +export function toSnakeCase(str: T): SnakeCase { + return toLowerCase(toDelimiterCase(str, '_')) +} diff --git a/src/utils/word-case/to-title-case.test.ts b/src/utils/word-case/to-title-case.test.ts new file mode 100644 index 0000000..02545b1 --- /dev/null +++ b/src/utils/word-case/to-title-case.test.ts @@ -0,0 +1,32 @@ +import { + type WeirdTextUnion, + WEIRD_TEXT, + SEPARATORS_TEXT, +} from '../../internal/fixtures.js' +import { type TitleCase, toTitleCase } from './to-title-case.js' + +namespace TypeTransforms { + type test = Expect< + Equal< + TitleCase, + | 'Some Weird Cased $* String 1986 Foo Bar W For Wumbo' + | 'Dont Distribute Unions' + > + > +} + +describe('toTitleCase', () => { + test('casing functions', () => { + const expected = + 'Some Weird Cased $* String 1986 Foo Bar W For Wumbo' as const + const result = toTitleCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('with various separators', () => { + const result = toTitleCase(SEPARATORS_TEXT) + const expected = 'One Two Three Four Five Six Seven Eight Nine Ten' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/to-title-case.ts b/src/utils/word-case/to-title-case.ts new file mode 100644 index 0000000..1fbc0ec --- /dev/null +++ b/src/utils/word-case/to-title-case.ts @@ -0,0 +1,16 @@ +import { type PascalCase, toPascalCase } from './to-pascal-case.js' +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' + +/** + * Transforms a string to "Title Case". + */ +export type TitleCase = DelimiterCase, ' '> +/** + * A strongly typed version of `toTitleCase` that works in both runtime and type level. + * @param str the string to convert to title case. + * @returns the title cased string. + * @example toTitleCase('hello world') // 'Hello World' + */ +export function toTitleCase(str: T): TitleCase { + return toDelimiterCase(toPascalCase(str), ' ') +} diff --git a/src/utils/word-case/upper-case.test.ts b/src/utils/word-case/upper-case.test.ts new file mode 100644 index 0000000..9304e28 --- /dev/null +++ b/src/utils/word-case/upper-case.test.ts @@ -0,0 +1,18 @@ +import { WEIRD_TEXT, SEPARATORS_TEXT } from '../../internal/fixtures.js' +import { upperCase } from './upper-case.js' + +describe('upperCase', () => { + test('casing functions', () => { + const expected = + 'SOME WEIRD CASED $* STRING 1986 FOO BAR W FOR WUMBO' as const + const result = upperCase(WEIRD_TEXT) + expect(result).toEqual(expected) + type test = Expect> + }) + test('upperCase', () => { + const result = upperCase(SEPARATORS_TEXT) + const expected = 'ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE TEN' + expect(result).toEqual(expected) + type test = Expect> + }) +}) diff --git a/src/utils/word-case/upper-case.ts b/src/utils/word-case/upper-case.ts new file mode 100644 index 0000000..b4b977b --- /dev/null +++ b/src/utils/word-case/upper-case.ts @@ -0,0 +1,14 @@ +import { toUpperCase } from '../../native/to-upper-case.js' +import { type DelimiterCase, toDelimiterCase } from './to-delimiter-case.js' + +/** + * A strongly-typed version of `upperCase` that works in both runtime and type level. + * @param str the string to convert to upper case. + * @returns the uppercased string. + * @example upperCase('hello-world') // 'HELLO WORLD' + */ +export function upperCase( + str: T, +): Uppercase> { + return toUpperCase(toDelimiterCase(str, ' ')) +} diff --git a/src/utils/words.test.ts b/src/utils/words.test.ts new file mode 100644 index 0000000..e11d2c3 --- /dev/null +++ b/src/utils/words.test.ts @@ -0,0 +1,86 @@ +import type { Words } from './words.js' +import { words } from './words.js' + +namespace WordsTests { + type test1 = Expect< + Equal< + Words<' someWeird-cased$*String1986Foo Bar obj.items[0]'>, + [ + 'some', + 'Weird', + 'cased', + '$*', + 'String', + '1986', + 'Foo', + 'Bar', + 'obj', + 'items', + '0', + ] + > + > +} + +type Mutable = { + -readonly [Key in keyof Type]: Type[Key] +} + +describe('words', () => { + test('it splits words at separators', () => { + const expected = [ + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + ] as const + const result = words( + '[one] two-three/four.five(six){seven}|eight_nine\\ten', + ) + expect(result).toEqual(expected) + type test = Expect>> + }) + + test('it splits words at digits', () => { + const expected = ['2', 'Weird', 'Cased', '1986', 'Foo'] as const + const result = words('2WeirdCased1986Foo') + expect(result).toEqual(expected) + type test = Expect>> + }) + + test('it splits words at special chars', () => { + const expected = ['$', '2', 'Weird', 'Cased', '@@', 'Foo'] as const + const result = words('$2WeirdCased@@Foo') + expect(result).toEqual(expected) + type test = Expect>> + }) + + test('it splits words at casing', () => { + const expected = ['some', 'Weird', 'Cased', 'STRING', 'Foo'] as const + const result = words('someWeirdCasedSTRINGFoo') + expect(result).toEqual(expected) + type test = Expect>> + }) + + test('it combines all of the rules above and trims the word', () => { + const expected = [ + 'some', + 'Weird', + 'cased', + '$*', + 'String', + '1986', + 'Foo', + 'Bar', + ] as const + const result = words(' someWeird-cased$*String1986Foo Bar ') + expect(result).toEqual(expected) + type test = Expect>> + }) +}) diff --git a/src/utils.ts b/src/utils/words.ts similarity index 61% rename from src/utils.ts rename to src/utils/words.ts index 4351dce..d1d1c0d 100644 --- a/src/utils.ts +++ b/src/utils/words.ts @@ -1,18 +1,17 @@ -import type { Math } from './math' -import { join, type Join, type Length, type Slice } from './primitives' -import type { Reject, DropSuffix } from './internals' -import type { IsSeparator } from './separators' -import { SEPARATOR_REGEX } from './separators' -import type { IsDigit, IsLower, IsSpecial, IsUpper } from './chars' +import type { Reject, DropSuffix } from '../internal/internals.js' +import type { IsSeparator } from './characters/separators.js' +import { SEPARATOR_REGEX } from './characters/separators.js' +import type { IsLower, IsUpper } from './characters/letters.js' +import type { IsDigit } from './characters/numbers.js' +import type { IsSpecial } from './characters/special.js' -// STRING FUNCTIONS /** * Splits a string into words. * sentence: The current string to split. * word: The current word. * prev: The previous character. */ -type Words< +export type Words< sentence extends string, word extends string = '', prev extends string = '', @@ -57,7 +56,7 @@ type Words< * @returns an array of words in both type level and runtime. * @example words('helloWorld') // ['hello', 'World'] */ -function words(sentence: T): Words { +export function words(sentence: T): Words { return sentence .replace(SEPARATOR_REGEX, ' ') // Step 1: Remove separators .replace(/([a-zA-Z])([0-9])/g, '$1 $2') // Step 2: From non-digit to digit @@ -69,39 +68,3 @@ function words(sentence: T): Words { .trim() // Step 8: Trim the last word .split(/\s+/g) as Words } - -/** - * Truncate a string if it's longer than the given maximum length. - * The last characters of the truncated string are replaced with the omission string which defaults to "...". - */ -type Truncate< - T extends string, - Size extends number, - Omission extends string = '...', -> = Math.IsNegative extends true - ? Omission - : Math.Subtract, Size> extends 0 - ? T - : Join<[Slice>>, Omission]> - -/** - * A strongly typed function to truncate a string if it's longer than the given maximum string length. - * The last characters of the truncated string are replaced with the omission string which defaults to "...". - * @param sentence the sentence to extract the words from. - * @param length the maximum length of the string. - * @param omission the string to append to the end of the truncated string. - * @returns the truncated string - * @example truncate('Hello, World', 8) // 'Hello...' - */ -function truncate( - sentence: T, - length: S, - omission = '...' as P, -): Truncate { - if (length < 0) return omission as Truncate - if (sentence.length <= length) return sentence as Truncate - return join([sentence.slice(0, length - omission.length), omission]) -} - -export type { Truncate, Words } -export { truncate, words } diff --git a/tsconfig.json b/tsconfig.json index 810efdc..19af09a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,5 @@ { - "include": [ - "src/**/*.ts", - "src/**/*.tsx" - ], + "include": ["src/**/*.ts"], "compilerOptions": { "declaration": true, "esModuleInterop": true,