From 27b3ce8bded029c69a9ab5b2c366f3af0a931dc9 Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 10 Oct 2024 23:37:30 -0700 Subject: [PATCH 01/14] feat: add is-uppercase-letter utility --- src/string/is-uppercase-letter.test.ts | 40 +++++++++++++ src/string/is-uppercase-letter.ts | 80 ++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/string/is-uppercase-letter.test.ts create mode 100644 src/string/is-uppercase-letter.ts diff --git a/src/string/is-uppercase-letter.test.ts b/src/string/is-uppercase-letter.test.ts new file mode 100644 index 00000000..1adff039 --- /dev/null +++ b/src/string/is-uppercase-letter.test.ts @@ -0,0 +1,40 @@ +import { $, String, Test } from '..' + +type IsUppercaseLetter_Spec = [ + /** + * Can check if a string is an uppercase letter. + */ + Test.Expect<$, true>, + + /** + * Can check if a string is not an uppercase letter. + */ + Test.Expect<$, false>, + + /** + * An empty string is not an uppercase letter. + */ + Test.Expect<$, false>, + + /** + * A general string is not an uppercase letter. + */ + Test.Expect<$, false>, + + /** + * A template literal string is not an uppercase letter. + */ + Test.Expect<$, false> +] + +it('should check if a string is an uppercase letter', () => { + expect(String.isUppercaseLetter('A')).toBe(true) +}) + +it('returns false for an empty string', () => { + expect(String.isUppercaseLetter('')).toBe(false) +}) + +it('returns false for a general string', () => { + expect(String.isUppercaseLetter('foo')).toBe(false) +}) diff --git a/src/string/is-uppercase-letter.ts b/src/string/is-uppercase-letter.ts new file mode 100644 index 00000000..3ff6aff6 --- /dev/null +++ b/src/string/is-uppercase-letter.ts @@ -0,0 +1,80 @@ +import { Kind, Type } from '..' + +type _$uppercaseLetter = + | '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' + +/** + * `_$isUppercaseLetter` is a type-level function that takes in a string `S` and + * returns a boolean indicating whether the string is an uppercase letter. + * + * @template {string} S - The string to check. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$isUppercaseLetter<'A'>; // true + * ``` + */ +export type _$isUppercaseLetter = S extends _$uppercaseLetter + ? true + : false + +/** + * `IsUppercaseLetter` is a type-level function that takes in a string `S` and + * returns a boolean indicating whether the string is an uppercase letter. + * + * @template {string} S - The string to check. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // true + * ``` + */ +export interface IsUppercaseLetter extends Kind.Kind { + f(x: Type._$cast): _$isUppercaseLetter +} + +/** + * Given a string, return a boolean indicating whether the string is an + * uppercase letter. + * + * @param {string} x - The string to check. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.isUppercaseLetter('A') + * // ^? true + * ``` + */ +export const isUppercaseLetter = ((x: string) => + x.match(/^[A-Z]$/) !== null) as Kind._$reify From 0207805e56733e336a28571e966e98ef15802105 Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 10 Oct 2024 23:37:42 -0700 Subject: [PATCH 02/14] feat: add is-lowercase-letter utility --- src/string/is-lowercase-letter.test.ts | 40 +++++++++++++ src/string/is-lowercase-letter.ts | 80 ++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/string/is-lowercase-letter.test.ts create mode 100644 src/string/is-lowercase-letter.ts diff --git a/src/string/is-lowercase-letter.test.ts b/src/string/is-lowercase-letter.test.ts new file mode 100644 index 00000000..cca9592c --- /dev/null +++ b/src/string/is-lowercase-letter.test.ts @@ -0,0 +1,40 @@ +import { $, String, Test } from '..' + +type IsLowercaseLetter_Spec = [ + /** + * Can check if a string is a lowercase letter. + */ + Test.Expect<$, true>, + + /** + * Can check if a string is not a lowercase letter. + */ + Test.Expect<$, false>, + + /** + * An empty string is not a lowercase letter. + */ + Test.Expect<$, false>, + + /** + * A general string is not a lowercase letter. + */ + Test.Expect<$, false>, + + /** + * A template literal string is not a lowercase letter. + */ + Test.Expect<$, false> +] + +it('should check if a string is a lowercase letter', () => { + expect(String.isLowercaseLetter('a')).toBe(true) +}) + +it('returns false for an empty string', () => { + expect(String.isLowercaseLetter('')).toBe(false) +}) + +it('returns false for a general string', () => { + expect(String.isLowercaseLetter('FOO')).toBe(false) +}) diff --git a/src/string/is-lowercase-letter.ts b/src/string/is-lowercase-letter.ts new file mode 100644 index 00000000..443bf9f3 --- /dev/null +++ b/src/string/is-lowercase-letter.ts @@ -0,0 +1,80 @@ +import { Kind, Type } from '..' + +type _$lowercaseLetter = + | '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' + +/** + * `_$isLowercaseLetter` is a type-level function that takes in a string `S` and + * returns a boolean indicating whether the string is a lowercase letter. + * + * @template {string} S - The string to check. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$isLowercaseLetter<'a'>; // true + * ``` + */ +export type _$isLowercaseLetter = S extends _$lowercaseLetter + ? true + : false + +/** + * `IsLowercaseLetter` is a type-level function that takes in a string `S` and + * returns a boolean indicating whether the string is a lowercase letter. + * + * @template {string} S - The string to check. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // true + * ``` + */ +export interface IsLowercaseLetter extends Kind.Kind { + f(x: Type._$cast): _$isLowercaseLetter +} + +/** + * Given a string, return a boolean indicating whether the string is a + * lowercase letter. + * + * @param {string} x - The string to check. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.isLowercaseLetter('a') + * // ^? true + * ``` + */ +export const isLowercaseLetter = ((x: string) => + x.match(/^[a-z]$/) !== null) as Kind._$reify From fc3af7c2ea78153efbe1c6a5da1fa8d42a102df8 Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 10 Oct 2024 23:38:02 -0700 Subject: [PATCH 03/14] feat: add utilities to string index file --- src/string/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/string/index.ts b/src/string/index.ts index 60e762a6..0e9d9e5a 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -8,8 +8,10 @@ export * from './from-list' export * from './includes' export * from './init' export * from './is-letter' +export * from './is-lowercase-letter' export * from './is-string' export * from './is-template' +export * from './is-uppercase-letter' export * from './join' export * from './last' export * from './length' From 7fe833e61b1e49082f7a0c9c52b4a020e372935a Mon Sep 17 00:00:00 2001 From: poteat Date: Thu, 10 Oct 2024 23:40:11 -0700 Subject: [PATCH 04/14] feat: add is-digit string method --- src/string/index.ts | 1 + src/string/is-digit.test.ts | 45 ++++++++++++++++++++++++++++++++ src/string/is-digit.ts | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/string/is-digit.test.ts create mode 100644 src/string/is-digit.ts diff --git a/src/string/index.ts b/src/string/index.ts index 0e9d9e5a..4732848f 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -7,6 +7,7 @@ export * from './from-char-code' export * from './from-list' export * from './includes' export * from './init' +export * from './is-digit' export * from './is-letter' export * from './is-lowercase-letter' export * from './is-string' diff --git a/src/string/is-digit.test.ts b/src/string/is-digit.test.ts new file mode 100644 index 00000000..e2582cb8 --- /dev/null +++ b/src/string/is-digit.test.ts @@ -0,0 +1,45 @@ +import { $, String, Test } from '..' + +type IsDigit_Spec = [ + /** + * Can check if a string is a digit. + */ + Test.Expect<$, true>, + + /** + * Can check if a string is a digit + */ + Test.Expect<$, true>, + + /** + * Can check if a string is not a digit. + */ + Test.Expect<$, false>, + + /** + * An empty string is not a digit. + */ + Test.Expect<$, false>, + + /** + * A general string is not a digit. + */ + Test.Expect<$, false>, + + /** + * A template literal string is not a digit. + */ + Test.Expect<$, false> +] + +it('should check if a string is a digit', () => { + expect(String.isDigit('0')).toBe(true) +}) + +it('returns false for an empty string', () => { + expect(String.isDigit('')).toBe(false) +}) + +it('returns false for a general string', () => { + expect(String.isDigit('000')).toBe(false) +}) diff --git a/src/string/is-digit.ts b/src/string/is-digit.ts new file mode 100644 index 00000000..4a494f90 --- /dev/null +++ b/src/string/is-digit.ts @@ -0,0 +1,52 @@ +import { Kind, Type } from '..' + +type _$digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + +/** + * `_$isDigit` is a type-level function that takes in a string `S` and + * returns a boolean indicating whether the string is a digit. + * + * @template {string} S - The string to check. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$isDigit<'0'>; // true + * ``` + */ +export type _$isDigit = S extends _$digit ? true : false + +/** + * `IsDigit` is a type-level function that takes in a string `S` and + * returns a boolean indicating whether the string is a digit. + * + * @template {string} S - The string to check. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // true + * ``` + */ +export interface IsDigit extends Kind.Kind { + f(x: Type._$cast): _$isDigit +} + +/** + * Given a string, return a boolean indicating whether the string is a + * digit. + * + * @param {string} x - The string to check. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.isDigit('0') + * // ^? true + * ``` + */ +export const isDigit = ((x: string) => + x.match(/^[0-9]$/) !== null) as Kind._$reify From 04ce59698b1e7f5d1bca2651f23f7494195cf07f Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 16:15:50 -0700 Subject: [PATCH 05/14] feat: implement word-splitting for strings --- src/string/index.ts | 1 + src/string/words.test.ts | 44 ++++++ src/string/words.ts | 304 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 src/string/words.test.ts create mode 100644 src/string/words.ts diff --git a/src/string/index.ts b/src/string/index.ts index 4732848f..584585f5 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -29,3 +29,4 @@ export * from './to-char-code' export * from './to-list' export * from './to-lower' export * from './to-upper' +export * from './words' diff --git a/src/string/words.test.ts b/src/string/words.test.ts new file mode 100644 index 00000000..dd8c2538 --- /dev/null +++ b/src/string/words.test.ts @@ -0,0 +1,44 @@ +import { $, String, Test } from '..' + +type Words_Spec = [ + /** + * Can split a string into words. + */ + Test.Expect<$, ['hello', 'world']>, + + /** + * Can split a string with digits + */ + Test.Expect<$, ['hello', '42', 'world']>, + + /** + * Can split a string into words with multiple spaces. + */ + Test.Expect<$, ['foo', 'bar']>, + + /** + * Can properly handle acronyms. + */ + Test.Expect<$, ['XML', 'Http', 'Request']>, + + /** + * Can handle alphanumeric input with no delimiters. + */ + Test.Expect<$, ['hello', '42', 'world']> +] + +it('should split a string into words', () => { + expect(String.words('hello world')).toEqual(['hello', 'world']) +}) + +it('should split a string with digits', () => { + expect(String.words('hello42world')).toEqual(['hello', '42', 'world']) +}) + +it('should split a string into words with multiple spaces', () => { + expect(String.words('foo bar')).toEqual(['foo', 'bar']) +}) + +it('should properly handle acronyms', () => { + expect(String.words('XMLHttpRequest')).toEqual(['XML', 'Http', 'Request']) +}) diff --git a/src/string/words.ts b/src/string/words.ts new file mode 100644 index 00000000..ee5965a6 --- /dev/null +++ b/src/string/words.ts @@ -0,0 +1,304 @@ +import { String, Type, Kind } from '..' + +type _$isDelimiter = S extends ' ' | '-' | '_' ? true : false + +/** + * `_$words` is a type-level function that takes in a string `S` and returns + * a list of words in the string. Words are defined as sequences of characters + * separated by whitespace, or delimited by case changes. Acronyms in the input + * are identified as individual words. + * + * Consecutive digits are treated as their own word, separate from any adjacent + * letter characters. + * + * @template {string} S - The string to split. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$words<'helloWorld'>; // ['hello', 'World'] + * type Result2 = String._$words<'hello world'>; // ['hello', 'world'] + * type Result3 = String._$words<'XMLHttpRequest'>; // ['XML', 'Http', 'Request'] + * ``` + */ +export type _$words< + S extends string, + /** + * The current word being built up as we iterate through the string, + * represented as a list of characters. + */ + CURRENT_WORD extends string[] = [], + /** + * The list of words currently identified. + */ + WORDS extends unknown[] = [], + /** + * The previous character in the string. + */ + PREV_CHAR extends string = '' +> = S extends `${infer Head}${infer Tail}` + ? _$isDelimiter extends true + ? /** + * The current character is a delimiter, so check if the current word is + * empty. + */ + CURRENT_WORD['length'] extends 0 + ? /** + * If the character is a delimiter, and the current word is empty, skip + * the current character. + */ + _$words + : /** + * If the character is a delimiter, and the current word is not empty, + * add the current word to the list of words and start a new empty word. + */ + _$words], Head> + : /** + * The character is not a delimiter, so perform additional checks. + */ + String._$isUppercaseLetter extends true + ? String._$isUppercaseLetter extends true + ? /** + * If both the current character and the previous character are + * uppercase, we need to look ahead to see if the next character is + * lowercase. If it is, we treat the current character as the start of + * a new word. + */ + Tail extends `${infer Next}${string}` + ? String._$isLowercaseLetter extends true + ? /** + * Since the next character is lowercase, treat the current + * character as the start of a new word. + */ + CURRENT_WORD['length'] extends 0 + ? /** + * If the current word is empty, add the current character to + * the current word. + */ + _$words + : /** + * If the current word is not empty, add the current word to + * the list of words and start a new empty word. + */ + _$words< + Tail, + [Head], + [...WORDS, String._$fromList], + Head + > + : /** + * Otherwise, the next character is not lowercase, so we can defer + * handling to the next iteration. + */ + _$words + : /** + * If both this character and the prior character was uppercase, + * and there is no next character, we can defer handling to the + * next iteration. + */ + _$words + : /** + * If the previous character is lowercase, then we are transitioning + * from lowercase to uppercase, which implies the start of a new word. + */ + String._$isLowercaseLetter extends true + ? CURRENT_WORD['length'] extends 0 + ? /** + * If the current word is empty, add the current character to the + * current word. + */ + _$words + : /** + * If the current word is not empty, add the current word to the + * list of words and start a new empty word. + */ + _$words< + Tail, + [Head], + [...WORDS, String._$fromList], + Head + > + : /** + * If the previous character was not lowercase, and the current + * character is uppercase, then we include the current character in + * the current word. + */ + _$words + : /** + * If the current character isn't uppercase, we check if it's a digit. + * If it is, it is treated as a separate word from any adjacent letter + * characters. + */ + String._$isDigit extends true + ? /** + * Because the prior character might also be a digit, we check if it + * is. If it is a prior digit, then the current character should be + * appended to that word. + */ + String._$isDigit extends true + ? _$words + : CURRENT_WORD['length'] extends 0 + ? /** + * If the current word is empty, add the current character to the + * a new word. + */ + _$words + : /** + * If the current word is not empty, add the current word to the + * list of words and start a new empty word. + */ + _$words< + Tail, + [Head], + [...WORDS, String._$fromList], + Head + > + : /** + * If the current character is not uppercase or a digit, then we treat + * it as part of the current word as long as the previous character + * was not a digit. + */ + String._$isDigit extends true + ? /** + * If the previous character was a digit, and the current character + * isn't, then we need to start a new word. + */ + CURRENT_WORD['length'] extends 0 + ? /** + * If the current word is empty, add the current character to the + * current word. + */ + _$words + : /** + * If the current word is not empty, add the current word to the + * list of words and start a new empty word. + */ + _$words< + Tail, + [Head], + [...WORDS, String._$fromList], + Head + > + : _$words + : /** + * If we've iterated through all of the characters, return the list of + * collected words. + */ + CURRENT_WORD['length'] extends 0 + ? WORDS + : [...WORDS, String._$fromList] + +/** + * `Words` is a type-level function that takes in a string `S` and returns + * a list of words in the string. Words are defined as sequences of characters + * separated by whitespace, or delimited by case changes. Acronyms in the input + * are identified as individual words. + * + * Consecutive digits are treated as their own word, separate from any adjacent + * letter characters. + * + * @template {string} S - The string to split. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // ['hello', 'World'] + * type Result2 = $; // ['hello', 'world'] + * type Result3 = $; // ['XML', 'Http', 'Request'] + * ``` + */ +export interface Words extends Kind.Kind { + f(x: Type._$cast): _$words +} + +const isUpperCase = (char: string) => char >= 'A' && char <= 'Z' + +const isLowerCase = (char: string) => char >= 'a' && char <= 'z' + +const isDigit = (char: string) => char >= '0' && char <= '9' + +const isDelimiter = (char: string) => + char === ' ' || char === '-' || char === '_' + +const isLetter = (char: string) => isUpperCase(char) || isLowerCase(char) + +/** + * Given a string, return a list of words in the string. + * + * @param {string} input - The string to split into words. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.words('hello42world') + * // ^? ['hello', '42', 'world'] + * ``` + */ +export const words = ((input: string) => { + const words: string[] = [] + let currentWord = '' + + for (let i = 0; i < input.length; i++) { + const char = input[i] + const prevChar = i > 0 ? input[i - 1] : '' + const nextChar = i < input.length - 1 ? input[i + 1] : '' + + if (isDelimiter(char)) { + if (currentWord.length > 0) { + words.push(currentWord) + currentWord = '' + } + continue + } + + const isCurrentDigit = isDigit(char) + const isPrevDigit = isDigit(prevChar) + const isCurrentUpper = isUpperCase(char) + const isPrevUpper = isUpperCase(prevChar) + const isPrevLower = isLowerCase(prevChar) + const isNextLower = isLowerCase(nextChar) + + if (isCurrentDigit) { + if (!isPrevDigit && currentWord.length > 0) { + // Transition from non-digit to digit, start new word + words.push(currentWord) + currentWord = char + } else { + currentWord += char + } + } else if (isLetter(char)) { + if (isPrevDigit && currentWord.length > 0) { + // Transition from digit to letter, start new word + words.push(currentWord) + currentWord = char + } else if ( + isCurrentUpper && + (isPrevLower || (isPrevUpper && isNextLower)) + ) { + // Transition from lowercase to uppercase or acronym ending + if (currentWord.length > 0) { + words.push(currentWord) + } + currentWord = char + } else { + currentWord += char + } + } else { + // Other characters (non-letter, non-digit, non-delimiter) + if (currentWord.length > 0) { + words.push(currentWord) + currentWord = '' + } + } + } + + // Add the last word if it's not empty + if (currentWord.length > 0) { + words.push(currentWord) + } + + return words +}) as Kind._$reify From f46fcb95be68cf64ef980ae9907d2e5e13fc2cf5 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 17:48:42 -0700 Subject: [PATCH 06/14] feat: properly handle template literal strings --- src/string/words.test.ts | 12 +++++- src/string/words.ts | 79 ++++++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/src/string/words.test.ts b/src/string/words.test.ts index dd8c2538..c3664f09 100644 --- a/src/string/words.test.ts +++ b/src/string/words.test.ts @@ -24,7 +24,17 @@ type Words_Spec = [ /** * Can handle alphanumeric input with no delimiters. */ - Test.Expect<$, ['hello', '42', 'world']> + Test.Expect<$, ['hello', '42', 'world']>, + + /** + * Splitting a generic string results in a `string[]` type. + */ + Test.Expect<$, string[]>, + + /** + * Splitting a template literal string results in a `string[]` type. + */ + Test.Expect<$, string[]> ] it('should split a string into words', () => { diff --git a/src/string/words.ts b/src/string/words.ts index ee5965a6..36a88925 100644 --- a/src/string/words.ts +++ b/src/string/words.ts @@ -2,27 +2,7 @@ import { String, Type, Kind } from '..' type _$isDelimiter = S extends ' ' | '-' | '_' ? true : false -/** - * `_$words` is a type-level function that takes in a string `S` and returns - * a list of words in the string. Words are defined as sequences of characters - * separated by whitespace, or delimited by case changes. Acronyms in the input - * are identified as individual words. - * - * Consecutive digits are treated as their own word, separate from any adjacent - * letter characters. - * - * @template {string} S - The string to split. - * - * @example - * ```ts - * import { String } from "hkt-toolbelt"; - * - * type Result = String._$words<'helloWorld'>; // ['hello', 'World'] - * type Result2 = String._$words<'hello world'>; // ['hello', 'world'] - * type Result3 = String._$words<'XMLHttpRequest'>; // ['XML', 'Http', 'Request'] - * ``` - */ -export type _$words< +type _$words2< S extends string, /** * The current word being built up as we iterate through the string, @@ -48,12 +28,12 @@ export type _$words< * If the character is a delimiter, and the current word is empty, skip * the current character. */ - _$words + _$words2 : /** * If the character is a delimiter, and the current word is not empty, * add the current word to the list of words and start a new empty word. */ - _$words], Head> + _$words2], Head> : /** * The character is not a delimiter, so perform additional checks. */ @@ -76,12 +56,12 @@ export type _$words< * If the current word is empty, add the current character to * the current word. */ - _$words + _$words2 : /** * If the current word is not empty, add the current word to * the list of words and start a new empty word. */ - _$words< + _$words2< Tail, [Head], [...WORDS, String._$fromList], @@ -91,13 +71,13 @@ export type _$words< * Otherwise, the next character is not lowercase, so we can defer * handling to the next iteration. */ - _$words + _$words2 : /** * If both this character and the prior character was uppercase, * and there is no next character, we can defer handling to the * next iteration. */ - _$words + _$words2 : /** * If the previous character is lowercase, then we are transitioning * from lowercase to uppercase, which implies the start of a new word. @@ -108,12 +88,12 @@ export type _$words< * If the current word is empty, add the current character to the * current word. */ - _$words + _$words2 : /** * If the current word is not empty, add the current word to the * list of words and start a new empty word. */ - _$words< + _$words2< Tail, [Head], [...WORDS, String._$fromList], @@ -124,7 +104,7 @@ export type _$words< * character is uppercase, then we include the current character in * the current word. */ - _$words + _$words2 : /** * If the current character isn't uppercase, we check if it's a digit. * If it is, it is treated as a separate word from any adjacent letter @@ -137,18 +117,18 @@ export type _$words< * appended to that word. */ String._$isDigit extends true - ? _$words + ? _$words2 : CURRENT_WORD['length'] extends 0 ? /** * If the current word is empty, add the current character to the * a new word. */ - _$words + _$words2 : /** * If the current word is not empty, add the current word to the * list of words and start a new empty word. */ - _$words< + _$words2< Tail, [Head], [...WORDS, String._$fromList], @@ -169,18 +149,18 @@ export type _$words< * If the current word is empty, add the current character to the * current word. */ - _$words + _$words2 : /** * If the current word is not empty, add the current word to the * list of words and start a new empty word. */ - _$words< + _$words2< Tail, [Head], [...WORDS, String._$fromList], Head > - : _$words + : _$words2 : /** * If we've iterated through all of the characters, return the list of * collected words. @@ -189,6 +169,33 @@ export type _$words< ? WORDS : [...WORDS, String._$fromList] +/** + * `_$words` is a type-level function that takes in a string `S` and returns + * a list of words in the string. Words are defined as sequences of characters + * separated by whitespace, or delimited by case changes. Acronyms in the input + * are identified as individual words. + * + * Consecutive digits are treated as their own word, separate from any adjacent + * letter characters. + * + * @template {string} S - The string to split. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$words<'helloWorld'>; // ['hello', 'World'] + * type Result2 = String._$words<'hello world'>; // ['hello', 'world'] + * type Result3 = String._$words<'XMLHttpRequest'>; // ['XML', 'Http', 'Request'] + * ``` + */ +export type _$words = + String._$isTemplate extends true + ? string[] + : string extends S + ? string[] + : _$words2 + /** * `Words` is a type-level function that takes in a string `S` and returns * a list of words in the string. Words are defined as sequences of characters From a605a8a6a42e619382f89a2930b4a1abad0fafd0 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 17:54:28 -0700 Subject: [PATCH 07/14] feat: reify capitalize method --- .../{capitalize.spec.ts => capitalize.test.ts} | 14 +++++++++++++- src/string/capitalize.ts | 18 +++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) rename src/string/{capitalize.spec.ts => capitalize.test.ts} (62%) diff --git a/src/string/capitalize.spec.ts b/src/string/capitalize.test.ts similarity index 62% rename from src/string/capitalize.spec.ts rename to src/string/capitalize.test.ts index f72e147a..13f4df29 100644 --- a/src/string/capitalize.spec.ts +++ b/src/string/capitalize.test.ts @@ -1,4 +1,4 @@ -import { $, Test, String } from 'hkt-toolbelt' +import { $, Test, String } from '..' type Capitalize_Spec = [ /** @@ -28,3 +28,15 @@ type Capitalize_Spec = [ // @ts-expect-error $<$, number> ] + +it('should capitalize a string', () => { + expect(String.capitalize('hello')).toBe('Hello') +}) + +it('should capitalize a string with first character already capitalized', () => { + expect(String.capitalize('Hello')).toBe('Hello') +}) + +it('should handle empty string input', () => { + expect(String.capitalize('')).toBe('') +}) diff --git a/src/string/capitalize.ts b/src/string/capitalize.ts index 5bc5abbb..c6dd1402 100644 --- a/src/string/capitalize.ts +++ b/src/string/capitalize.ts @@ -1,4 +1,4 @@ -import { Kind, Type } from '..' +import _, { Kind, Type } from '..' /** * `String._$capitalize` is a type-level function that capitalizes the first character of a string. @@ -27,3 +27,19 @@ interface _Capitalize extends Kind.Kind { } export { _Capitalize as Capitalize } + +/** + * Given a string, return a new string with the first letter capitalized. + * + * @param {string} x - The string to capitalize. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.capitalize('hello') + * // ^? 'Hello' + * ``` + */ +export const capitalize = ((x: string) => + x.charAt(0).toUpperCase() + x.slice(1)) as Kind._$reify<_Capitalize> From 5ea6afe8461c16006cd45c472ac5df80c96c1279 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:00:29 -0700 Subject: [PATCH 08/14] feat: add camel case utility --- src/string/camel-case.test.ts | 57 ++++++++++++++++++++++++++++++ src/string/camel-case.ts | 65 +++++++++++++++++++++++++++++++++++ src/string/index.ts | 1 + 3 files changed, 123 insertions(+) create mode 100644 src/string/camel-case.test.ts create mode 100644 src/string/camel-case.ts diff --git a/src/string/camel-case.test.ts b/src/string/camel-case.test.ts new file mode 100644 index 00000000..2bdeba00 --- /dev/null +++ b/src/string/camel-case.test.ts @@ -0,0 +1,57 @@ +import { $, Test, String } from '..' + +type CamelCase_Spec = [ + /** + * Can convert a string to camelCase. + */ + Test.Expect<$, 'helloWorld'>, + + /** + * Can convert a string with multiple words. + */ + Test.Expect<$, 'helloWorld42'>, + + /** + * Can convert a string with acronyms. + */ + Test.Expect<$, 'xmlHttpRequest'>, + + /** + * Can convert a string with numbers. + */ + Test.Expect<$, 'hello42World'>, + + /** + * Can convert a string with a mix of words and numbers. + */ + Test.Expect<$, 'hello42World42'>, + + /** + * Can handle an empty string. + */ + Test.Expect<$, ''> +] + +it('should convert a string to camelCase', () => { + expect(String.camelCase('hello world')).toBe('helloWorld') +}) + +it('should convert a string with multiple words', () => { + expect(String.camelCase('hello world 42')).toBe('helloWorld42') +}) + +it('should convert a string with acronyms', () => { + expect(String.camelCase('XMLHttpRequest')).toBe('xmlHttpRequest') +}) + +it('should convert a string with numbers', () => { + expect(String.camelCase('hello42world')).toBe('hello42World') +}) + +it('should convert a string with a mix of words and numbers', () => { + expect(String.camelCase('hello42world 42')).toBe('hello42World42') +}) + +it('should handle an empty string', () => { + expect(String.camelCase('')).toBe('') +}) diff --git a/src/string/camel-case.ts b/src/string/camel-case.ts new file mode 100644 index 00000000..c6b4c85b --- /dev/null +++ b/src/string/camel-case.ts @@ -0,0 +1,65 @@ +import { Kind, Type, String } from '..' + +/** + * `_$camelCase` is a type-level function that takes in a string `S` and returns + * a new string in the "camelCase" format, whereby the first letter of each word + * is capitalized, except for the first word. Capitalized acronyms in the input + * are identified. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$camelCase<'hello world'>; // 'helloWorld' + * ``` + */ +export type _$camelCase< + S extends string, + WORDS extends string[] = String._$words +> = String._$fromList<{ + [K in keyof WORDS]: K extends '0' + ? String._$toLower + : String._$capitalize> +}> + +/** + * `CamelCase` is a type-level function that takes in a string `S` and returns + * a new string in the "camelCase" format, whereby the first letter of each word + * is capitalized, except for the first word. Capitalized acronyms in the input + * are identified. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // 'helloWorld' + * ``` + */ +export interface CamelCase extends Kind.Kind { + f(x: Type._$cast): _$camelCase +} + +/** + * Given a string, return a new string in the "camelCase" format, whereby the + * first letter of each word is capitalized, except for the first word. + * + * @param {string} x - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.camelCase('hello world') + * // ^? 'helloWorld' + * ``` + */ +export const camelCase = ((x: string) => + String.words(x) + .map((word, i) => + i === 0 ? word.toLowerCase() : String.capitalize(word.toLowerCase()) + ) + .join('')) as Kind._$reify diff --git a/src/string/index.ts b/src/string/index.ts index 584585f5..0d551944 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -1,4 +1,5 @@ export * from './append' +export * from './camel-case' export * from './capitalize' export * from './ends-with' export * from './entries' From 28421e64362b0cd468cb0c382bd1c6f0fc143588 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:36:51 -0700 Subject: [PATCH 09/14] feat: implement pascal-case utility --- src/string/index.ts | 1 + src/string/pascal-case.test.ts | 43 ++++++++++++++++++++++++ src/string/pascal-case.ts | 61 ++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/string/pascal-case.test.ts create mode 100644 src/string/pascal-case.ts diff --git a/src/string/index.ts b/src/string/index.ts index 0d551944..079f7d83 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -19,6 +19,7 @@ export * from './last' export * from './length' export * from './pad-end' export * from './pad-start' +export * from './pascal-case' export * from './prepend' export * from './replace' export * from './reverse' diff --git a/src/string/pascal-case.test.ts b/src/string/pascal-case.test.ts new file mode 100644 index 00000000..446090ea --- /dev/null +++ b/src/string/pascal-case.test.ts @@ -0,0 +1,43 @@ +import { $, Test, String } from '..' + +type PascalCase_Spec = [ + /** + * Can convert a string to PascalCase. + */ + Test.Expect<$, 'HelloWorld'>, + + /** + * Can convert a string with multiple words. + */ + Test.Expect<$, 'HelloWorld42'>, + + /** + * Can convert a string with acronyms. + */ + Test.Expect<$, 'XmlHttpRequest'>, + + /** + * Can convert a string with numbers. + */ + Test.Expect<$, 'Hello42World'> +] + +it('should convert a string to PascalCase', () => { + expect(String.pascalCase('hello world')).toBe('HelloWorld') +}) + +it('should convert a string with multiple words', () => { + expect(String.pascalCase('hello world 42')).toBe('HelloWorld42') +}) + +it('should convert a string with acronyms', () => { + expect(String.pascalCase('XMLHttpRequest')).toBe('XmlHttpRequest') +}) + +it('should convert a string with numbers', () => { + expect(String.pascalCase('hello42world')).toBe('Hello42World') +}) + +it('should handle an empty string', () => { + expect(String.pascalCase('')).toBe('') +}) diff --git a/src/string/pascal-case.ts b/src/string/pascal-case.ts new file mode 100644 index 00000000..75085113 --- /dev/null +++ b/src/string/pascal-case.ts @@ -0,0 +1,61 @@ +import { Kind, Type, String as String_ } from '..' + +/** + * `_$pascalCase` is a type-level function that takes in a string `S` and returns + * a new string in the "PascalCase" format, whereby the first letter of each word + * is capitalized, except for the first word. Capitalized acronyms in the input + * are identified. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$pascalCase<'hello world'>; // 'HelloWorld' + * ``` + */ +export type _$pascalCase< + S extends string, + WORDS extends string[] = String_._$words +> = String_._$fromList<{ + [K in keyof WORDS]: String_._$capitalize> +}> + +/** + * `PascalCase` is a type-level function that takes in a string `S` and returns + * a new string in the "PascalCase" format, whereby the first letter of each word + * is capitalized, except for the first word. Capitalized acronyms in the input + * are identified. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // 'HelloWorld' + * ``` + */ +export interface PascalCase extends Kind.Kind { + f(x: Type._$cast): _$pascalCase +} + +/** + * Given a string, return a new string in the "PascalCase" format, whereby the + * first letter of each word is capitalized, except for the first word. + * + * @param {string} x - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.pascalCase('hello world') + * // ^? 'HelloWorld' + * ``` + */ +export const pascalCase = ((x: string) => + String_.words(x) + .map((word) => String_.capitalize(word.toLowerCase())) + .join('')) as Kind._$reify From 4986844a07c5b253b6fe07e305f1102d3034fb72 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:40:12 -0700 Subject: [PATCH 10/14] fix: docs for pascal-case --- src/string/pascal-case.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/string/pascal-case.ts b/src/string/pascal-case.ts index 75085113..49e23e46 100644 --- a/src/string/pascal-case.ts +++ b/src/string/pascal-case.ts @@ -2,9 +2,7 @@ import { Kind, Type, String as String_ } from '..' /** * `_$pascalCase` is a type-level function that takes in a string `S` and returns - * a new string in the "PascalCase" format, whereby the first letter of each word - * is capitalized, except for the first word. Capitalized acronyms in the input - * are identified. + * a new string in the "PascalCase" format. * * @template {string} S - The string to convert. * @@ -24,9 +22,7 @@ export type _$pascalCase< /** * `PascalCase` is a type-level function that takes in a string `S` and returns - * a new string in the "PascalCase" format, whereby the first letter of each word - * is capitalized, except for the first word. Capitalized acronyms in the input - * are identified. + * a new string in the "PascalCase" format. * * @template {string} S - The string to convert. * @@ -43,7 +39,7 @@ export interface PascalCase extends Kind.Kind { /** * Given a string, return a new string in the "PascalCase" format, whereby the - * first letter of each word is capitalized, except for the first word. + * first letter of each word is capitalized. * * @param {string} x - The string to convert. * From 683dfd59354a456fb3d6e6e11117ec217a03bb42 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:43:14 -0700 Subject: [PATCH 11/14] feat: add snake-case string method --- src/string/index.ts | 1 + src/string/snake-case.test.ts | 43 ++++++++++++++++++++++++ src/string/snake-case.ts | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/string/snake-case.test.ts create mode 100644 src/string/snake-case.ts diff --git a/src/string/index.ts b/src/string/index.ts index 079f7d83..7381cc1b 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -24,6 +24,7 @@ export * from './prepend' export * from './replace' export * from './reverse' export * from './slice' +export * from './snake-case' export * from './split' export * from './starts-with' export * from './tail' diff --git a/src/string/snake-case.test.ts b/src/string/snake-case.test.ts new file mode 100644 index 00000000..9f799dc5 --- /dev/null +++ b/src/string/snake-case.test.ts @@ -0,0 +1,43 @@ +import { $, Test, String } from '..' + +type SnakeCase_Spec = [ + /** + * Can convert a string to snake_case. + */ + Test.Expect<$, 'hello_world'>, + + /** + * Can convert a string with multiple words. + */ + Test.Expect<$, 'hello_world_42'>, + + /** + * Can convert a string with acronyms. + */ + Test.Expect<$, 'xml_http_request'>, + + /** + * Can convert a string with numbers. + */ + Test.Expect<$, 'hello_42_world'> +] + +it('should convert a string to snake_case', () => { + expect(String.snakeCase('hello world')).toBe('hello_world') +}) + +it('should convert a string with multiple words', () => { + expect(String.snakeCase('hello world 42')).toBe('hello_world_42') +}) + +it('should convert a string with acronyms', () => { + expect(String.snakeCase('XMLHttpRequest')).toBe('xml_http_request') +}) + +it('should convert a string with numbers', () => { + expect(String.snakeCase('hello42world')).toBe('hello_42_world') +}) + +it('should handle an empty string', () => { + expect(String.snakeCase('')).toBe('') +}) diff --git a/src/string/snake-case.ts b/src/string/snake-case.ts new file mode 100644 index 00000000..ba8f4281 --- /dev/null +++ b/src/string/snake-case.ts @@ -0,0 +1,62 @@ +import { Kind, Type, String as String_ } from '..' + +/** + * `_$snakeCase` is a type-level function that takes in a string `S` and returns + * a new string in the "snake_case" format, whereby each word is lowercased and + * separated by underscores. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$snakeCase<'hello world'>; // 'hello_world' + * ``` + */ +export type _$snakeCase< + S extends string, + WORDS extends string[] = String_._$words +> = String_._$join< + { + [K in keyof WORDS]: String_._$toLower + }, + '_' +> + +/** + * `SnakeCase` is a type-level function that takes in a string `S` and returns + * a new string in the "snake_case" format, whereby each word is lowercased and + * separated by underscores. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // 'hello_world' + * ``` + */ +export interface SnakeCase extends Kind.Kind { + f(x: Type._$cast): _$snakeCase +} + +/** + * Given a string, return a new string in the "snake_case" format, whereby each + * word is lowercased and separated by underscores. + * + * @param {string} x - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.snakeCase('hello world') + * // ^? 'hello_world' + * ``` + */ +export const snakeCase = ((x: string) => + String_.words(x) + .map((word) => word.toLowerCase()) + .join('_')) as Kind._$reify From 54026cc539a0649056424812a826d5f34a94585c Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:45:29 -0700 Subject: [PATCH 12/14] feat: add kebab-case method --- src/string/index.ts | 1 + src/string/kebab-case.test.ts | 43 ++++++++++++++++++++++++ src/string/kebab-case.ts | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/string/kebab-case.test.ts create mode 100644 src/string/kebab-case.ts diff --git a/src/string/index.ts b/src/string/index.ts index 7381cc1b..38c7f175 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -15,6 +15,7 @@ export * from './is-string' export * from './is-template' export * from './is-uppercase-letter' export * from './join' +export * from './kebab-case' export * from './last' export * from './length' export * from './pad-end' diff --git a/src/string/kebab-case.test.ts b/src/string/kebab-case.test.ts new file mode 100644 index 00000000..4964f57f --- /dev/null +++ b/src/string/kebab-case.test.ts @@ -0,0 +1,43 @@ +import { $, Test, String } from '..' + +type KebabCase_Spec = [ + /** + * Can convert a string to kebab-case. + */ + Test.Expect<$, 'hello-world'>, + + /** + * Can convert a string with multiple words. + */ + Test.Expect<$, 'hello-world-42'>, + + /** + * Can convert a string with acronyms. + */ + Test.Expect<$, 'xml-http-request'>, + + /** + * Can convert a string with numbers. + */ + Test.Expect<$, 'hello-42-world'> +] + +it('should convert a string to kebab-case', () => { + expect(String.kebabCase('hello world')).toBe('hello-world') +}) + +it('should convert a string with multiple words', () => { + expect(String.kebabCase('hello world 42')).toBe('hello-world-42') +}) + +it('should convert a string with acronyms', () => { + expect(String.kebabCase('XMLHttpRequest')).toBe('xml-http-request') +}) + +it('should convert a string with numbers', () => { + expect(String.kebabCase('hello42world')).toBe('hello-42-world') +}) + +it('should handle an empty string', () => { + expect(String.kebabCase('')).toBe('') +}) diff --git a/src/string/kebab-case.ts b/src/string/kebab-case.ts new file mode 100644 index 00000000..9facceda --- /dev/null +++ b/src/string/kebab-case.ts @@ -0,0 +1,62 @@ +import { Kind, Type, String as String_ } from '..' + +/** + * `_$kebabCase` is a type-level function that takes in a string `S` and returns + * a new string in the "kebab-case" format, whereby each word is lowercased and + * separated by hyphens. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$kebabCase<'hello world'>; // 'hello-world' + * ``` + */ +export type _$kebabCase< + S extends string, + WORDS extends string[] = String_._$words +> = String_._$join< + { + [K in keyof WORDS]: String_._$toLower + }, + '-' +> + +/** + * `KebabCase` is a type-level function that takes in a string `S` and returns + * a new string in the "kebab-case" format, whereby each word is lowercased and + * separated by hyphens. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // 'hello-world' + * ``` + */ +export interface KebabCase extends Kind.Kind { + f(x: Type._$cast): _$kebabCase +} + +/** + * Given a string, return a new string in the "kebab-case" format, whereby each + * word is lowercased and separated by hyphens. + * + * @param {string} x - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.kebabCase('hello world') + * // ^? 'hello-world' + * ``` + */ +export const kebabCase = ((x: string) => + String_.words(x) + .map((word) => word.toLowerCase()) + .join('-')) as Kind._$reify From 852abc2a0c11c24db7086bae0bd301c329a1a4ee Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:47:41 -0700 Subject: [PATCH 13/14] feat: add constant-case method --- src/string/constant-case.test.ts | 43 ++++++++++++++++++++++ src/string/constant-case.ts | 62 ++++++++++++++++++++++++++++++++ src/string/index.ts | 1 + 3 files changed, 106 insertions(+) create mode 100644 src/string/constant-case.test.ts create mode 100644 src/string/constant-case.ts diff --git a/src/string/constant-case.test.ts b/src/string/constant-case.test.ts new file mode 100644 index 00000000..52108b68 --- /dev/null +++ b/src/string/constant-case.test.ts @@ -0,0 +1,43 @@ +import { $, Test, String } from '..' + +type ConstantCase_Spec = [ + /** + * Can convert a string to CONSTANT_CASE. + */ + Test.Expect<$, 'HELLO_WORLD'>, + + /** + * Can convert a string with multiple words. + */ + Test.Expect<$, 'HELLO_WORLD_42'>, + + /** + * Can convert a string with acronyms. + */ + Test.Expect<$, 'XML_HTTP_REQUEST'>, + + /** + * Can convert a string with numbers. + */ + Test.Expect<$, 'HELLO_42_WORLD'> +] + +it('should convert a string to CONSTANT_CASE', () => { + expect(String.constantCase('hello world')).toBe('HELLO_WORLD') +}) + +it('should convert a string with multiple words', () => { + expect(String.constantCase('hello world 42')).toBe('HELLO_WORLD_42') +}) + +it('should convert a string with acronyms', () => { + expect(String.constantCase('XMLHttpRequest')).toBe('XML_HTTP_REQUEST') +}) + +it('should convert a string with numbers', () => { + expect(String.constantCase('hello42world')).toBe('HELLO_42_WORLD') +}) + +it('should handle an empty string', () => { + expect(String.constantCase('')).toBe('') +}) diff --git a/src/string/constant-case.ts b/src/string/constant-case.ts new file mode 100644 index 00000000..a28a4ded --- /dev/null +++ b/src/string/constant-case.ts @@ -0,0 +1,62 @@ +import { Kind, Type, String as String_ } from '..' + +/** + * `_$constantCase` is a type-level function that takes in a string `S` and returns + * a new string in the "CONSTANT_CASE" format, whereby each word is uppercased + * and separated by underscores. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * type Result = String._$constantCase<'hello world'>; // 'HELLO_WORLD' + * ``` + */ +export type _$constantCase< + S extends string, + WORDS extends string[] = String_._$words +> = String_._$join< + { + [K in keyof WORDS]: String_._$toUpper + }, + '_' +> + +/** + * `ConstantCase` is a type-level function that takes in a string `S` and returns + * a new string in the "CONSTANT_CASE" format, whereby each word is uppercased + * and separated by underscores. + * + * @template {string} S - The string to convert. + * + * @example + * ```ts + * import { $, String } from "hkt-toolbelt"; + * + * type Result = $; // 'HELLO_WORLD' + * ``` + */ +export interface ConstantCase extends Kind.Kind { + f(x: Type._$cast): _$constantCase +} + +/** + * Given a string, return a new string in the "CONSTANT_CASE" format, whereby + * each word is uppercased and separated by underscores. + * + * @param {string} x - The string to convert. + * + * @example + * ```ts + * import { String } from "hkt-toolbelt"; + * + * const result = String.constantCase('hello world') + * // ^? 'HELLO_WORLD' + * ``` + */ +export const constantCase = ((x: string) => + String_.words(x) + .map((word) => word.toUpperCase()) + .join('_')) as Kind._$reify diff --git a/src/string/index.ts b/src/string/index.ts index 38c7f175..3d5d412d 100644 --- a/src/string/index.ts +++ b/src/string/index.ts @@ -1,6 +1,7 @@ export * from './append' export * from './camel-case' export * from './capitalize' +export * from './constant-case' export * from './ends-with' export * from './entries' export * from './first' From 9eb7df583ed50fb0322a4a55e87afa224d4eaac2 Mon Sep 17 00:00:00 2001 From: poteat Date: Fri, 11 Oct 2024 18:56:23 -0700 Subject: [PATCH 14/14] docs: add changelog for 0.24.10 --- changelog.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/changelog.md b/changelog.md index 9f01a0d5..d7a3df80 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # Changelog +## [0.24.10] + +- Add `String.CamelCase` to convert a string to camelCase. +- Add `String.PascalCase` to convert a string to PascalCase. +- Add `String.SnakeCase` to convert a string to snake_case. +- Add `String.KebabCase` to convert a string to kebab-case. +- Add `String.ConstantCase` to convert a string to CONSTANT_CASE. +- Reify `String.Capitalize` to a value-level function. +- Add `String.Words` to split a string into words. +- Add `String.IsUppercaseLetter` to check if a string is uppercase. +- Add `String.IsLowercaseLetter` to check if a string is lowercase. +- Add `String.IsDigit` to check if a string is a digit. + ## [0.24.9] - Reify `NaturalNumber.Add` to a value-level function.