Skip to content

Commit d473981

Browse files
committed
improvement(utils): add escapeRegExp and compareStrings to @sim/utils/string
1 parent 04096b6 commit d473981

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎packages/utils/src/string.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44
import { describe, expect, it } from 'vitest'
55
import {
6+
compareStrings,
7+
escapeRegExp,
68
forEachSearchOccurrence,
79
formatQuotedNameList,
810
isVersionedType,
@@ -246,3 +248,41 @@ describe('forEachSearchOccurrence', () => {
246248
expect(spans('\u0130xyz target', 'target')).toEqual(['target'])
247249
})
248250
})
251+
252+
describe('escapeRegExp', () => {
253+
it('escapes every regex metacharacter', () => {
254+
const metacharacters = `.*+?^$${'{}'}()|[]\\`
255+
expect(escapeRegExp(metacharacters)).toBe('\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\')
256+
})
257+
258+
it('leaves ordinary text untouched', () => {
259+
expect(escapeRegExp('plain text 42')).toBe('plain text 42')
260+
})
261+
262+
it('matches the literal value once interpolated', () => {
263+
const pattern = new RegExp(escapeRegExp('a.b'))
264+
expect(pattern.test('a.b')).toBe(true)
265+
expect(pattern.test('axb')).toBe(false)
266+
})
267+
268+
it('escapes every occurrence, not just the first', () => {
269+
expect(escapeRegExp('a.b.c')).toBe('a\\.b\\.c')
270+
})
271+
})
272+
273+
describe('compareStrings', () => {
274+
it('orders by code unit', () => {
275+
expect(compareStrings('a', 'b')).toBe(-1)
276+
expect(compareStrings('b', 'a')).toBe(1)
277+
expect(compareStrings('a', 'a')).toBe(0)
278+
})
279+
280+
it('sorts uppercase before lowercase, unlike localeCompare', () => {
281+
expect(compareStrings('Z', 'a')).toBe(-1)
282+
expect(['a', 'Z'].sort(compareStrings)).toEqual(['Z', 'a'])
283+
})
284+
285+
it('orders digit-led keys lexically, not numerically', () => {
286+
expect(['2', '10'].sort(compareStrings)).toEqual(['10', '2'])
287+
})
288+
})

‎packages/utils/src/string.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,26 @@ function identityStarts(length: number): number[] {
337337
for (let index = 0; index <= length; index += 1) starts[index] = index
338338
return starts
339339
}
340+
341+
/**
342+
* Escapes every regex metacharacter in `value` so it matches only itself when
343+
* interpolated into a `RegExp`.
344+
*
345+
* @example
346+
* new RegExp(escapeRegExp('a.b')) // matches the literal 'a.b', not 'axb'
347+
*/
348+
export function escapeRegExp(value: string): string {
349+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
350+
}
351+
352+
/**
353+
* Compares two strings by code unit, the ordering `Array.prototype.sort` applies
354+
* by default. Deliberately not `localeCompare`: ordering that feeds a hash, a
355+
* fingerprint, or a value compared across processes must not vary with the
356+
* host's locale.
357+
*/
358+
export function compareStrings(left: string, right: string): number {
359+
if (left < right) return -1
360+
if (left > right) return 1
361+
return 0
362+
}

0 commit comments

Comments
 (0)