Skip to content

Commit 4c3929e

Browse files
authored
Merge pull request #26 from cketti/StringBuilder_extensions
Add `StringBuilder` extension functions to set/insert/delete code points
2 parents 037c4b5 + eef026f commit 4c3929e

File tree

5 files changed

+422
-0
lines changed

5 files changed

+422
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [unreleased]
4+
### Added
5+
- Added `StringBuilder.setCodePointAt()`, `StringBuilder.insertCodePointAt()`, and `StringBuilder.deleteCodePointAt()`
6+
to both `kotlin-codepoints` and `kotlin-codepoints-deluxe`.
7+
38
## [0.6.1] - 2023-03-11
49
### Fixed
510
- `CharSequence.codePointCount()` now returns the correct result for a zero-length range.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package de.cketti.codepoints.deluxe
2+
3+
import de.cketti.codepoints.setCodePointAt as intSetCodePointAt
4+
import de.cketti.codepoints.insertCodePointAt as intInsertCodePointAt
5+
6+
/**
7+
* The code point at the specified index is set to `codePoint`.
8+
*
9+
* This sequence is altered to represent a new character sequence that is identical to the old character sequence,
10+
* except that the UTF-16 representation of the code point at position `index` is replaced by the UTF-16 representation
11+
* of `codePoint`.
12+
* If the old and the new code point differ in [charCount][CodePoint.charCount], the length of this character sequence
13+
* grows or shrinks by one character.
14+
*
15+
* @throws IndexOutOfBoundsException if [index] is out of bounds of this string builder.
16+
*/
17+
fun StringBuilder.setCodePointAt(index: Int, codePoint: CodePoint): StringBuilder {
18+
return intSetCodePointAt(index, codePoint.value)
19+
}
20+
21+
/**
22+
* Insert `codePoint` at the specified index.
23+
*
24+
* The UTF-16 representation of `codePoint` is inserted into this sequence at the position `index`, moving up any
25+
* characters originally above that position and increasing the length of this sequence by the code point's
26+
* [charCount][CodePoint.charCount].
27+
*
28+
* @throws IndexOutOfBoundsException if [index] is less than zero or greater than the length of this string builder.
29+
*/
30+
fun StringBuilder.insertCodePointAt(index: Int, codePoint: CodePoint): StringBuilder {
31+
return intInsertCodePointAt(index, codePoint.value)
32+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package de.cketti.codepoints.deluxe
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFailsWith
6+
7+
class StringBuilderExtensionsTest {
8+
@Test
9+
fun setCodePointAt() {
10+
val stringBuilder = StringBuilder().apply {
11+
append('a')
12+
append("\uD83E\uDD95")
13+
append('c')
14+
}
15+
16+
stringBuilder.setCodePointAt(0, 'A'.toCodePoint())
17+
assertEquals(stringBuilder.toString(), "A\uD83E\uDD95c")
18+
19+
stringBuilder.setCodePointAt(1, "\uD83E\uDD96".codePointAt(0))
20+
assertEquals(stringBuilder.toString(), "A\uD83E\uDD96c")
21+
22+
stringBuilder.setCodePointAt(3, 'C'.toCodePoint())
23+
assertEquals(stringBuilder.toString(), "A\uD83E\uDD96C")
24+
25+
stringBuilder.setCodePointAt(0, "\uD83E\uDD95".codePointAt(0))
26+
assertEquals(stringBuilder.toString(), "\uD83E\uDD95\uD83E\uDD96C")
27+
28+
stringBuilder.setCodePointAt(2, 'Y'.toCodePoint())
29+
assertEquals(stringBuilder.toString(), "\uD83E\uDD95YC")
30+
31+
stringBuilder.setCodePointAt(3, "\uD83E\uDD96".codePointAt(0))
32+
assertEquals(stringBuilder.toString(), "\uD83E\uDD95Y\uD83E\uDD96")
33+
34+
stringBuilder.setCodePointAt(2, "\uD83D\uDC4B".codePointAt(0))
35+
assertEquals(stringBuilder.toString(), "\uD83E\uDD95\uD83D\uDC4B\uD83E\uDD96")
36+
37+
stringBuilder.setCodePointAt(0, "\uD83D\uDC56".codePointAt(0))
38+
assertEquals(stringBuilder.toString(), "\uD83D\uDC56\uD83D\uDC4B\uD83E\uDD96")
39+
40+
stringBuilder.setCodePointAt(4, "\uD83D\uDC57".codePointAt(0))
41+
assertEquals(stringBuilder.toString(), "\uD83D\uDC56\uD83D\uDC4B\uD83D\uDC57")
42+
43+
stringBuilder.setCodePointAt(0, 'X'.toCodePoint())
44+
assertEquals(stringBuilder.toString(), "X\uD83D\uDC4B\uD83D\uDC57")
45+
46+
stringBuilder.setCodePointAt(3, 'Z'.toCodePoint())
47+
assertEquals(stringBuilder.toString(), "X\uD83D\uDC4BZ")
48+
}
49+
50+
@Test
51+
fun setCodePointAt_between_surrogate_pairs() {
52+
val stringBuilder = StringBuilder().apply {
53+
append("\uD83E\uDD95")
54+
}
55+
56+
stringBuilder.setCodePointAt(1, "\uD83E\uDD96".codePointAt(0))
57+
assertEquals(stringBuilder.toString(), "\uD83E\uD83E\uDD96")
58+
59+
stringBuilder.setCodePointAt(2, 'X'.toCodePoint())
60+
assertEquals(stringBuilder.toString(), "\uD83E\uD83EX")
61+
}
62+
63+
@Test
64+
fun setCodePointAt_unmatched_surrogates() {
65+
val stringBuilder = StringBuilder().apply {
66+
append("\uDD95\uD83E\uD83E")
67+
}
68+
69+
stringBuilder.setCodePointAt(0, 'X'.toCodePoint())
70+
assertEquals(stringBuilder.toString(), "X\uD83E\uD83E")
71+
72+
stringBuilder.setCodePointAt(1, 'Y'.toCodePoint())
73+
assertEquals(stringBuilder.toString(), "XY\uD83E")
74+
75+
stringBuilder.setCodePointAt(2, "\uD83D\uDC4B".codePointAt(0))
76+
assertEquals(stringBuilder.toString(), "XY\uD83D\uDC4B")
77+
}
78+
79+
@Test
80+
fun setCodePointAt_with_invalid_index() {
81+
val stringBuilder = StringBuilder().apply {
82+
append(" ")
83+
}
84+
85+
assertFailsWith<IndexOutOfBoundsException> {
86+
stringBuilder.setCodePointAt(-1, 'a'.toCodePoint())
87+
}
88+
89+
assertFailsWith<IndexOutOfBoundsException> {
90+
stringBuilder.setCodePointAt(1, 'a'.toCodePoint())
91+
}
92+
}
93+
94+
@Test
95+
fun insertCodePointAt() {
96+
val stringBuilder = StringBuilder()
97+
98+
stringBuilder.insertCodePointAt(0, "\uD83E\uDD95".codePointAt(0))
99+
assertEquals(stringBuilder.toString(), "\uD83E\uDD95")
100+
101+
stringBuilder.insertCodePointAt(0, 'a'.toCodePoint())
102+
assertEquals(stringBuilder.toString(), "a\uD83E\uDD95")
103+
104+
stringBuilder.insertCodePointAt(3, "\uD83E\uDD96".codePointAt(0))
105+
assertEquals(stringBuilder.toString(), "a\uD83E\uDD95\uD83E\uDD96")
106+
}
107+
108+
@Test
109+
fun insertCodePointAt_between_surrogate_pairs() {
110+
val stringBuilder = StringBuilder().apply {
111+
append("\uD83E\uDD95")
112+
}
113+
114+
stringBuilder.insertCodePointAt(1, "\uD83E\uDD96".codePointAt(0))
115+
assertEquals(stringBuilder.toString(), "\uD83E\uD83E\uDD96\uDD95")
116+
117+
stringBuilder.insertCodePointAt(2, 'a'.toCodePoint())
118+
assertEquals(stringBuilder.toString(), "\uD83E\uD83Ea\uDD96\uDD95")
119+
}
120+
121+
@Test
122+
fun insertCodePointAt_with_invalid_index() {
123+
val stringBuilder = StringBuilder()
124+
125+
assertFailsWith<IndexOutOfBoundsException> {
126+
stringBuilder.insertCodePointAt(-1, 'a'.toCodePoint())
127+
}
128+
129+
assertFailsWith<IndexOutOfBoundsException> {
130+
stringBuilder.insertCodePointAt(1, 'a'.toCodePoint())
131+
}
132+
}
133+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package de.cketti.codepoints
2+
3+
import de.cketti.codepoints.CodePoints.highSurrogate
4+
import de.cketti.codepoints.CodePoints.isBmpCodePoint
5+
import de.cketti.codepoints.CodePoints.isSupplementaryCodePoint
6+
import de.cketti.codepoints.CodePoints.lowSurrogate
7+
import de.cketti.codepoints.CodePoints.toChars
8+
9+
/**
10+
* The code point at the specified index is set to `codePoint`.
11+
*
12+
* This sequence is altered to represent a new character sequence that is identical to the old character sequence,
13+
* except that the UTF-16 representation of the code point at position `index` is replaced by the UTF-16 representation
14+
* of `codePoint`.
15+
* If the old and the new code point differ in [charCount][CodePoints.charCount], the length of this character sequence
16+
* grows or shrinks by one character.
17+
*
18+
* @throws IndexOutOfBoundsException if [index] is out of bounds of this string builder.
19+
*/
20+
fun StringBuilder.setCodePointAt(index: Int, codePoint: Int): StringBuilder = apply {
21+
val oldCodePoint = codePointAt(index)
22+
if (isBmpCodePoint(oldCodePoint)) {
23+
if (isBmpCodePoint(codePoint)) {
24+
set(index, codePoint.toChar())
25+
} else {
26+
set(index, highSurrogate(codePoint))
27+
insert(index + 1, lowSurrogate(codePoint))
28+
}
29+
} else {
30+
if (isBmpCodePoint(codePoint)) {
31+
set(index, codePoint.toChar())
32+
deleteAt(index + 1)
33+
} else {
34+
set(index, highSurrogate(codePoint))
35+
set(index + 1, lowSurrogate(codePoint))
36+
}
37+
}
38+
}
39+
40+
/**
41+
* Insert `codePoint` at the specified index.
42+
*
43+
* The UTF-16 representation of `codePoint` is inserted into this sequence at the position `index`, moving up any
44+
* characters originally above that position and increasing the length of this sequence by the code point's
45+
* [charCount][CodePoints.charCount].
46+
*
47+
* @throws IndexOutOfBoundsException if [index] is less than zero or greater than the length of this string builder.
48+
*/
49+
fun StringBuilder.insertCodePointAt(index: Int, codePoint: Int): StringBuilder = apply {
50+
if (isBmpCodePoint(codePoint)) {
51+
insert(index, codePoint.toChar())
52+
} else {
53+
insert(index, toChars(codePoint))
54+
}
55+
}
56+
57+
/**
58+
* Removes the code point at the specified index.
59+
*
60+
* If the code point at the position `index` is encoded using a surrogate pair, both `Char´s of the surrogate pair are
61+
* removed.
62+
*
63+
* @throws IndexOutOfBoundsException if [index] is out of bounds of this string builder.
64+
*/
65+
fun StringBuilder.deleteCodePointAt(index: Int): StringBuilder = apply {
66+
val codePoint = codePointAt(index)
67+
if (isBmpCodePoint(codePoint)) {
68+
deleteAt(index)
69+
} else {
70+
deleteRange(index, index + 2)
71+
}
72+
}

0 commit comments

Comments
 (0)