Skip to content

Commit 8f3a153

Browse files
authored
Version 1.0.67 (#1491)
* StringGuard * ChangeLog * Version
1 parent aa4f766 commit 8f3a153

10 files changed

Lines changed: 602 additions & 212 deletions

File tree

changelog/1.0.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
---
44

55
### Version Updates
6+
- [Revision 1.0.67](https://github.com/sinclairzx81/typebox/pull/1491)
7+
- StringGuard Module for Grapheme Cluster Counting
68
- [Revision 1.0.66](https://github.com/sinclairzx81/typebox/pull/1490)
79
- String Length Grapheme Segmentation Polyfill for Intl.Segmenter
810
- [Revision 1.0.65](https://github.com/sinclairzx81/typebox/pull/1489)

example/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@ import Type from 'typebox'
99
// ------------------------------------------------------------------
1010
// Settings
1111
// ------------------------------------------------------------------
12+
1213
System.Settings.Set({ enumerableKind: false })
1314

15+
// ------------------------------------------------------------------
16+
// Guard
17+
// ------------------------------------------------------------------
18+
19+
console.log(Guard.GraphemeCount('📦'))
20+
1421
// ------------------------------------------------------------------
1522
// Type
1623
// ------------------------------------------------------------------
24+
1725
const T = Type.Object({
1826
x: Type.Number(),
1927
y: Type.Number(),
@@ -23,13 +31,15 @@ const T = Type.Object({
2331
// ------------------------------------------------------------------
2432
// Script
2533
// ------------------------------------------------------------------
34+
2635
const S = Type.Script({ T }, `{
2736
[K in keyof T]: T[K] | null
2837
}`)
2938

3039
// ------------------------------------------------------------------
3140
// Infer
3241
// ------------------------------------------------------------------
42+
3343
type T = Type.Static<typeof T>
3444
type S = Type.Static<typeof S>
3545

@@ -42,7 +52,6 @@ const R = Value.Parse(T, { x: 1, y: 2, z: 3 })
4252
// ------------------------------------------------------------------
4353
// Compile
4454
// ------------------------------------------------------------------
45-
4655
const C = Compile(S)
4756

4857
const X = C.Parse({ x: 1, y: 2, z: 3 })

src/guard/emit.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,11 @@ export function IsGreaterEqualThan(left: string, right: string): string {
134134
// --------------------------------------------------------------------------
135135
// String
136136
// --------------------------------------------------------------------------
137-
export function StringGraphemeCount(value: string): string {
138-
return `Guard.StringGraphemeCount(${value})`
137+
export function IsMinLength(value: string, length: string): string {
138+
return `Guard.IsMinLength(${value}, ${length})`
139+
}
140+
export function IsMaxLength(value: string, length: string): string {
141+
return `Guard.IsMaxLength(${value}, ${length})`
139142
}
140143
// --------------------------------------------------------------------------
141144
// Array
@@ -211,7 +214,7 @@ export function ReduceOr(operands: string[]): string {
211214
return G.IsEqual(operands.length, 0) ? 'false' : operands.reduce((left, right) => Or(left, right))
212215
}
213216
// --------------------------------------------------------------------------
214-
// Arithmatic
217+
// Arithmetic
215218
// --------------------------------------------------------------------------
216219
export function PrefixIncrement(expression: string): string {
217220
return `++${expression}`

src/guard/guard.ts

Lines changed: 17 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29+
import * as String from './string.ts'
30+
2931
// --------------------------------------------------------------------------
3032
// Guards
3133
// --------------------------------------------------------------------------
@@ -148,6 +150,21 @@ export function IsValueLike(value: unknown): value is bigint | boolean | null |
148150
IsUndefined(value)
149151
}
150152
// --------------------------------------------------------------------------
153+
// String
154+
// --------------------------------------------------------------------------
155+
/** Returns the number of grapheme clusters in the string */
156+
export function GraphemeCount(value: string): number {
157+
return String.GraphemeCount(value)
158+
}
159+
/** Returns true if the string has at most the given number of graphemes */
160+
export function IsMaxLength(value: string, length: number): boolean {
161+
return String.IsMaxLengthFast(value, length)
162+
}
163+
/** Returns true if the string has at least the given number of graphemes */
164+
export function IsMinLength(value: string, length: number): boolean {
165+
return String.IsMinLengthFast(value, length)
166+
}
167+
// --------------------------------------------------------------------------
151168
// Array
152169
// --------------------------------------------------------------------------
153170
export function Every<T>(value: T[], offset: number, callback: (value: T, index: number) => boolean): boolean {
@@ -210,86 +227,3 @@ export function IsDeepEqual(left: unknown, right: unknown): boolean {
210227
IsArray(left) ? DeepEqualArray(left, right) : IsObject(left) ? DeepEqualObject(left, right) : IsEqual(left, right)
211228
)
212229
}
213-
// --------------------------------------------------------------------------
214-
// StringGraphemeCountIntl - Intl.Segmenter Polyfill
215-
// --------------------------------------------------------------------------
216-
//
217-
// const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
218-
// function StringGraphemeCountIntl(value: string): number {
219-
// const iterator = segmenter.segment(value)[Symbol.iterator]()
220-
// let length = 0
221-
// while (!iterator.next().done) length++
222-
// return length
223-
// }
224-
//
225-
// --------------------------------------------------------------------------
226-
function IsRegionalIndicator(value: number): boolean {
227-
return value >= 0x1F1E6 && value <= 0x1F1FF
228-
}
229-
function IsVariationSelector(value: number): boolean {
230-
return value >= 0xFE00 && value <= 0xFE0F
231-
}
232-
function IsCombiningMark(value: number): boolean {
233-
return (
234-
(value >= 0x0300 && value <= 0x036F) ||
235-
(value >= 0x1AB0 && value <= 0x1AFF) ||
236-
(value >= 0x1DC0 && value <= 0x1DFF) ||
237-
(value >= 0xFE20 && value <= 0xFE2F)
238-
)
239-
}
240-
function CodePointLength(value: number) {
241-
return value > 0xFFFF ? 2 : 1
242-
}
243-
function StringGraphemeCountIntl(value: string): number {
244-
let count = 0
245-
let index = 0
246-
while (index < value.length) {
247-
const start = value.codePointAt(index)!
248-
let clusterEnd = index + CodePointLength(start)
249-
// Combining marks & variation selectors
250-
while (clusterEnd < value.length) {
251-
const next = value.codePointAt(clusterEnd)!
252-
if (IsCombiningMark(next) || IsVariationSelector(next)) {
253-
clusterEnd += CodePointLength(next)
254-
} else {
255-
break
256-
}
257-
}
258-
// ZWJ sequences
259-
while (clusterEnd < value.length - 1 && value[clusterEnd] === '\u200D') {
260-
const next = value.codePointAt(clusterEnd + 1)!
261-
clusterEnd += 1 + CodePointLength(next)
262-
}
263-
// Regional indicator pairs (flags)
264-
const first = IsRegionalIndicator(start)
265-
const second = clusterEnd < value.length && IsRegionalIndicator(value.codePointAt(clusterEnd)!)
266-
if (first && second) {
267-
const next = value.codePointAt(clusterEnd)!
268-
clusterEnd += CodePointLength(next)
269-
}
270-
count++
271-
index = clusterEnd
272-
}
273-
return count
274-
}
275-
// --------------------------------------------------------------------------
276-
// StringGraphemeCount
277-
// --------------------------------------------------------------------------
278-
function IsComplexGraphemeCodeUnit(value: number): boolean {
279-
return (
280-
(value >= 0xD800 && value <= 0xDBFF) || // High surrogate
281-
(value >= 0x0300 && value <= 0x036F) || // Combining diacritical marks
282-
(value === 0x200D) // Zero-width joiner
283-
)
284-
}
285-
/** Returns the number of Unicode Grapheme Clusters */
286-
export function StringGraphemeCount(value: string): number {
287-
let count = 0
288-
for (let index = 0; index < value.length; index++) {
289-
if (IsComplexGraphemeCodeUnit(value.charCodeAt(index))) {
290-
return StringGraphemeCountIntl(value)
291-
}
292-
count++
293-
}
294-
return count
295-
}

src/guard/string.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*--------------------------------------------------------------------------
2+
3+
TypeBox
4+
5+
The MIT License (MIT)
6+
7+
Copyright (c) 2017-2025 Haydn Paterson
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in
17+
all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
27+
---------------------------------------------------------------------------*/
28+
29+
// --------------------------------------------------------------------------
30+
// IsBetween
31+
// --------------------------------------------------------------------------
32+
function IsBetween(value: number, min: number, max: number): boolean {
33+
return value >= min && value <= max
34+
}
35+
// --------------------------------------------------------------------------
36+
// IsRegionalIndicator
37+
// --------------------------------------------------------------------------
38+
function IsRegionalIndicator(value: number): boolean {
39+
return IsBetween(value, 0x1F1E6, 0x1F1FF)
40+
}
41+
// --------------------------------------------------------------------------
42+
// IsVariationSelector
43+
// --------------------------------------------------------------------------
44+
function IsVariationSelector(value: number): boolean {
45+
return IsBetween(value, 0xFE00, 0xFE0F)
46+
}
47+
// --------------------------------------------------------------------------
48+
// IsCombiningMark
49+
// --------------------------------------------------------------------------
50+
function IsCombiningMark(value: number): boolean {
51+
return (
52+
IsBetween(value, 0x0300, 0x036F) ||
53+
IsBetween(value, 0x1AB0, 0x1AFF) ||
54+
IsBetween(value, 0x1DC0, 0x1DFF) ||
55+
IsBetween(value, 0xFE20, 0xFE2F)
56+
)
57+
}
58+
// --------------------------------------------------------------------------
59+
// CodePointLength
60+
// --------------------------------------------------------------------------
61+
function CodePointLength(value: number): number {
62+
return value > 0xFFFF ? 2 : 1
63+
}
64+
// --------------------------------------------------------------------------
65+
// ConsumeModifiers (helper)
66+
// --------------------------------------------------------------------------
67+
function ConsumeModifiers(value: string, index: number): number {
68+
while (index < value.length) {
69+
const point = value.codePointAt(index)!
70+
if (IsCombiningMark(point) || IsVariationSelector(point)) {
71+
index += CodePointLength(point)
72+
} else {
73+
break
74+
}
75+
}
76+
return index
77+
}
78+
// --------------------------------------------------------------------------
79+
// NextGraphemeClusterIndex
80+
// --------------------------------------------------------------------------
81+
function NextGraphemeClusterIndex(value: string, clusterStart: number): number {
82+
const startCP = value.codePointAt(clusterStart)!
83+
let clusterEnd = clusterStart + CodePointLength(startCP)
84+
// Consume combining marks & variation selectors
85+
clusterEnd = ConsumeModifiers(value, clusterEnd)
86+
// Handle multi-ZWJ sequences
87+
while (clusterEnd < value.length - 1 && value[clusterEnd] === '\u200D') {
88+
const nextCP = value.codePointAt(clusterEnd + 1)!
89+
clusterEnd += 1 + CodePointLength(nextCP)
90+
clusterEnd = ConsumeModifiers(value, clusterEnd)
91+
}
92+
// Handle regional indicator pairs (flags)
93+
if (
94+
IsRegionalIndicator(startCP) &&
95+
clusterEnd < value.length &&
96+
IsRegionalIndicator(value.codePointAt(clusterEnd)!)
97+
) {
98+
clusterEnd += CodePointLength(value.codePointAt(clusterEnd)!)
99+
}
100+
return clusterEnd
101+
}
102+
// --------------------------------------------------------------------------
103+
// IsGraphemeCodePoint
104+
// --------------------------------------------------------------------------
105+
function IsGraphemeCodePoint(value: number): boolean {
106+
return (
107+
IsBetween(value, 0xD800, 0xDBFF) || // High surrogate
108+
IsBetween(value, 0x0300, 0x036F) || // Combining diacritical marks
109+
(value === 0x200D) // Zero-width joiner
110+
)
111+
}
112+
// --------------------------------------------------------------------------
113+
// GraphemeCount
114+
// --------------------------------------------------------------------------
115+
/** Returns the number of grapheme clusters in a string */
116+
export function GraphemeCount(value: string): number {
117+
let count = 0
118+
let index = 0
119+
while (index < value.length) {
120+
index = NextGraphemeClusterIndex(value, index)
121+
count++
122+
}
123+
return count
124+
}
125+
// --------------------------------------------------------------------------
126+
// IsMinLength
127+
// --------------------------------------------------------------------------
128+
/** Checks if a string has at least a minimum number of grapheme clusters */
129+
export function IsMinLength(value: string, minLength: number): boolean {
130+
let count = 0
131+
let index = 0
132+
while (index < value.length) {
133+
index = NextGraphemeClusterIndex(value, index)
134+
count++
135+
if (count >= minLength) return true
136+
}
137+
return false
138+
}
139+
// --------------------------------------------------------------------------
140+
// IsMaxLength
141+
// --------------------------------------------------------------------------
142+
/** Checks if a string has at most a maximum number of grapheme clusters */
143+
export function IsMaxLength(value: string, maxLength: number): boolean {
144+
let count = 0
145+
let index = 0
146+
while (index < value.length) {
147+
index = NextGraphemeClusterIndex(value, index)
148+
count++
149+
if (count > maxLength) return false
150+
}
151+
return true
152+
}
153+
// --------------------------------------------------------------------------
154+
// IsMinLengthFast
155+
// --------------------------------------------------------------------------
156+
/** Fast check for minimum grapheme length, falls back to full check if needed */
157+
export function IsMinLengthFast(value: string, minLength: number): boolean {
158+
let index = 0
159+
while (index < value.length) {
160+
if (IsGraphemeCodePoint(value.charCodeAt(index))) {
161+
return IsMinLength(value, minLength)
162+
}
163+
index++
164+
if (index >= minLength) return true
165+
}
166+
return false
167+
}
168+
// --------------------------------------------------------------------------
169+
// IsMaxLengthFast
170+
// --------------------------------------------------------------------------
171+
/** Fast check for maximum grapheme length, falls back to full check if needed */
172+
export function IsMaxLengthFast(value: string, maxLength: number): boolean {
173+
let index = 0
174+
while (index < value.length) {
175+
if (IsGraphemeCodePoint(value.charCodeAt(index))) {
176+
return IsMaxLength(value, maxLength)
177+
}
178+
index++
179+
if (index > maxLength) return false
180+
}
181+
return true
182+
}

src/schema/engine/maxLength.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ import { Guard as G, EmitGuard as E } from '../../guard/index.ts'
3737
// Build
3838
// ------------------------------------------------------------------
3939
export function BuildMaxLength(stack: Stack, context: BuildContext, schema: Schema.XMaxLength, value: string): string {
40-
return E.IsLessEqualThan(E.StringGraphemeCount(value), E.Constant(schema.maxLength))
40+
return E.IsMaxLength(value, E.Constant(schema.maxLength))
4141
}
4242
// ------------------------------------------------------------------
4343
// Check
4444
// ------------------------------------------------------------------
4545
export function CheckMaxLength(stack: Stack, context: CheckContext, schema: Schema.XMaxLength, value: string): boolean {
46-
return G.IsLessEqualThan(G.StringGraphemeCount(value), schema.maxLength)
46+
return G.IsMaxLength(value, schema.maxLength)
4747
}
4848
// ------------------------------------------------------------------
4949
// Error

0 commit comments

Comments
 (0)