Skip to content

Commit 1c6c471

Browse files
committed
Experimentally expose OffsetMap from matchers/normalized
1 parent 9666432 commit 1c6c471

11 files changed

Lines changed: 110 additions & 178 deletions

deno.jsonc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"lock": false,
33
"name": "@li/irregex",
4-
"version": "0.7.5",
4+
"version": "0.7.6",
55
"exports": {
66
".": "./src/mod.ts",
77
"./irregex": "./src/irregex.ts",
@@ -16,6 +16,7 @@
1616
"@std/assert": "jsr:@std/assert@^1.0.3",
1717
"@li/irregex": "./mod.ts",
1818
"@std/cli": "jsr:@std/cli@^1.0.6",
19+
"@std/collections": "jsr:@std/collections@^1.1.3",
1920
"@std/testing": "jsr:@std/testing@^1.0.10",
2021
"string-dedent": "https://esm.sh/string-dedent@3.0.1"
2122
},

src/asRegExp.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Irregex } from '../src/irregex.ts'
33

44
class Irre extends Irregex {
55
re = /./g
6-
override trackLastIndex = [this.re]
6+
protected override trackLastIndex = [this.re]
77

88
protected override getMatch(str: string) {
99
return this.re.exec(str)

src/irregex.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Deno.test('throw match', () => {
4545

4646
class ThrowMatcher extends Irregex {
4747
#re = /./g
48-
override trackLastIndex = [this.#re]
48+
protected override trackLastIndex = [this.#re]
4949

5050
protected override getMatch(str: string) {
5151
if (this.lastIndex) throw new ThrowMatcherError()
@@ -151,4 +151,41 @@ Deno.test('zero-length matches', async (t) => {
151151
assertEquals(matcher.lastIndex, 0)
152152
})
153153
})
154+
155+
await t.step('fromIter iterated/cursor tracking of lastIndex', () => {
156+
const ir = new (class extends Irregex {
157+
#re = /./g
158+
protected override trackLastIndex = [this.#re]
159+
protected override getMatch(str: string) {
160+
return this.fromIter(str, function* () {
161+
for (const x of str.matchAll(this.#re)) {
162+
yield x
163+
}
164+
})
165+
}
166+
})()
167+
168+
const str = 'abc'
169+
170+
const next = () => ir.exec(str)?.[0] ?? null
171+
172+
assertEquals(next(), 'a')
173+
assertEquals(next(), 'b')
174+
assertEquals(next(), 'c')
175+
assertEquals(next(), null)
176+
177+
assertEquals(next(), 'a')
178+
assertEquals(next(), 'b')
179+
assertEquals(next(), 'c')
180+
assertEquals(next(), null)
181+
182+
ir.lastIndex = 2
183+
assertEquals(next(), 'c')
184+
ir.lastIndex = 1
185+
assertEquals(next(), 'b')
186+
ir.lastIndex = 500
187+
assertEquals(next(), null)
188+
ir.lastIndex = 0
189+
assertEquals(next(), 'a')
190+
})
154191
})

src/irregex.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { replaceValueToReplacer } from './replace.ts'
2+
import { binarySearch } from '@std/collections/unstable-binary-search'
23

34
/**
45
* A contract fulfilled by both `RegExp`s and `Irregex`s. The `Irregex` class implements these properties and methods of
@@ -61,19 +62,21 @@ export abstract class Irregex<T = unknown> implements IrregexCompatible {
6162
* A list of matchers that should have their `lastIndex` property kept in sync with the parent `Irregex`'s
6263
* `lastIndex` property. Useful for keeping internally-used regexes (or other matchers) in sync.
6364
*/
64-
trackLastIndex?: Pick<Matcher, 'lastIndex'>[]
65+
protected trackLastIndex?: { lastIndex: number }[]
6566

6667
exec(str: string): (RegExpExecArray & T) | null {
6768
const match = this.getMatch(str)
6869
this.lastIndex = match ? match.index + match[0].length : 0
6970
return match
7071
}
7172

72-
#lastCached?: {
73-
input: string
73+
static #MAX_CACHE_SIZE = 10
74+
#lastCached = new Map<string, {
7475
iterated: (RegExpExecArray & T)[]
7576
iterator: Iterator<RegExpExecArray & T>
76-
}
77+
cursor: number
78+
indices: number[]
79+
}>()
7780

7881
/**
7982
* Convenience method for converting an iterator function to a match getter suitable for use with `getMatch`.
@@ -87,24 +90,61 @@ export abstract class Irregex<T = unknown> implements IrregexCompatible {
8790
str: string,
8891
getter: (this: this) => Iterable<RegExpExecArray & T>,
8992
): (RegExpExecArray & T) | null {
90-
if (this.#lastCached?.input !== str) {
93+
let lastCached = this.#lastCached.get(str)
94+
95+
if (lastCached == null) {
9196
const iterator = getter.call(this)[Symbol.iterator]()
9297

93-
this.#lastCached = {
94-
input: str,
95-
iterated: [],
96-
iterator,
98+
this.#lastCached.set(
99+
str,
100+
lastCached = {
101+
iterated: [],
102+
iterator,
103+
cursor: 0,
104+
indices: [],
105+
},
106+
)
107+
108+
if (this.#lastCached.size > Irregex.#MAX_CACHE_SIZE) {
109+
this.#lastCached.delete(this.#lastCached.keys().next().value!)
97110
}
98111
}
99112

100-
for (const x of this.#lastCached.iterated) {
101-
if (x.index >= this.lastIndex) return x
113+
if (this.lastIndex === 0) lastCached.cursor = 0
114+
115+
// if we've already iterated past the current lastIndex, the result is already in the `iterated` array
116+
if ((lastCached.iterated.at(-1)?.index ?? -1) >= this.lastIndex) {
117+
// check after the cursor first (assuming usually iterated sequentially)
118+
for (let i = lastCached.cursor; i < lastCached.iterated.length; ++i) {
119+
const x = lastCached.iterated[i]!
120+
const prev = lastCached.iterated[i - 1]
121+
if (x.index >= this.lastIndex && (prev?.index ?? -1) < this.lastIndex) {
122+
lastCached.cursor = i
123+
return x
124+
}
125+
}
126+
127+
const { cursor } = lastCached
128+
lastCached.cursor = 0
129+
130+
// const indices = lastCached.iterated.map(x=>x.index).slice(0, cursor)
131+
132+
// throw '!'
133+
134+
const i = binarySearch(
135+
lastCached.indices.slice(0, cursor),
136+
this.lastIndex,
137+
)
138+
return lastCached.iterated[i < 0 ? ~i : i]!
102139
}
140+
103141
while (true) {
104-
const next = this.#lastCached.iterator.next()
142+
const next = lastCached.iterator.next()
105143
if (next.done) return null
106144
const x = next.value
107-
this.#lastCached.iterated.push(x)
145+
146+
lastCached.cursor = lastCached.iterated.push(x) - 1
147+
lastCached.indices.push(x.index)
108148
if (x.index >= this.lastIndex) return x
109149
}
110150
}

src/matchers/normalized.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dedent from 'string-dedent'
22
import { assertEquals } from '@std/assert'
3-
import { NormalizedMatcher } from './normalized.ts'
3+
import { NormalizedMatcher, OffsetMap } from './normalized.ts'
44

55
Deno.test(NormalizedMatcher.name, async (t) => {
66
await t.step('`g` and `d` flags', () => {
@@ -187,8 +187,6 @@ Deno.test(NormalizedMatcher.name, async (t) => {
187187
})
188188
})
189189

190-
const OffsetMap = NormalizedMatcher['OffsetMap']
191-
192190
Deno.test(OffsetMap.name, () => {
193191
const offsetMap = new OffsetMap([[2, 3], [4, 2], [10, 5]])
194192

src/matchers/normalized.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ type OffsetMapState = Readonly<{
1717
increment: number
1818
}>
1919

20-
class OffsetMap {
20+
/** @experimental */
21+
export class OffsetMap {
2122
#cursor = 0
2223
#increment = 0
2324

2425
#latestIncrement = 0
25-
get latestIncrement() {
26+
get latestIncrement(): number {
2627
return this.#latestIncrement
2728
}
2829

@@ -45,7 +46,7 @@ class OffsetMap {
4546
this.#increment = increment
4647
}
4748

48-
remapToOriginal(offset: number) {
49+
remapToOriginal(offset: number): number {
4950
this.#latestIncrement = 0
5051

5152
for (; this.#cursor < this.offsets.length; ++this.#cursor) {
@@ -83,7 +84,7 @@ export class NormalizedMatcher extends Irregex {
8384
return this.fromIter(input, function* () {
8485
let replacementIncrement = 0
8586

86-
let offsetMap = new NormalizedMatcher.OffsetMap([])
87+
let offsetMap = new OffsetMap([])
8788
let inputNormalized = input
8889

8990
for (const { selector, replacer } of this.#normalizers) {
@@ -114,7 +115,7 @@ export class NormalizedMatcher extends Irregex {
114115
}),
115116
)
116117

117-
offsetMap = new NormalizedMatcher.OffsetMap([
118+
offsetMap = new OffsetMap([
118119
...offsetMap.offsets,
119120
...offsets,
120121
].sort(([a], [b]) => a - b))
@@ -193,6 +194,4 @@ export class NormalizedMatcher extends Irregex {
193194
}
194195
})
195196
}
196-
197-
private static OffsetMap = OffsetMap
198197
}

src/matchers/physicalLineBoundary.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Irregex } from '../irregex.ts'
33

44
export class PhysicalLineBoundaryMatcher extends Irregex {
55
colWidth: number
6+
stringWidth = unicodeWidth
67

78
constructor(colWidth: number) {
89
super()
@@ -18,9 +19,8 @@ export class PhysicalLineBoundaryMatcher extends Irregex {
1819
const rest = str.slice(prevLineBreak?.index ?? 0, m.index)
1920
const line = rest.slice(rest.lastIndexOf('\n') + 1)
2021

21-
if (unicodeWidth(line) > this.colWidth) {
22-
if (prevWordBreak != null) yield prevWordBreak
23-
prevLineBreak = prevWordBreak
22+
if (this.stringWidth(line) > this.colWidth && prevWordBreak != null) {
23+
yield prevLineBreak = prevWordBreak
2424
}
2525

2626
prevWordBreak = m

src/matchers/word.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Deno.test(WordMatcher.name, async (t) => {
88
const replacer = '($<abbr>)'
99
const expected = '(Mon), (Tue), (Wed), (Thu), (Fri), (Sat), (Sun)'
1010

11-
for (let i = 0; i < 2; ++i) {
11+
for (let i = 0; i < 3; ++i) {
1212
assertEquals(matcher[Symbol.replace](input, replacer), expected)
1313
}
1414
})
@@ -17,7 +17,7 @@ Deno.test(WordMatcher.name, async (t) => {
1717
const input = '此地无银三百两'
1818
const expected = ['此地', '无', '银', '三百', '两']
1919

20-
for (let i = 0; i < 2; ++i) {
20+
for (let i = 0; i < 3; ++i) {
2121
assertEquals(matcher[Symbol.match](input), expected)
2222
}
2323
})

src/replace.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ export type NativeReplacerFn = (substring: string, ...args: unknown[]) => string
77
*
88
* @example Index
99
* ```ts
10+
* import { assertEquals } from '@std/assert'
1011
* assertEquals(
11-
* 'Hello world'.replace(/\w+/g, createReplacerFunction((m) => `${m.index}:${m[0]}`)),
12+
* 'Hello world'.replace(/\w+/g, convertReplacerFunction((m) => `${m.index}:${m[0]}`)),
1213
* '0:Hello 6:world',
1314
* )
1415
* ```
1516
* @example Named capture groups
1617
* ```ts
18+
* import { assertEquals } from '@std/assert'
1719
* assertEquals(
18-
* 'Hello world'.replace(/(?<initial>\w)\w+/g, createReplacerFunction((m) => m.groups!.initial)),
20+
* 'Hello world'.replace(/(?<initial>\w)\w+/g, convertReplacerFunction((m) => m.groups!.initial!)),
1921
* 'H w',
2022
* )
2123
* ```

0 commit comments

Comments
 (0)