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

fix: handle template literals in startsWith #182

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/native/ends-with.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ namespace TypeTests {
type test2 = Expect<Equal<EndsWith<string, 'c'>, boolean>>
type test3 = Expect<Equal<EndsWith<Uppercase<string>, 'c'>, boolean>>
type test4 = Expect<Equal<EndsWith<'abc', string>, boolean>>
type test6 = Expect<Equal<EndsWith<'abcde', 'd', 4>, true>>
type test7 = Expect<Equal<EndsWith<'abcde', 'e', 4>, false>>
type test8 = Expect<Equal<EndsWith<'abcde', 'e', 6>, true>>
type test9 = Expect<Equal<EndsWith<'abcde', 'e', -1>, false>>

// Template strings
type testTS1 = Expect<Equal<EndsWith<`${string}cba`, 'a'>, true>>
type testTS2 = Expect<Equal<EndsWith<`${string}abc`, 'a'>, false>>
type testTS3 = Expect<Equal<EndsWith<`zyx${string}cba`, 'a'>, true>>
type testTS4 = Expect<Equal<EndsWith<`xyz${string}abc`, 'a'>, false>>
type testTS5 = Expect<Equal<EndsWith<`abc${string}xyz`, 'c', 3>, true>>
type testTS6 = Expect<Equal<EndsWith<`abc${string}xyz`, 'c', 4>, boolean>>
}

describe('endsWith', () => {
Expand Down
26 changes: 20 additions & 6 deletions src/native/ends-with.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Math } from '../internal/math.js'
import type { Length } from './length.js'
import type { Slice } from './slice.js'
import type { StartsWith } from './starts-with.js'
import type { Reverse } from '../utils/reverse.js'
import type {
All,
IsNumberLiteral,
Expand All @@ -16,17 +18,29 @@ import type {
export type EndsWith<
T extends string,
S extends string,
P extends number = Length<T>,
> = All<[IsStringLiteral<T | S>, IsNumberLiteral<P>]> extends true
P extends number | undefined = undefined,
> = P extends number ? _EndsWith<T, S, P> : _EndsWithNoPosition<T, S>

type _EndsWith<T extends string, S extends string, P extends number> = All<
[IsStringLiteral<S>, IsNumberLiteral<P>]
> extends true
? Math.IsNegative<P> extends false
? P extends Length<T>
? S extends Slice<T, Math.Subtract<Length<T>, Length<S>>, Length<T>>
? true
: false
: EndsWith<Slice<T, 0, P>, S, Length<T>> // P !== T.length, slice
? IsStringLiteral<T> extends true
? S extends Slice<T, Math.Subtract<Length<T>, Length<S>>, Length<T>>
? true
: false
: _EndsWithNoPosition<Slice<T, 0, P>, S> // Eg: EndsWith<`abc${string}xyz`, 'c', 3>
: _EndsWithNoPosition<Slice<T, 0, P>, S> // P !== T.length, slice
: false // P is negative, false
: boolean

/** Overload of EndsWith without P */
type _EndsWithNoPosition<T extends string, S extends string> = StartsWith<
Reverse<T>,
Reverse<S>
>

/**
* A strongly-typed version of `String.prototype.endsWith`.
* @param text the string to search.
Expand Down
10 changes: 10 additions & 0 deletions src/native/slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ namespace TypeTests {
type test4 = Expect<Equal<Slice<Uppercase<string>, 5, 9>, string>>
type test5 = Expect<Equal<Slice<'some nice string', number, 9>, string>>
type test6 = Expect<Equal<Slice<'some nice string', 5, number>, string>>
type test7 = Expect<Equal<Slice<'abcde', -3>, 'cde'>>
type test8 = Expect<Equal<Slice<'abc', 10>, ''>>
type test9 = Expect<Equal<Slice<'abc', 10, 12>, ''>>

// Template literals
type testTS1 = Expect<Equal<Slice<`abc${string}`, 1, 3>, 'bc'>>
type testTS2 = Expect<Equal<Slice<`abcd${string}`, 1, 3>, 'bc'>>
type testTS3 = Expect<Equal<Slice<`abc${string}xyz`, 0, 3>, 'abc'>>
type testTS4 = Expect<Equal<Slice<`${string}abcd`, 1, 3>, string>>
type testTS5 = Expect<Equal<Slice<`abc${string}`, 1>, `bc${string}`>>
}

describe('slice', () => {
Expand Down
85 changes: 61 additions & 24 deletions src/native/slice.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import type { Math } from '../internal/math.js'
import type { Length } from './length.js'
import type {
All,
IsStringLiteral,
IsNumberLiteral,
} from '../internal/literals.js'
import type { IsStringLiteral, IsNumberLiteral } from '../internal/literals.js'

/**
* Slices a string from a startIndex to an endIndex.
Expand All @@ -15,26 +10,68 @@ import type {
export type Slice<
T extends string,
startIndex extends number = 0,
endIndex extends number = Length<T>,
> = All<
[IsStringLiteral<T>, IsNumberLiteral<startIndex | endIndex>]
> extends true
endIndex extends number | undefined = undefined,
> = endIndex extends number
? _Slice<T, startIndex, endIndex>
: _SliceStart<T, startIndex>

/** Slice with startIndex and endIndex */
type _Slice<
T extends string,
startIndex extends number,
endIndex extends number,
_result extends string = '',
> = IsNumberLiteral<startIndex | endIndex> extends true
? T extends `${infer head}${infer rest}`
? startIndex extends 0
? endIndex extends 0
? ''
: `${head}${Slice<
? IsStringLiteral<head> extends true
? startIndex extends 0
? endIndex extends 0
? _result
: _Slice<
rest,
0,
Math.Subtract<Math.GetPositiveIndex<T, endIndex>, 1>,
`${_result}${head}`
>
: _Slice<
rest,
Math.Subtract<Math.GetPositiveIndex<T, startIndex>, 1>,
Math.Subtract<Math.GetPositiveIndex<T, endIndex>, 1>
>}`
: `${Slice<
rest,
Math.Subtract<Math.GetPositiveIndex<T, startIndex>, 1>,
Math.Subtract<Math.GetPositiveIndex<T, endIndex>, 1>
>}`
: ''
Math.Subtract<Math.GetPositiveIndex<T, endIndex>, 1>,
_result
>
: startIndex | endIndex extends 0
? _result
: string // Head is non-literal
: IsStringLiteral<T> extends true // Couldn't be split into head/tail
? _result // T ran out
: startIndex | endIndex extends 0
? _result // Eg: Slice<`abc${string}`, 1, 3> -> 'bc'
: string // Head is non-literal
: string

/** Slice with startIndex only */
type _SliceStart<
T extends string,
startIndex extends number,
_result extends string = '',
> = IsNumberLiteral<startIndex> extends true
? T extends `${infer head}${infer rest}`
? IsStringLiteral<head> extends true
? startIndex extends 0
? T
: _SliceStart<
rest,
Math.Subtract<Math.GetPositiveIndex<T, startIndex>, 1>,
_result
>
: string
: IsStringLiteral<T> extends true
? _result
: startIndex extends 0
? _result
: string
: string

/**
* A strongly-typed version of `String.prototype.slice`.
* @param str the string to slice.
Expand All @@ -46,7 +83,7 @@ export type Slice<
export function slice<
T extends string,
S extends number = 0,
E extends number = Length<T>,
>(str: T, start: S = 0 as S, end: E = str.length as E) {
E extends number | undefined = undefined,
>(str: T, start: S = 0 as S, end: E = undefined as E) {
return str.slice(start, end) as Slice<T, S, E>
}
4 changes: 4 additions & 0 deletions src/native/starts-with.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ namespace TypeTests {
type test4 = Expect<Equal<StartsWith<string, 'a'>, boolean>>
type test5 = Expect<Equal<StartsWith<'abc', string>, boolean>>
type test6 = Expect<Equal<StartsWith<'abc', 'a', number>, boolean>>
type test7 = Expect<Equal<StartsWith<`abc${string}`, 'a'>, true>>
type test8 = Expect<Equal<StartsWith<`cba${string}`, 'a'>, false>>
type test9 = Expect<Equal<StartsWith<`abc${string}`, 'abc'>, true>>
type test10 = Expect<Equal<StartsWith<`abc${string}`, 'b', 1>, true>>
}

describe('startsWith', () => {
Expand Down
16 changes: 12 additions & 4 deletions src/native/starts-with.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ export type StartsWith<
T extends string,
S extends string,
P extends number = 0,
> = All<[IsStringLiteral<T | S>, IsNumberLiteral<P>]> extends true
> = All<[IsStringLiteral<S>, IsNumberLiteral<P>]> extends true
? Math.IsNegative<P> extends false
? P extends 0
? T extends `${S}${string}`
? true
: false
? S extends `${infer SHead}${infer SRest}`
? T extends `${infer THead}${infer TRest}`
? IsStringLiteral<THead | SHead> extends true
? THead extends SHead
? StartsWith<TRest, SRest>
: false // Heads weren't equal
: boolean // THead is non-literal
: IsStringLiteral<T> extends true // Couldn't split T
? false // T ran out, but we still have S
: boolean // T (or TRest) is not a literal
: true // Couldn't split S, we've already ruled out non-literal
: StartsWith<Slice<T, P>, S, 0> // P is >0, slice
: StartsWith<T, S, 0> // P is negative, ignore it
: boolean
Expand Down
11 changes: 9 additions & 2 deletions src/utils/reverse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace ReverseTests {
>
type test4 = Expect<Equal<Reverse<string>, string>>
type test5 = Expect<Equal<Reverse<Uppercase<string>>, Uppercase<string>>>

// Template strings
type testTS1 = Expect<Equal<Reverse<`abc${string}`>, `${string}cba`>>
type testTS2 = Expect<Equal<Reverse<`abc${string}xyz`>, `zyx${string}cba`>>
type testTS3 = Expect<Equal<Reverse<`${string}xyz`>, `zyx${string}`>>
}

describe('reverse', () => {
Expand All @@ -20,8 +25,10 @@ describe('reverse', () => {
})

test('should reverse a long string', () => {
const expected = 'murobal tse di mina tillom tnuresed aiciffo iuq apluc ni tnus ,tnediorp non tatadipuc taceacco tnis ruetpecxE .rutairap allun taiguf ue erolod mullic esse tilev etatpulov ni tiredneherper ni rolod eruri etua siuD .tauqesnoc odommoc ae xe piuqila tu isin sirobal ocmallu noitaticrexe durtson siuq ,mainev minim da mine tU .auqila angam erolod te erobal tu tnudidicni ropmet domsuie od des ,tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL'
const data = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
const expected =
'murobal tse di mina tillom tnuresed aiciffo iuq apluc ni tnus ,tnediorp non tatadipuc taceacco tnis ruetpecxE .rutairap allun taiguf ue erolod mullic esse tilev etatpulov ni tiredneherper ni rolod eruri etua siuD .tauqesnoc odommoc ae xe piuqila tu isin sirobal ocmallu noitaticrexe durtson siuq ,mainev minim da mine tU .auqila angam erolod te erobal tu tnudidicni ropmet domsuie od des ,tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL'
const data =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
const result = reverse(data)
expect(result).toEqual(expected)
type test = Expect<Equal<typeof result, typeof expected>>
Expand Down
9 changes: 7 additions & 2 deletions src/utils/reverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
* Reverses a string.
* - `T` The string to reverse.
*/
type Reverse<T extends string, _acc extends string = ''> = T extends `${infer Head}${infer Tail}`
type Reverse<
T extends string,
_acc extends string = '',
> = T extends `${infer Head}${infer Tail}`
? Reverse<Tail, `${Head}${_acc}`>
: _acc extends '' ? T : _acc
: _acc extends ''
? T
: `${T}${_acc}`

/**
* A strongly-typed function to reverse a string.
Expand Down
Loading