-
Notifications
You must be signed in to change notification settings - Fork 15
/
String.x
1073 lines (941 loc) · 36.2 KB
/
String.x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* String is a well-known, immutable data type for holding textual information.
*/
const String
implements UniformIndexed<Int, Char>
implements Iterable<Char>
implements Sliceable<Int>
implements Stringable
implements Destringable {
// ----- constructors --------------------------------------------------------------------------
/**
* Construct a String from an array of characters.
*
* @param chars an array of characters to include in the String
*/
construct(Char[] chars) {
this.chars = chars.is(immutable Char[]) ? chars : chars.freeze();
}
@Override
construct(String! text) {
construct String(text.chars.reify(Constant));
}
// ----- properties ----------------------------------------------------------------------------
/**
* The array of characters that form the content of the String.
*/
Char[] chars;
/**
* A lazily calculated, cached hash code.
*/
private @Lazy Int64 hash.calc() {
Int64 hash = Int64:982_451_653; // start with a prime number
for (Char char : chars) {
hash = hash * 31 + char.toInt64();
}
return hash;
}
/**
* Support for link-time conditional evaluation: Determine if the name represented by this
* String is defined (an enabled option), within the [TypeSystem] of the current service.
*
* For example, to determine if the `TypeSystem` is formed using the "test" mode:
*
* if ("test".defined) {...}
*
* Or, to specify that a class is only present when in "test" mode:
*
* @Iff("test".defined) class ...
*/
Boolean defined.get() {
return this:service.typeSystem.definedNames.contains(this);
}
// ----- operators -----------------------------------------------------------------------------
/**
* Duplicate this String the specified number of times.
*
* @param n the number of times to duplicate this String
*
* @return a String that is the result of duplicating this String the specified number of times
*/
@Op("*") String! dup(Int n) {
if (n <= 1) {
return n == 1
? this
: "";
}
StringBuffer buf = new StringBuffer(size*n);
for (Int i = 0; i < n; ++i) {
appendTo(buf);
}
return buf.toString();
}
/**
* Add the String form of the passed object to this String, returning the result.
*
* @param o the object to render as a String and append to this String
*
* @return the concatenation of the String form of the passed object onto this String
*/
@Op("+") String! add(Object o) {
Int add = o.is(Stringable) ? o.estimateStringLength() : 0x0F;
StringBuffer buf = new StringBuffer(size + add);
return (buf + this + o).toString();
}
// ----- operations ----------------------------------------------------------------------------
/**
* Obtain a portion of this String, beginning at the specified character index, and continuing
* to the end of the String.
*
* To obtain a starting or middle portion of this string, use the [slice] method instead of this
* method.
*
* This method is tolerant of a starting index that is not within the bounds of this string. An
* index less than zero indicates the number of characters from the end of this string to take
* as the substring. An index is greater than or equal to the size of this string indicates that
* an empty string should be returned.
*
* @param startAt the index into this string of the first character of the string to return; a
* negative value indicates the number of characters from the end of this string
* to take as the substring
*
* @return the specified sub-string
*/
String! substring(Int startAt) {
if (startAt < 0) {
startAt += size;
}
return startAt <= 0 ? this :
startAt < size ? this[startAt ..< size] : "";
}
/**
* Obtain this String, but with its contents reversed.
*
* @return the reversed form of this String
*/
String! reversed() {
return size <= 1
? this
: this[size >.. 0];
}
/**
* Strip the whitespace off of the front and back of the string.
*
* @param whitespace a function that determines whether a character should be trimmed
*
* @return the contents of this String, but without any leading or trailing whitespace
*/
String trim(function Boolean(Char) whitespace = Char.isWhitespace) {
Int leading = 0;
val length = size;
while (leading < length && whitespace(this[leading])) {
++leading;
}
if (leading == length) {
return "";
}
Int trailing = 0;
while (whitespace(this[length-trailing-1])) {
++trailing;
}
return leading == 0 && trailing == 0
? this
: this[leading ..< size-trailing];
}
/**
* Count the number of occurrences of the specified character in this String.
*
* @param value the character to search for
*
* @return the number of times that the specified character occurs in this String
*/
Int count(Char value) {
Int count = 0;
for (Char ch : chars) {
if (ch == value) {
++count;
}
}
return count;
}
/**
* Split the String into an array of Strings, by finding each occurrence of the specified
* separator character within the String, and collecting the array of Strings demarcated by that
* character.
*
* @param separator the character that separates the items in the String
* @param omitEmpty (optional) indicates whether empty strings are to be omitted from the
* resulting array
* @param trim (optional) pass `True` to trim whitespace from each string in the
* resulting array, or pass a function that determines what whitespace
* characters to trim
*
* @return an immutable array of Strings
*/
String![] split(Char separator,
Boolean omitEmpty = False,
Boolean | function Boolean(Char) trim = False,
) {
Int length = size;
if (length == 0) {
return omitEmpty ? [] : [""];
}
String[] results = new String[];
if (trim == False) {
Int start = 0;
while (Int next := indexOf(separator, start)) {
if (start == next) {
if (!omitEmpty) {
results += "";
}
} else {
results += this[start ..< next];
}
start = next + 1;
}
// whatever remains after the last separator (or the entire String, if there were no
// separators found)
String last = substring(start);
if (!omitEmpty || !last.empty) {
results += last;
}
} else {
Char[] chars = this.chars;
Int offset = 0;
Int start = 0;
Boolean leading = True;
function Boolean(Char) whitespace = trim.is(function Boolean(Char)) ?: Char.isWhitespace;
while (True) {
if (offset >= length || chars[offset] == separator) {
Int end = offset-1;
while (end >= start && whitespace(chars[end])) {
--end;
}
if (end >= start) {
results += this[start..end];
} else if (!omitEmpty) {
results += "";
}
if (offset >= length) {
break;
}
start = offset + 1;
leading = True;
} else if (leading) {
if (whitespace(chars[offset])) {
start = offset + 1;
} else {
leading = False;
}
}
++offset;
}
}
return results.freeze(True);
}
/**
* Extract the value at the specified index of a delimited String, and returning an empty string
* for an index beyond the range of indexes in the delimited String. The behavior is the same as
* if the following code were executed:
*
* try {
* return split(separator)[index];
* } catch (OutOfBounds exception) {
* return "";
* }
*
* @param separator the character that separates the items in the String
* @param index specifies the _n_-th item in the delimited String
* @param defaultValue (optional) the value to return if the index is out of bounds
*
* @return the specified item from the delimited String, or the `defaultValue` if the index
* is out of bounds
*/
String extract(Char separator, Int index, String defaultValue="") {
if (size == 0 || index < 0) {
return size == index ? "" : defaultValue;
}
Int start = 0;
Int count = 0;
while (Int next := indexOf(separator, start)) {
if (count == index) {
return start == next ? "" : this[start ..< next];
}
start = next + 1;
++count;
}
return count == index
? substring(start)
: defaultValue;
}
/**
* Split the String into an map of String keys and String values, by finding each occurrence of
* the specified entry separator character within the String, and collecting the array of
* Strings demarcated by that character.
*
* @param kvSeparator the character that separates each key from its corresponding value in
* each entry of the "map entries" represented by the String
* @param entrySeparator the character that separates each entry from the next entry in the
* the sequence of "map entries" represented by the String
* @param whitespace a function that identifies white space to strip off of keys and values
*
* @return an array of Strings
*/
Map<String!, String!> splitMap(Char kvSeparator ='=',
Char entrySeparator =',',
function Boolean(Char) whitespace = ch -> ch.isWhitespace()) {
if (size == 0) {
return [];
}
return new StringMap(this, kvSeparator, entrySeparator, whitespace);
}
/**
* A lightweight, immutable Map implementation over a delimited String. Note that the
* implementation does not attempt to de-duplicate keys; the search for a specified key is
* sequential, e.g. a call to `get(k)` in the Map will return the value from the first entry
* with that key.
*/
protected static const StringMap(String data,
Char kvSep,
Char entrySep,
function Boolean(Char) whitespace)
implements Map<String, String>
incorporates collections.maps.KeySetBasedMap<String, String> {
@Override
conditional Orderer? ordered() {
return True, Null;
}
@Override
@Lazy Int size.calc() {
return data.count(entrySep) + 1;
}
@Override
Boolean empty.get() {
return False;
}
@Override
Boolean contains(String key) {
return find(key);
}
@Override
conditional String get(String key) {
if ((Int keyStart, Int sepOffset, Int valueEnd) := find(key)) {
return True, valueEnd > sepOffset+1 ? data[sepOffset >..< valueEnd].trim(whitespace) : "";
}
return False;
}
/**
* Internal helper to find a "key in a map" that is actually in the underlying String.
*/
protected conditional (Int keyStart, Int sepOffset, Int valueEnd) find(String key) {
String data = data;
Int length = data.size;
Int offset = 0;
Int keyLength = key.size;
Int keyOffset = 0;
EachEntry: while (offset + keyLength <= length) {
keyOffset = offset;
while (whitespace(data[offset])) {
if (++offset >= length) {
return False;
}
}
Boolean match = True;
for (Char keyChar : key) {
if (offset >= length) {
return False;
}
Char mapChar = data[offset++];
if (mapChar != keyChar) {
if (mapChar == entrySep) {
continue EachEntry;
} else {
match = False;
break;
}
}
}
while (offset < length && whitespace(data[offset])) {
++offset;
}
if (offset >= length) {
// key is at the very end, with no delimiter
return match, keyOffset, length, length;
}
Int sepOffset = offset;
Char sepChar = data[offset++];
if (match && sepChar == entrySep) {
// key is followed immediately by the entry separator, so value is blank
return True, keyOffset, sepOffset, sepOffset;
}
// find the separator offset
while (offset < length && data[offset] != entrySep) {
++offset;
}
if (match && sepChar == kvSep) {
// we did find the key, and now we have found the end of the value
return True, keyOffset, sepOffset, offset;
}
++offset;
}
return False;
}
@Override
@Lazy Set<String> keys.calc() {
return new KeySet();
}
const KeySet
implements Set<String> {
@Override
conditional Orderer? ordered() {
return True, Null;
}
@Override
Int size.get() {
return this.StringMap.size;
}
@Override
Boolean empty.get() {
return False;
}
@Override
Boolean contains(String value) {
return this.StringMap.contains(value);
}
@Override
Iterator<String> iterator() {
return new Iterator<String>() {
String data = this.StringMap.data;
Int offset = 0;
@Override
conditional String next() {
Int length = data.size;
if (offset >= length) {
return False;
}
// find the end of the entry
Int endEntry = length;
endEntry := data.indexOf(entrySep, offset);
// the delimiter between key and value is optional (i.e. value assumed
// to be "")
Int endKey = endEntry;
if (endKey := data.indexOf(kvSep, offset), endKey > endEntry) {
endKey = endEntry;
}
String key = data[offset ..< endKey].trim(whitespace);
offset = endEntry + 1;
return True, key;
}
};
}
}
}
/**
* Determine if this String _starts-with_ the specified character.
*
* @param ch a character to look for at the beginning of this String
*
* @return True iff this String starts-with the specified character
*/
Boolean startsWith(Char ch) {
return size > 0 && chars[0] == ch;
}
/**
* Determine if `this` String _starts-with_ `that` String. A String `this` of at least `n`
* characters "starts-with" another String `that` of exactly `n` elements iff, for each index
* `0..<n`, the character at the index in `this` String is equal to the character at the same
* index in `that` String.
*
* @param that a String to look for at the beginning of this String
*
* @return True iff this String starts-with that String
*/
Boolean startsWith(String that) {
return this.chars.startsWith(that.chars);
}
/**
* Determine if this String _ends-with_ the specified character.
*
* @param ch a character to look for at the end of this String
*
* @return True iff this String ends-with the specified character
*/
Boolean endsWith(Char ch) {
Int size = this.size;
return size > 0 && chars[size-1] == ch;
}
/**
* Determine if `this` String _ends-with_ `that` String. A String `this` of `m` characters
* "ends-with" another String `that` of `n` characters iff `n <= m` and, for each index `i`
* in the range `0..<n`, the character at the index `m-n+i` in `this` String is equal to the
* character at index `i` in `that` String.
*
* @param that a String to look for at the end of this String
*
* @return True iff this String ends-with that String
*/
Boolean endsWith(String that) {
return this.chars.endsWith(that.chars);
}
/**
* Look for the specified character starting at the specified index.
*
* @param value the character to search for
* @param startAt the first index to search from (optional)
*
* @return True iff this string contains the character, at or after the `startAt` index
* @return (conditional) the index at which the specified character was found
*/
conditional Int indexOf(Char value, Int startAt = 0) {
return chars.indexOf(value, startAt);
}
/**
* Look for the specified character starting at the specified index and searching backwards.
*
* @param value the character to search for
* @param startAt the index to start searching backwards from (optional)
*
* @return True iff this string contains the character, at or before the `startAt` index
* @return (conditional) the index at which the specified character was found
*/
conditional Int lastIndexOf(Char value, Int startAt = MaxValue) {
return chars.lastIndexOf(value, startAt);
}
/**
* Look for the specified `that` starting at the specified index.
*
* @param that the substring to search for
* @param startAt the first index to search from (optional)
*
* @return True iff this string contains the specified string, at or after the `startAt` index
* @return (conditional) the index at which the specified string was found
*/
conditional Int indexOf(String that, Int startAt = 0) {
Int thisLen = this.size;
Int thatLen = that.size;
// there has to be enough room to fit "that"
if (startAt > thisLen - thatLen) {
return False;
}
// can't start before the start of the string (at zero)
startAt = startAt.notLessThan(0);
// break out the special conditions (for small search strings)
if (thatLen <= 1) {
// assume that we can find the empty string wherever we look
if (thatLen == 0) {
return True, startAt;
}
// for single-character strings, use the more efficient single-character search
return indexOf(that[0], startAt);
}
// otherwise, brute force
Char first = that[0];
NextTry: while (Int index := indexOf(first, startAt)) {
if (index > thisLen - thatLen) {
return False;
}
for (Int of = 1; of < thatLen; ++of) {
if (this[index+of] != that[of]) {
startAt = index + 1;
continue NextTry;
}
}
return True, index;
}
return False;
}
/**
* Look for the specified `that` starting at the specified index and searching backwards.
*
* @param that the substring to search for
* @param startAt the first index to search backwards from (optional)
*
* @return True iff this string contains the specified string, at or before the `startAt` index
* @return (conditional) the index at which the specified string was found
*/
conditional Int lastIndexOf(String that, Int startAt = MaxValue) {
Int thisLen = this.size;
Int thatLen = that.size;
// there has to be enough room to fit "that"
if (startAt < thatLen) {
return False;
}
// can't start beyond the end of the string
startAt = startAt.notGreaterThan(thisLen);
// break out the special conditions (for small search strings)
if (thatLen <= 1) {
// assume that we can find the empty string wherever we look
if (thatLen == 0) {
return True, startAt;
}
// for single-character strings, use the more efficient single-character search
return lastIndexOf(that[0], startAt);
}
// otherwise, brute force
Char first = that[0];
NextTry: while (Int index := lastIndexOf(first, startAt)) {
if (index > thisLen - thatLen) {
startAt = index - 1;
continue NextTry;
}
for (Int of = 1; of < thatLen; ++of) {
if (this[index+of] != that[of]) {
startAt = index - 1;
continue NextTry;
}
}
return True, index;
}
return False;
}
/**
* Match all characters in this String to a regular expression pattern.
*
* @param pattern the regular expression to match
*
* @return True iff this entire String matches the pattern
* @return (conditional) a Matcher resulting from matching the regular expression with this String
*/
conditional Match matches(RegEx pattern) {
return pattern.match(this);
}
/**
* Match the start of this String to a regular expression pattern.
*
* Unlike the `match` method that matches the whole String value this method only matches the
* beginning, subsequent characters remaining after the pattern was matched are ignored.
*
* @param pattern the regular expression to match
*
* @return True iff this String starts with a sub-sequence that matches the regular expression
* @return (conditional) a Matcher resulting from matching the start of this String
*/
conditional Match prefixMatches(RegEx pattern) {
return pattern.matchPrefix(this);
}
/**
* Find the first sub-sequence of characters in this String that match a regular expression
* pattern.
*
* This method will start at the specified `offset` in this String and search for the first
* sub-sequence that matches the expression. Subsequent matches may be found by calling the
* `matchAny()` method with an offset equal to the `end` property of the returned `Match`.
*
* When searching for matches any non-matching sub-sequences of characters will be skipped.
*
* @param pattern the regular expression to match
* @param offset the position in the String to start searching from
*
* @return True iff the input contains a sub-sequence that matches the pattern
* @return (conditional) a Match resulting from matching the pattern
*/
conditional Match containsMatch(RegEx pattern, Int offset = 0) {
return pattern.find(this, offset);
}
/**
* Replaces every subsequence of this String that matches the pattern with the given
* replacement string; optionally starting at a given offset in this String.
*
* This method first resets this matcher. It then scans the input sequence looking for matches
* of the pattern. Characters that are not part of any match are appended directly to the
* result string; each match is replaced in the result by the replacement string.
*
* Note that backslashes `\` and dollar signs `$` in the replacement string may cause the
* results to be different than if it were being treated as a literal replacement string.
* Dollar signs may be treated as references to captured subsequences as described above, and
* backslashes are used to escape literal characters in the replacement string.
*
* Invoking this method changes this matcher's state. If the matcher is to be used in further
* matching operations then it should first be reset.
*
* @param pattern the regular expression to match
* @param replacement the replacement string
* @param offset the position in the String to start searching from
*
* @return A String constructed by replacing each matching subsequence by the replacement
* string
*/
String! replaceAll(RegEx pattern, String replacement, Int offset = 0) {
return pattern.replaceAll(this[offset ..< this.size], replacement);
}
/**
* Format this String into a left-justified String of the specified length, with the remainder
* of the new String filled with the specified character. If the specified length is shorter
* than the size of this String, then the result will be a truncated copy of this String,
* containing only the first _length_ characters of this String.
*
* @param length the size of the resulting String
* @param fill an optional fill character to use
*
* @return this String formatted into a left-justified String filled with the specified
* character
*/
String! leftJustify(Int length, Char fill = ' ') {
if (length <= 0) {
return "";
}
Int append = length - size;
return switch (append.sign) {
case Negative: this[0 ..< length];
case Zero : this;
case Positive: new StringBuffer(length)
.addAll(chars)
.addDup(fill, append)
.toString();
};
}
/**
* Format this String into a right-justified String of the specified length, with the remainder
* of the new String left-filled with the specified `fill` character. If the specified length is
* shorter than the size of this String, then the result will be a truncated copy of this
* String, containing only the last _length_ characters of this String.
*
* @param length the size of the resulting String
* @param fill an optional fill character to use
*
* @return this String formatted into a left-justified String filled with the specified
* character
*/
String! rightJustify(Int length, Char fill = ' ') {
if (length <= 0) {
return "";
}
Int append = length - size;
return switch (append.sign) {
case Negative: this.substring(-append);
case Zero : this;
case Positive: new StringBuffer(length)
.addDup(fill, append)
.addAll(chars)
.toString();
};
}
/**
* Format this String into the center of a String with the specified length, with the remainder
* of the new String left- and right-filled with the specified `fill` character. If the
* specified length is shorter than the size of this String, then the result will be a truncated copy of this
* String, containing only the first _length_ characters of this String.
*
* @param length the size of the resulting String
* @param fill an optional fill character to use
*
* @return this String formatted into a center-justified String filled with the specified
* character
*/
String! center(Int length, Char fill = ' ') {
if (length <= 0) {
return "";
}
Int append = length - size;
Int appendL = append >> 1;
return switch (append.sign) {
case Negative: this[0 ..< length];
case Zero : this;
case Positive: new StringBuffer(length)
.addDup(fill, appendL)
.addAll(chars)
.addDup(fill, append-appendL)
.toString();
};
}
/**
* Replace every appearance of the `match` substring in this String with the `replace` String.
*
* @param match the substring to be replaced
* @param replace the replacement String
*
* @return the resulting String
*/
String! replace(String! match, String! replace) {
Int replaceSize = replace.size;
if (Int matchOffset := indexOf(match)) {
Int thisSize = this.size;
Int matchSize = match.size;
Int startOffset = 0;
StringBuffer buffer = new StringBuffer(thisSize - matchSize + replaceSize);
do {
buffer.addAll(chars[startOffset ..< matchOffset]);
if (replaceSize > 0) {
buffer.addAll(replace.chars);
}
startOffset = matchOffset + matchSize;
} while (startOffset < thisSize, matchOffset := indexOf(match, startOffset));
return buffer.addAll(chars[startOffset ..< thisSize]).toString();
}
return this;
}
// ----- Iterable methods ----------------------------------------------------------------------
@Override
Int size.get() {
return chars.size;
}
@Override
Iterator<Char> iterator() {
return chars.iterator();
}
// ----- UniformIndexed methods ----------------------------------------------------------------
@Override
@Op("[]") Char getElement(Int index) {
return chars[index];
}
// ----- Sliceable methods ---------------------------------------------------------------------
@Override
@Op("[..]") String slice(Range<Int> indexes) {
return new String(chars[indexes]);
}
// ----- conversions ---------------------------------------------------------------------------
/**
* @return the UTF-8 conversion of this String into a byte array
*/
immutable Byte[] utf8() {
Int length = calcUtf8Length();
Byte[] bytes = new Byte[length];
Int actual = formatUtf8(bytes, 0);
assert actual == length;
return bytes.makeImmutable();
}
/**
* @return the characters of this String as an array
*/
immutable Char[] toCharArray() {
return chars;
}
/**
* @return a Reader over the characters of this String
*/
Reader toReader() {
return new io.CharArrayReader(chars);
}
@Override
String! toString() {
return this;
}
/**
* Convert this `String` to its all-upper-case form.
*
* @return the upper-case form of this `String`
*/
String! toUppercase() {
Each: for (Char char : chars) {
if (char != char.uppercase) {
Int checked = Each.count;
Char[] upperChars = new Char[size](offset ->
offset < checked ? chars[offset] : chars[offset].uppercase);
return new String(upperChars.makeImmutable());
}
}
return this;
}
/**
* Convert this `String` to its all-lower-case form.
*
* @return the lower-case form of this `String`
*/
String! toLowercase() {
Each: for (Char char : chars) {
if (char != char.lowercase) {
Int checked = Each.count;
Char[] lowerChars = new Char[size](offset ->
offset < checked ? chars[offset] : chars[offset].lowercase);
return new String(lowerChars.makeImmutable());
}
}
return this;
}
// ----- helper methods ------------------------------------------------------------------------
/**
* @return the minimum number of bytes necessary to encode the string in UTF8 format
*/
Int calcUtf8Length() {
Int len = 0;
for (Char ch : chars) {
len += ch.calcUtf8Length();
}
return len;
}
/**
* Encode this string into the passed byte array using the UTF8 format.
*
* @param bytes the byte array to write the UTF8 bytes into
* @param of the offset into the byte array to write the first byte
*
* @return the number of bytes used to encode the character in UTF8 format
*/
Int formatUtf8(Byte[] bytes, Int of) {
Int len = 0;
for (Char ch : chars) {
len += ch.formatUtf8(bytes, of + len);
}
return len;
}
/**
* Determine if the string needs to be escaped in order to be displayed.
*
* @return True iff the string should be escaped in order to be displayed
* @return (conditional) the number of characters in the escaped string
*/
conditional Int isEscaped() {
Int total = size;
for (Char ch : chars) {
if (Int n := ch.isEscaped()) {
total += n - 1;