• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jackfirth / resyntax / #457

09 Jul 2026 07:15AM UTC coverage: 94.119% (+0.1%) from 93.976%
#457

push

cover

web-flow
Merge fde1192da into aa2655136

252 of 256 new or added lines in 9 files covered. (98.44%)

16387 of 17411 relevant lines covered (94.12%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.44
/grimoire/string-replacement.rkt
1
#lang racket/base
1✔
2

3
;; A string replacement is an operation that can be applied to a string which can insert, delete, and
4
;; move around the contents of the string.
5

6

7
(require racket/contract/base)
1✔
8

9

10
(provide
1✔
11
 (contract-out
1✔
12
  [string-replacement? (-> any/c boolean?)]
1✔
13
  [string-replacement
1✔
14
   (->i
1✔
15
    #:chaperone
1✔
16
    (#:start [start natural?]
1✔
17
     #:end [end natural?]
1✔
18
     #:contents [contents (sequence/c string-piece?)])
1✔
19
    #:pre/name (start end) "end cannot be before start" (<= start end)
1✔
20
    [_ string-replacement?])]
1✔
21
  [string-piece? (-> any/c boolean?)]
1✔
22
  [string-piece-span (-> string-piece? exact-nonnegative-integer?)]
1✔
23
  [string-replacement-start (-> string-replacement? natural?)]
1✔
24
  [string-replacement-original-end (-> string-replacement? natural?)]
1✔
25
  [string-replacement-original-span (-> string-replacement? natural?)]
1✔
26
  [string-replacement-new-end (-> string-replacement? natural?)]
1✔
27
  [string-replacement-new-span (-> string-replacement? natural?)]
1✔
28
  [string-replacement-contents (-> string-replacement? (listof string-piece?))]
1✔
29
  [string-replacement-preserved-locations (-> string-replacement? range-set?)]
1✔
30
  [string-replacement-overlaps? (-> string-replacement? string-replacement? boolean?)]
1✔
31
  [string-replacement-focus
1✔
32
   (->* (string-replacement? string?)
1✔
33
        (#:preserve-start (or/c exact-nonnegative-integer? #false)
1✔
34
         #:preserve-end (or/c exact-nonnegative-integer? #false))
1✔
35
        string-replacement?)]
1✔
36
  [string-replacement-union
1✔
37
   (->i ([replacement1 string-replacement?]
1✔
38
         [replacement2 string-replacement?])
1✔
39
        #:pre/name (replacement1 replacement2)
1✔
40
        "replacements must not overlap"
1✔
41
        (not (string-replacement-overlaps? replacement1 replacement2))
1✔
42
        [_ string-replacement?])]
1✔
43
  [union-into-string-replacement (reducer/c string-replacement? string-replacement?)]
1✔
44
  [string-replacement-new-text (-> string-replacement? string? immutable-string?)]
1✔
45
  [string-replacement-apply (-> string-replacement? string? immutable-string?)]
1✔
46
  [string-replacement-apply-to-file! (-> string-replacement? path-string? void?)]
1✔
47
  [inserted-string? (-> any/c boolean?)]
1✔
48
  [inserted-string (-> string? inserted-string?)]
1✔
49
  [inserted-string-contents (-> inserted-string? immutable-string?)]
1✔
50
  [copied-string? (-> any/c boolean?)]
1✔
51
  [copied-string
1✔
52
   (->i
1✔
53
    #:chaperone
1✔
54
    ([start natural?]
1✔
55
     [end natural?])
1✔
56
    #:pre/name (start end) "end cannot be before start" (<= start end)
1✔
57
    [_ copied-string?])]
1✔
58
  [copied-string-start (-> copied-string? natural?)]
1✔
59
  [copied-string-end (-> copied-string? natural?)]))
1✔
60

61

62
(require guard
1✔
63
         racket/file
1✔
64
         racket/match
1✔
65
         racket/math
1✔
66
         racket/sequence
1✔
67
         rebellion/base/comparator
1✔
68
         rebellion/base/immutable-string
1✔
69
         rebellion/base/option
1✔
70
         rebellion/base/range
1✔
71
         rebellion/collection/list
1✔
72
         rebellion/collection/range-set
1✔
73
         rebellion/private/static-name
1✔
74
         rebellion/streaming/reducer
1✔
75
         rebellion/streaming/transducer
1✔
76
         rebellion/type/record
1✔
77
         resyntax/grimoire/source)
1✔
78

79

80
(module+ test
81
  (require rackunit
82
           (submod "..")))
83

84

85
;@----------------------------------------------------------------------------------------------------
86

87

88
(define-record-type string-replacement
1✔
89
  (start original-end original-span new-end new-span required-length contents)
1✔
90
  #:omit-root-binding)
1✔
91

92

93
(define (string-replacement #:start start #:end end #:contents contents)
1✔
94
  (define content-list
1✔
95
    (for/fold ([accumulated '()]
1✔
96
               [previous #false]
1✔
97
               #:result
1✔
98
               (reverse (append (if previous (list previous) (list)) accumulated)))
1✔
99
              ([piece contents]
1✔
100
               #:when (positive? (string-piece-span piece)))
1✔
101
      (match (list previous piece)
1✔
102
        [(list #false _) (values accumulated piece)]
1✔
103
        [(list (inserted-string s1) (inserted-string s2))
1✔
104
         (values accumulated (inserted-string (string-append s1 s2)))]
1✔
105
        [(list (copied-string start1 end1) (copied-string start2 end2))
1✔
106
         #:when (equal? end1 start2)
1✔
107
         (values accumulated (copied-string start1 end2))]
1✔
108
        [(list _ _) (values (cons previous accumulated) piece)])))
1✔
109
  (define new-span (transduce content-list (mapping string-piece-span) #:into into-sum))
1✔
110
  (define max-end
1✔
111
    (transduce content-list
1✔
112
               (filtering copied-string?)
1✔
113
               (mapping copied-string-end)
1✔
114
               #:into (into-max)))
1✔
115
  (constructor:string-replacement
1✔
116
   #:start start
1✔
117
   #:original-end end
1✔
118
   #:original-span (- end start)
1✔
119
   #:new-end (+ start new-span)
1✔
120
   #:new-span new-span
1✔
121
   #:required-length (max end (option-get max-end 0))
1✔
122
   #:contents content-list))
1✔
123

124

125
(module+ test
126
  (test-case "string-replacement constructor"
127

128
    (test-case "should merge insertions"
129
      (define initial-pieces
130
        (list (inserted-string "aaa") (inserted-string "bbb") (inserted-string "ccc")))
131
      (define expected-pieces (list (inserted-string "aaabbbccc")))
132
      (define replacement
133
        (string-replacement
134
         #:start 0
135
         #:end 10
136
         #:contents initial-pieces))
137
      (check-equal? (string-replacement-contents replacement) expected-pieces))
138

139
    (test-case "should merge adjacent copied pieces"
140
      (define initial-pieces
141
        (list (copied-string 2 5) (copied-string 5 7) (copied-string 7 9)))
142
      (define expected-pieces (list (copied-string 2 9)))
143
      (define replacement
144
        (string-replacement
145
         #:start 0
146
         #:end 10
147
         #:contents initial-pieces))
148
      (check-equal? (string-replacement-contents replacement) expected-pieces))
149

150
    (test-case "should not merge non-adjacent copied pieces"
151
      (define initial-pieces
152
        (list (copied-string 2 5) (copied-string 6 8) (copied-string 10 12)))
153
      (define replacement
154
        (string-replacement
155
         #:start 0
156
         #:end 10
157
         #:contents initial-pieces))
158
      (check-equal? (string-replacement-contents replacement) initial-pieces))
159

160
    (test-case "should not merge inserted pieces with copied pieces"
161
      (define initial-pieces
162
        (list (inserted-string "aaa") (copied-string 5 7) (inserted-string "bbb")))
163
      (define replacement
164
        (string-replacement
165
         #:start 0
166
         #:end 10
167
         #:contents initial-pieces))
168
      (check-equal? (string-replacement-contents replacement) initial-pieces))
169

170
    (test-case "should merge inserted pieces when before copied piece"
171
      (define initial-pieces
172
        (list (inserted-string "aaa") (inserted-string "bbb") (copied-string 5 7)))
173
      (define expected-pieces (list (inserted-string "aaabbb") (copied-string 5 7)))
174
      (define replacement
175
        (string-replacement
176
         #:start 0
177
         #:end 10
178
         #:contents initial-pieces))
179
      (check-equal? (string-replacement-contents replacement) expected-pieces))
180

181
    (test-case "should merge inserted pieces when after copied piece"
182
      (define initial-pieces
183
        (list (copied-string 5 7) (inserted-string "ccc") (inserted-string "ddd")))
184
      (define expected-pieces (list (copied-string 5 7) (inserted-string "cccddd")))
185
      (define replacement
186
        (string-replacement
187
         #:start 0
188
         #:end 10
189
         #:contents initial-pieces))
190
      (check-equal? (string-replacement-contents replacement) expected-pieces))))
191

192

193
(define (string-replacement-length-change replacement)
1✔
194
  (- (string-replacement-new-span replacement) (string-replacement-original-span replacement)))
1✔
195

196

197
(define (string-replacement-range replacement)
1✔
198
  (closed-open-range
1✔
199
   (string-replacement-start replacement)
1✔
200
   (string-replacement-original-end replacement)
1✔
201
   #:comparator natural<=>))
1✔
202

203

204
(define (string-replacement-overlaps? replacement other-replacement)
1✔
205
  (range-overlaps?
1✔
206
   (string-replacement-range replacement) (string-replacement-range other-replacement)))
1✔
207

208

209
(struct string-piece () #:transparent)
1✔
210

211

212
(struct inserted-string string-piece (contents) #:transparent
1✔
213
  #:guard (λ (contents _) (string->immutable-string contents))
1✔
214
  #:property prop:custom-print-quotable 'never)
1✔
215

216

217
(struct copied-string string-piece (start end) #:transparent
1✔
218
  #:property prop:custom-print-quotable 'never)
1✔
219

220

221
(define (string-piece-span piece)
1✔
222
  (match piece
1✔
223
    [(inserted-string inserted-string) (string-length inserted-string)]
1✔
224
    [(copied-string start end) (- end start)]))
1✔
225

226

227
(define (replacement-string-drop-left piece amount)
1✔
228
  (match piece
1✔
229
    [(inserted-string s)
1✔
230
     (inserted-string (string->immutable-string (substring s amount)))]
1✔
231
    [(copied-string start end) (copied-string (+ start amount) end)]))
1✔
232

233

234
(define (replacement-string-drop-right piece amount)
1✔
235
  (match piece
×
236
    [(inserted-string s)
×
237
     (define new-length (- (string-length s) amount))
×
238
     (inserted-string (string->immutable-string (substring s 0 new-length)))]
×
239
    [(copied-string start end) (copied-string start (- end amount))]))
×
240

241

242
(define (string-replacement-new-text replacement original-string)
1✔
243
  (define immutable-original-string (string->immutable-string original-string))
1✔
244
  (define required-length (string-replacement-required-length replacement))
1✔
245
  (define original-length (string-length immutable-original-string))
1✔
246
  (unless (>= original-length required-length)
1✔
247
    (raise-arguments-error
1✔
248
     (name string-replacement-new-text)
1✔
249
     "string is not long enough"
1✔
250
     "string" immutable-original-string
1✔
251
     "string length" original-length
1✔
252
     "required length" required-length))
1✔
253
  (define content-length (string-replacement-new-span replacement))
1✔
254
  (define edited (make-string content-length))
1✔
255
  (for/fold ([start 0] #:result (void))
1✔
256
            ([piece (in-list (string-replacement-contents replacement))])
1✔
257
    (match piece
1✔
258
      [(inserted-string inserted)
1✔
259
       (string-copy! edited start inserted)
1✔
260
       (+ start (string-length inserted))]
1✔
261
      [(copied-string copy-start copy-end)
1✔
262
       (string-copy! edited start immutable-original-string copy-start copy-end)
1✔
263
       (+ start (- copy-end copy-start))]))
1✔
264
  (string->immutable-string edited))
1✔
265

266

267
(define (string-replacement-apply replacement string)
1✔
268
  (define immutable-string (string->immutable-string string))
1✔
269
  (define start (string-replacement-start replacement))
1✔
270
  (define end (string-replacement-original-end replacement))
1✔
271
  (define new-end (string-replacement-new-end replacement))
1✔
272
  (define contents (string-replacement-contents replacement))
1✔
273
  (define required-length (string-replacement-required-length replacement))
1✔
274
  (define original-length (string-length immutable-string))
1✔
275
  (unless (>= original-length required-length)
1✔
276
    (raise-arguments-error
1✔
277
     (name string-replacement-apply)
1✔
278
     "string is not long enough"
1✔
279
     "string" immutable-string
1✔
280
     "string length" original-length
1✔
281
     "required length" required-length))
1✔
282
  (define new-length (+ original-length (string-replacement-length-change replacement)))
1✔
283
  (define edited (make-string new-length))
1✔
284
  (string-copy! edited 0 immutable-string 0 start)
1✔
285
  (for/fold ([start start] #:result (void))
1✔
286
            ([piece (in-list contents)])
1✔
287
    (match piece
1✔
288
      [(inserted-string inserted)
1✔
289
       (string-copy! edited start inserted)
1✔
290
       (+ start (string-length inserted))]
1✔
291
      [(copied-string copy-start copy-end)
1✔
292
       (string-copy! edited start immutable-string copy-start copy-end)
1✔
293
       (+ start (- copy-end copy-start))]))
1✔
294
  (string-copy! edited new-end immutable-string end)
1✔
295
  (string->immutable-string edited))
1✔
296

297

298
(module+ test
299
  (test-case (name-string string-replacement-apply)
300

301
    (test-case "replace middle part"
302
      (define s "good morning and hello world")
303
      (define replacement-pieces
304
        (list
305
         (inserted-string "evening")
306
         (copied-string 12 17)
307
         (inserted-string "goodbye")))
308
      (define replacement
309
        (string-replacement
310
         #:start 5
311
         #:end 22
312
         #:contents replacement-pieces))
313
      (check-equal? (string-replacement-original-span replacement) 17)
314
      (check-equal? (string-replacement-new-span replacement) 19)
315
      (check-equal? (string-replacement-length-change replacement) 2)
316
      (check-equal? (string-replacement-new-end replacement) 24)
317
      (check-equal? (string-replacement-new-text replacement s) "evening and goodbye")
318
      (check-equal? (string-replacement-apply replacement s) "good evening and goodbye world"))
319

320
    (test-case "replace entire string"
321
      (define s "good morning and hello world")
322
      (define replacement
323
        (string-replacement #:start 0
324
                            #:end (string-length s)
325
                            #:contents (list (inserted-string "hi there"))))
326
      (check-equal? (string-replacement-apply replacement s) "hi there"))
327

328
    (test-case "string shorter than the replaced region"
329
      (define replacement
330
        (string-replacement #:start 0 #:end 10 #:contents (list (copied-string 0 2))))
331
      (check-exn #rx"string-replacement-apply:" (λ () (string-replacement-apply replacement "abc")))
332
      (check-exn #rx"string is not long enough"
333
                 (λ () (string-replacement-apply replacement "abc")))))
334

335
  (test-case (name-string string-replacement-new-text)
336
    (test-case "errors are reported under the right name"
337
      (define replacement
338
        (string-replacement #:start 0 #:end 2 #:contents (list (copied-string 10 20))))
339
      (check-exn #rx"string-replacement-new-text:"
340
                 (λ () (string-replacement-new-text replacement "ab"))))))
341

342

343
(define (string-replacement-apply-to-file! replacement path)
1✔
NEW
344
  (define replacement-text (string-replacement-apply replacement (source->string (file-source path))))
×
345
  (display-to-file replacement-text path #:mode 'text #:exists 'replace))
×
346

347

348
(define/guard (string-replacement-focus replacement original-string
1✔
349
                                            #:preserve-start [preserve-start #false]
1✔
350
                                            #:preserve-end [preserve-end #false])
1✔
351
  (define left-normalized
1✔
352
    (string-replacement-left-focus replacement original-string #:preserve-start preserve-start))
1✔
353
  (define left-reversed (string-replacement-reverse left-normalized original-string))
1✔
354
  (define reversed-original-string (string-reverse original-string))
1✔
355
  (define reversed
1✔
356
    (string-replacement-left-focus
1✔
357
     left-reversed
1✔
358
     reversed-original-string
1✔
359
     #:preserve-start (and preserve-end (- (string-length original-string) preserve-end))))
1✔
360
  (string-replacement-reverse reversed reversed-original-string))
1✔
361

362

363
(define/guard (string-replacement-left-focus replacement original-string
1✔
364
                                                 #:preserve-start [preserve-start #false])
×
365
  (define replaced (string-replacement-apply replacement original-string))
1✔
366
  (guard (not (equal? original-string replaced)) #:else replacement)
1✔
367
  (define actual-start
1✔
368
    (inexact->exact
1✔
369
     (min (or preserve-start +inf.0)
1✔
370
          (string-diff-start original-string replaced)
1✔
371
          (string-replacement-original-end replacement))))
1✔
372
  (define left-trimmed-pieces
1✔
373
    (let loop ([pieces (string-replacement-contents replacement)]
1✔
374
               [pos (string-replacement-start replacement)])
1✔
375
      (guarded-block
1✔
376
        (guard (< pos actual-start) #:else pieces)
1✔
377
        (guard-match (cons next-piece remaining) pieces #:else (list))
×
378
        (define piece-span (string-piece-span next-piece))
1✔
379
        (if (< (+ pos piece-span) actual-start)
1✔
380
            (loop remaining (+ pos piece-span))
1✔
381
            (cons (replacement-string-drop-left next-piece (- actual-start pos)) remaining)))))
1✔
382
  (string-replacement #:start actual-start
1✔
383
                      #:end (string-replacement-original-end replacement)
1✔
384
                      #:contents left-trimmed-pieces))
1✔
385

386

387
(define (string-replacement-reverse replacement original-string)
1✔
388
  (define new-start (- (string-length original-string) (string-replacement-original-end replacement)))
1✔
389
  (define new-end (+ new-start (string-replacement-original-span replacement)))
1✔
390
  (define new-contents
1✔
391
    (reverse
1✔
392
     (for/list ([piece (in-list (string-replacement-contents replacement))])
1✔
393
       (match piece
1✔
394
         [(inserted-string s) (inserted-string (string-reverse s))]
1✔
395
         [(copied-string start end)
1✔
396
          (define new-start (- (string-length original-string) end))
1✔
397
          (define new-end (+ new-start (- end start)))
1✔
398
          (copied-string new-start new-end)]))))
1✔
399
  (string-replacement #:start new-start #:end new-end #:contents new-contents))
1✔
400

401

402
(define (string-reverse s)
1✔
403
  (list->string (reverse (string->list s))))
1✔
404

405

406
(define (string-diff-start original new)
1✔
407
  (let loop ([i 0])
1✔
408
    (cond
1✔
409
      [(or (>= i (string-length original)) (>= i (string-length new))) #false]
1✔
410
      [(equal? (string-ref original i) (string-ref new i)) (loop (add1 i))]
1✔
411
      [else i])))
1✔
412

413

414
(define (string-diff-end original new)
1✔
415
  (let loop ([i 0])
1✔
416
    (guarded-block
1✔
417
      (guard (and (< i (string-length original)) (< i (string-length new))) #:else #false)
1✔
418
      (define orig-index (- (string-length original) i 1))
1✔
419
      (define new-index (- (string-length new) i 1))
1✔
420
      (if (equal? (string-ref original orig-index) (string-ref new new-index))
1✔
421
          (loop (add1 i))
1✔
422
          (+ orig-index 1)))))
1✔
423

424

425
(module+ test
426

427
  (test-case "string-diff-start"
428
    (check-false (string-diff-start "aaaaaa" "aaaaaa"))
429
    (check-equal? (string-diff-start "aaaaaa" "baaaaa") 0)
430
    (check-equal? (string-diff-start "aaaaaa" "abaaaa") 1)
431
    (check-equal? (string-diff-start "aaaaaa" "aabaaa") 2)
432
    (check-equal? (string-diff-start "aaaaaa" "aaabaa") 3)
433
    (check-equal? (string-diff-start "aaaaaa" "aaaaba") 4)
434
    (check-equal? (string-diff-start "aaaaaa" "aaaaab") 5)
435
    (check-equal? (string-diff-start "aaaaaa" "b") 0)
436
    (check-equal? (string-diff-start "aaaaaa" "ab") 1)
437
    (check-equal? (string-diff-start "aaaaaa" "aab") 2)
438
    (check-equal? (string-diff-start "aaaaaa" "aaab") 3)
439
    (check-equal? (string-diff-start "aaaaaa" "aaaab") 4)
440
    (check-equal? (string-diff-start "aaaa" "abaaa") 1)
441
    (check-equal? (string-diff-start "aaaa" "aaaba") 3)
442
    (check-equal? (string-diff-start "good morning and hello world"
443
                                     "good morning friend and hello world")
444
                  13))
445

446
  (test-case "string-diff-end"
447
    (check-false (string-diff-end "aaaaaa" "aaaaaa"))
448
    (check-equal? (string-diff-end "aaaaaa" "baaaaa") 1)
449
    (check-equal? (string-diff-end "aaaaaa" "abaaaa") 2)
450
    (check-equal? (string-diff-end "aaaaaa" "aabaaa") 3)
451
    (check-equal? (string-diff-end "aaaaaa" "aaabaa") 4)
452
    (check-equal? (string-diff-end "aaaaaa" "aaaaba") 5)
453
    (check-equal? (string-diff-end "aaaaaa" "aaaaab") 6)
454
    (check-equal? (string-diff-end "aaaaaa" "baaaa") 2)
455
    (check-equal? (string-diff-end "aaaaaa" "baaa") 3)
456
    (check-equal? (string-diff-end "aaaaaa" "baa") 4)
457
    (check-equal? (string-diff-end "aaaaaa" "ba") 5)
458
    (check-equal? (string-diff-end "aaaaaa" "b") 6)
459
    (check-equal? (string-diff-end "aaaa" "abaaa") 1)
460
    (check-equal? (string-diff-end "aaaa" "aaaba") 3)
461
    (check-equal? (string-diff-end "good morning and hello world"
462
                                   "good morning friend and hello world")
463
                  12))
464

465
  (test-case "string-replacement-focus"
466

467
    (test-case "empty replacement"
468
      (define s "good morning and hello world")
469
      (define replacement-pieces (list))
470
      (define replacement
471
        (string-replacement
472
         #:start 5
473
         #:end 5
474
         #:contents replacement-pieces))
475
      (check-equal? (string-replacement-focus replacement s) replacement))
476

477
    (test-case "insertion-only replacement"
478
      (define s "good morning and hello world")
479
      (define replacement-pieces (list (inserted-string "friend ")))
480
      (define replacement
481
        (string-replacement
482
         #:start 13
483
         #:end 13
484
         #:contents replacement-pieces))
485
      (check-equal? (string-replacement-focus replacement s) replacement))
486

487
    (test-case "redundant copied string before insertion"
488
      (define s "good morning and hello world")
489
      (define replacement-pieces (list (copied-string 5 13) (inserted-string "friend ")))
490
      (define replacement
491
        (string-replacement
492
         #:start 5
493
         #:end 13
494
         #:contents replacement-pieces))
495
      (check-equal? (string-replacement-focus replacement s)
496
                    (string-replacement #:start 13
497
                                        #:end 13
498
                                        #:contents (list (inserted-string "friend ")))))
499

500
    (test-case "redundant inserted string before insertion"
501
      (define s "good morning and hello world")
502
      (define replacement-pieces (list (inserted-string "morning ") (inserted-string "friend ")))
503
      (define replacement
504
        (string-replacement
505
         #:start 5
506
         #:end 13
507
         #:contents replacement-pieces))
508
      (check-equal? (string-replacement-focus replacement s)
509
                    (string-replacement #:start 13
510
                                        #:end 13
511
                                        #:contents (list (inserted-string "friend ")))))
512

513
    (test-case "redundant copied string after insertion"
514
      (define s "good morning and hello world")
515
      (define replacement-pieces (list (inserted-string "friend ") (copied-string 13 16)))
516
      (define replacement
517
        (string-replacement
518
         #:start 13
519
         #:end 16
520
         #:contents replacement-pieces))
521
      (check-equal? (string-replacement-focus replacement s)
522
                    (string-replacement #:start 13
523
                                        #:end 13
524
                                        #:contents (list (inserted-string "friend ")))))
525

526
    (test-case "redundant inserted string after insertion"
527
      (define s "good morning and hello world")
528
      (define replacement-pieces (list (inserted-string "friend ") (inserted-string "and")))
529
      (define replacement
530
        (string-replacement
531
         #:start 13
532
         #:end 16
533
         #:contents replacement-pieces))
534
      (check-equal? (string-replacement-focus replacement s)
535
                    (string-replacement #:start 13
536
                                        #:end 13
537
                                        #:contents (list (inserted-string "friend ")))))
538

539
    (test-case "normalizing by left-trimming a single size-increasing insertion"
540
      (define s "hello my big friend")
541
      (define s2 "hello my little friend")
542
      (define replacement-pieces (list (inserted-string "my little")))
543
      (define replacement (string-replacement #:start 6 #:end 12 #:contents replacement-pieces))
544
      (check-equal? (string-replacement-apply replacement s) s2)
545

546
      (define normalized (string-replacement-focus replacement s))
547

548
      (define expected
549
        (string-replacement #:start 9 #:end 12 #:contents (list (inserted-string "little"))))
550
      (check-equal? (string-replacement-apply expected s) s2)
551
      (check-equal? normalized expected))
552

553
    (test-case "normalizing by left-trimming a single size-decreasing insertion"
554
      (define s "hello my little friend")
555
      (define s2 "hello my big friend")
556
      (define replacement-pieces (list (inserted-string "my big")))
557
      (define replacement (string-replacement #:start 6 #:end 15 #:contents replacement-pieces))
558
      (check-equal? (string-replacement-apply replacement s) s2)
559

560
      (define normalized (string-replacement-focus replacement s))
561

562
      (define expected
563
        (string-replacement #:start 9 #:end 15 #:contents (list (inserted-string "big"))))
564
      (check-equal? (string-replacement-apply expected s) s2)
565
      (check-equal? normalized expected))
566

567
    (test-case "normalizing by right-trimming a single size-increasing insertion"
568
      (define s "hello my big friend")
569
      (define s2 "hello my little friend")
570
      (define replacement-pieces (list (inserted-string "little friend")))
571
      (define replacement (string-replacement #:start 9 #:end 19 #:contents replacement-pieces))
572
      (check-equal? (string-replacement-apply replacement s) s2)
573

574
      (define normalized (string-replacement-focus replacement s))
575

576
      (define expected
577
        (string-replacement #:start 9 #:end 12 #:contents (list (inserted-string "little"))))
578
      (check-equal? (string-replacement-apply expected s) s2)
579
      (check-equal? normalized expected))
580

581
    (test-case "normalizing by right-trimming a single size-decreasing insertion"
582
      (define s "hello my little friend")
583
      (define s2 "hello my big friend")
584
      (define replacement-pieces (list (inserted-string "big friend")))
585
      (define replacement (string-replacement #:start 9 #:end 22 #:contents replacement-pieces))
586
      (check-equal? (string-replacement-apply replacement s) s2)
587

588
      (define normalized (string-replacement-focus replacement s))
589

590
      (define expected
591
        (string-replacement #:start 9 #:end 15 #:contents (list (inserted-string "big"))))
592
      (check-equal? (string-replacement-apply expected s) s2)
593
      (check-equal? normalized expected))))
594

595

596
(define/guard (string-replacement-union replacement1 replacement2)
1✔
597
  (guard (<= (string-replacement-start replacement1) (string-replacement-start replacement2)) #:else
1✔
598
    (string-replacement-union replacement2 replacement1))
×
599
  (define piece-between
1✔
600
    (copied-string
1✔
601
     (string-replacement-original-end replacement1) (string-replacement-start replacement2)))
1✔
602
  (string-replacement
1✔
603
   #:start (string-replacement-start replacement1)
1✔
604
   #:end (string-replacement-original-end replacement2)
1✔
605
   #:contents
1✔
606
   (append
1✔
607
    (string-replacement-contents replacement1)
1✔
608
    (list piece-between)
1✔
609
    (string-replacement-contents replacement2))))
1✔
610

611

612
(define union-into-string-replacement
1✔
613
  (make-fold-reducer
1✔
614
   string-replacement-union
1✔
615
   (string-replacement #:start 0 #:end 0 #:contents (list))
1✔
616
   #:name (name union-into-string-replacement)))
1✔
617

618

619
(module+ test
620
  (test-case (name-string string-replacement-union)
621
    (define s "hello world")
622
    (define r1 (string-replacement #:start 0 #:end 5 #:contents (list (inserted-string "goodbye"))))
623
    (define r2 (string-replacement #:start 6 #:end 11 #:contents (list (inserted-string "friend"))))
624
    (check-false (string-replacement-overlaps? r1 r2))
625
    (define union (string-replacement-union r1 r2))
626
    (define expected
627
      (string-replacement
628
       #:start 0
629
       #:end 11
630
       #:contents (list (inserted-string "goodbye") (copied-string 5 6) (inserted-string "friend"))))
631
    (check-equal? union expected)
632
    (check-equal? (transduce (list r1 r2) #:into union-into-string-replacement) expected)))
633

634

635
(define (string-replacement-preserved-locations replacement)
1✔
636
  (define before-replacement
1✔
637
    (less-than-range (string-replacement-start replacement) #:comparator natural<=>))
1✔
638
  (define after-replacement
1✔
639
    (at-least-range (string-replacement-original-end replacement) #:comparator natural<=>))
1✔
640
  (for/fold ([ranges (range-set before-replacement after-replacement)])
1✔
641
            ([piece (in-list (string-replacement-contents replacement))]
1✔
642
             #:when (copied-string? piece))
1✔
643
    (match-define (copied-string start end) piece)
1✔
644
    (range-set-add ranges (closed-open-range start end #:comparator natural<=>))))
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc