Skip to content

Commit

Permalink
Merge pull request #96 from poteat/poteat/further-reified-types
Browse files Browse the repository at this point in the history
Add string case reified types
  • Loading branch information
poteat authored Oct 12, 2024
2 parents e965a47 + 9eb7df5 commit db7a013
Show file tree
Hide file tree
Showing 22 changed files with 1,291 additions and 2 deletions.
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

0 comments on commit db7a013

Please sign in to comment.