Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add string case reified types #96

Merged
merged 14 commits into from
Oct 12, 2024
13 changes: 13 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
57 changes: 57 additions & 0 deletions src/string/camel-case.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { $, Test, String } from '..'

type CamelCase_Spec = [
/**
* Can convert a string to camelCase.
*/
Test.Expect<$<String.CamelCase, 'hello world'>, 'helloWorld'>,

/**
* Can convert a string with multiple words.
*/
Test.Expect<$<String.CamelCase, 'hello world 42'>, 'helloWorld42'>,

/**
* Can convert a string with acronyms.
*/
Test.Expect<$<String.CamelCase, 'XMLHttpRequest'>, 'xmlHttpRequest'>,

/**
* Can convert a string with numbers.
*/
Test.Expect<$<String.CamelCase, 'hello42world'>, 'hello42World'>,

/**
* Can convert a string with a mix of words and numbers.
*/
Test.Expect<$<String.CamelCase, 'hello42world 42'>, 'hello42World42'>,

/**
* Can handle an empty string.
*/
Test.Expect<$<String.CamelCase, ''>, ''>
]

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('')
})
65 changes: 65 additions & 0 deletions src/string/camel-case.ts
Original file line number Diff line number Diff line change
@@ -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<S>
> = String._$fromList<{
[K in keyof WORDS]: K extends '0'
? String._$toLower<WORDS[K]>
: String._$capitalize<String._$toLower<WORDS[K]>>
}>

/**
* `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 interface CamelCase extends Kind.Kind {
f(x: Type._$cast<this[Kind._], string>): _$camelCase<typeof x>
}

/**
* 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<CamelCase>
14 changes: 13 additions & 1 deletion src/string/capitalize.spec.ts → src/string/capitalize.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $, Test, String } from 'hkt-toolbelt'
import { $, Test, String } from '..'

type Capitalize_Spec = [
/**
Expand Down Expand Up @@ -28,3 +28,15 @@ type Capitalize_Spec = [
// @ts-expect-error
$<$<Capitalize, ''>, 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('')
})
18 changes: 17 additions & 1 deletion src/string/capitalize.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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>
43 changes: 43 additions & 0 deletions src/string/constant-case.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { $, Test, String } from '..'

type ConstantCase_Spec = [
/**
* Can convert a string to CONSTANT_CASE.
*/
Test.Expect<$<String.ConstantCase, 'hello world'>, 'HELLO_WORLD'>,

/**
* Can convert a string with multiple words.
*/
Test.Expect<$<String.ConstantCase, 'hello world 42'>, 'HELLO_WORLD_42'>,

/**
* Can convert a string with acronyms.
*/
Test.Expect<$<String.ConstantCase, 'XMLHttpRequest'>, 'XML_HTTP_REQUEST'>,

/**
* Can convert a string with numbers.
*/
Test.Expect<$<String.ConstantCase, 'hello42world'>, '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('')
})
62 changes: 62 additions & 0 deletions src/string/constant-case.ts
Original file line number Diff line number Diff line change
@@ -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<S>
> = String_._$join<
{
[K in keyof WORDS]: String_._$toUpper<WORDS[K]>
},
'_'
>

/**
* `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 interface ConstantCase extends Kind.Kind {
f(x: Type._$cast<this[Kind._], string>): _$constantCase<typeof x>
}

/**
* 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<ConstantCase>
9 changes: 9 additions & 0 deletions src/string/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
export * from './append'
export * from './camel-case'
export * from './capitalize'
export * from './constant-case'
export * from './ends-with'
export * from './entries'
export * from './first'
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'
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'
export * from './pad-start'
export * from './pascal-case'
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'
export * from './to-char-code'
export * from './to-list'
export * from './to-lower'
export * from './to-upper'
export * from './words'
45 changes: 45 additions & 0 deletions src/string/is-digit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { $, String, Test } from '..'

type IsDigit_Spec = [
/**
* Can check if a string is a digit.
*/
Test.Expect<$<String.IsDigit, '0'>, true>,

/**
* Can check if a string is a digit
*/
Test.Expect<$<String.IsDigit, '9'>, true>,

/**
* Can check if a string is not a digit.
*/
Test.Expect<$<String.IsDigit, 'a'>, false>,

/**
* An empty string is not a digit.
*/
Test.Expect<$<String.IsDigit, ''>, false>,

/**
* A general string is not a digit.
*/
Test.Expect<$<String.IsDigit, 'foo'>, false>,

/**
* A template literal string is not a digit.
*/
Test.Expect<$<String.IsDigit, `0${string}1`>, 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)
})
Loading
Loading