Skip to content

Commit 7f02811

Browse files
fzhinkinshanshin
andauthored
Add Sink.writeString(CharSequence) (#318)
Fixes #261 Co-authored-by: Sergey Shanshin <[email protected]>
1 parent 8dec7f5 commit 7f02811

File tree

6 files changed

+96
-17
lines changed

6 files changed

+96
-17
lines changed

core/api/kotlinx-io-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ public final class kotlinx/io/Utf8Kt {
195195
public static final fun readString (Lkotlinx/io/Source;)Ljava/lang/String;
196196
public static final fun readString (Lkotlinx/io/Source;J)Ljava/lang/String;
197197
public static final fun writeCodePointValue (Lkotlinx/io/Sink;I)V
198+
public static final fun writeString (Lkotlinx/io/Sink;Ljava/lang/CharSequence;II)V
198199
public static final fun writeString (Lkotlinx/io/Sink;Ljava/lang/String;II)V
200+
public static synthetic fun writeString$default (Lkotlinx/io/Sink;Ljava/lang/CharSequence;IIILjava/lang/Object;)V
199201
public static synthetic fun writeString$default (Lkotlinx/io/Sink;Ljava/lang/String;IIILjava/lang/Object;)V
200202
}
201203

core/api/kotlinx-io-core.klib.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ final fun (kotlinx.io/Sink).kotlinx.io/writeHexadecimalUnsignedLong(kotlin/Long)
9292
final fun (kotlinx.io/Sink).kotlinx.io/writeIntLe(kotlin/Int) // kotlinx.io/writeIntLe|[email protected](kotlin.Int){}[0]
9393
final fun (kotlinx.io/Sink).kotlinx.io/writeLongLe(kotlin/Long) // kotlinx.io/writeLongLe|[email protected](kotlin.Long){}[0]
9494
final fun (kotlinx.io/Sink).kotlinx.io/writeShortLe(kotlin/Short) // kotlinx.io/writeShortLe|[email protected](kotlin.Short){}[0]
95+
final fun (kotlinx.io/Sink).kotlinx.io/writeString(kotlin/CharSequence, kotlin/Int =..., kotlin/Int =...) // kotlinx.io/writeString|[email protected](kotlin.CharSequence;kotlin.Int;kotlin.Int){}[0]
9596
final fun (kotlinx.io/Sink).kotlinx.io/writeString(kotlin/String, kotlin/Int =..., kotlin/Int =...) // kotlinx.io/writeString|[email protected](kotlin.String;kotlin.Int;kotlin.Int){}[0]
9697
final fun (kotlinx.io/Sink).kotlinx.io/writeUByte(kotlin/UByte) // kotlinx.io/writeUByte|[email protected](kotlin.UByte){}[0]
9798
final fun (kotlinx.io/Sink).kotlinx.io/writeUInt(kotlin/UInt) // kotlinx.io/writeUInt|[email protected](kotlin.UInt){}[0]

core/common/src/Utf8.kt

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,31 @@ public fun Sink.writeCodePointValue(codePoint: Int): Unit =
162162
* @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.writeUtf8Sample
163163
*/
164164
@OptIn(DelicateIoApi::class)
165-
public fun Sink.writeString(string: String, startIndex: Int = 0, endIndex: Int = string.length): Unit =
166-
writeToInternalBuffer { it.commonWriteUtf8(string, startIndex, endIndex) }
165+
public fun Sink.writeString(string: String, startIndex: Int = 0, endIndex: Int = string.length) {
166+
checkBounds(string.length, startIndex, endIndex)
167+
168+
writeToInternalBuffer { it.commonWriteUtf8(startIndex, endIndex, string::get) }
169+
}
170+
171+
/**
172+
* Encodes the characters at [startIndex] up to [endIndex] from [chars] in UTF-8 and writes it to this sink.
173+
*
174+
* @param chars the string to be encoded.
175+
* @param startIndex the index (inclusive) of the first character to encode, 0 by default.
176+
* @param endIndex the index (exclusive) of a character past to a last character to encode, `chars.length` by default.
177+
*
178+
* @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [chars] indices.
179+
* @throws IllegalArgumentException when `startIndex > endIndex`.
180+
* @throws IllegalStateException when the sink is closed.
181+
*
182+
* @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.writeUtf8SeqSample
183+
*/
184+
@OptIn(DelicateIoApi::class)
185+
public fun Sink.writeString(chars: CharSequence, startIndex: Int = 0, endIndex: Int = chars.length) {
186+
checkBounds(chars.length, startIndex, endIndex)
187+
188+
writeToInternalBuffer { it.commonWriteUtf8(startIndex, endIndex, chars::get) }
189+
}
167190

168191
/**
169192
* Removes all bytes from this source, decodes them as UTF-8, and returns the string.
@@ -431,13 +454,11 @@ private fun Buffer.commonReadUtf8CodePoint(): Int {
431454
}
432455
}
433456

434-
private fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endIndex: Int) {
435-
checkBounds(string.length, beginIndex, endIndex)
436-
437-
// Transcode a UTF-16 Java String to UTF-8 bytes.
457+
private inline fun Buffer.commonWriteUtf8(beginIndex: Int, endIndex: Int, charAt: (Int) -> Char) {
458+
// Transcode a UTF-16 chars to UTF-8 bytes.
438459
var i = beginIndex
439460
while (i < endIndex) {
440-
var c = string[i].code
461+
var c = charAt(i).code
441462

442463
when {
443464
c < 0x80 -> {
@@ -452,7 +473,7 @@ private fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endIndex: In
452473
// Fast-path contiguous runs of ASCII characters. This is ugly, but yields a ~4x performance
453474
// improvement over independent calls to writeByte().
454475
while (i < runLimit) {
455-
c = string[i].code
476+
c = charAt(i).code
456477
if (c >= 0x80) break
457478
data[segmentOffset + i++] = c.toByte() // 0xxxxxxx
458479
}
@@ -487,7 +508,7 @@ private fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endIndex: In
487508
// c is a surrogate. Make sure it is a high surrogate & that its successor is a low
488509
// surrogate. If not, the UTF-16 is invalid, in which case we emit a replacement
489510
// character.
490-
val low = (if (i + 1 < endIndex) string[i + 1].code else 0)
511+
val low = (if (i + 1 < endIndex) charAt(i + 1).code else 0)
491512
if (c > 0xdbff || low !in 0xdc00..0xdfff) {
492513
writeByte('?'.code.toByte())
493514
i++

core/common/test/AbstractSinkTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,27 @@ abstract class AbstractSinkTest internal constructor(
400400
assertFailsWith<IllegalArgumentException> { sink.writeString("hello", startIndex = 6) }
401401
}
402402

403+
@Test
404+
fun writeCharSequenceFromIndex() {
405+
sink.writeString(StringBuilder("12345"), 3)
406+
sink.emit()
407+
assertEquals("45", data.readString())
408+
}
409+
410+
@Test
411+
fun writeCharSequenceFromRange() {
412+
sink.writeString(StringBuilder("0123456789"), 4, 7)
413+
sink.emit()
414+
assertEquals("456", data.readString())
415+
}
416+
417+
@Test
418+
fun writeCharSequenceWithInvalidIndexes() {
419+
assertFailsWith<IndexOutOfBoundsException> { sink.writeString(StringBuilder("hello"), startIndex = -1) }
420+
assertFailsWith<IndexOutOfBoundsException> { sink.writeString(StringBuilder("hello"), startIndex = 0, endIndex = 6) }
421+
assertFailsWith<IllegalArgumentException> { sink.writeString(StringBuilder("hello"), startIndex = 6) }
422+
}
423+
403424
@Test
404425
fun writeUByte() {
405426
sink.writeUByte(0xffu)

core/common/test/Utf8Test.kt

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,24 +202,40 @@ class Utf8Test {
202202
bufferWriteUtf8StringCheck(Segment.SIZE - 1)
203203
}
204204

205-
private fun bufferWriteUtf8StringCheck(prefixLength: Int) {
205+
@Test
206+
fun bufferWriteUtf8CharSequence() {
207+
bufferWriteUtf8StringCheck(0) { buffer, string -> buffer.writeString(StringBuilder(string)) }
208+
}
209+
210+
@Test
211+
fun bufferWriteUtf8CharSequenceCrossSegments() {
212+
bufferWriteUtf8StringCheck(Segment.SIZE - 1) { buffer, string ->
213+
buffer.writeString(StringBuilder(string))
214+
}
215+
}
216+
217+
private inline fun bufferWriteUtf8StringCheck(
218+
prefixLength: Int,
219+
writeAction: (Buffer, String) -> Unit = { b, s -> b.writeString(s) }
220+
) {
206221
val buffer = Buffer()
207-
buffer.assertUtf8StringEncoded("68656c6c6f", "hello", prefixLength)
222+
buffer.assertUtf8StringEncoded("68656c6c6f", "hello", prefixLength, writeAction)
208223
buffer.assertUtf8StringEncoded("cf87ceb5cf81ceb5cf84ceb9cf83cebccf8ccf82", "χερετισμός",
209-
prefixLength)
224+
prefixLength, writeAction)
210225
buffer.assertUtf8StringEncoded(
211226
"e18392e18390e1839be18390e183a0e183afe1839de18391e18390",
212227
"გამარჯობა",
213-
prefixLength
228+
prefixLength,
229+
writeAction
214230
)
215231
buffer.assertUtf8StringEncoded(
216232
"f093878bf0938bb4f09380a5",
217233
"\uD80C\uDDCB\uD80C\uDEF4\uD80C\uDC25",/* 𓇋𓋴𓀥, to hail, AN EGYPTIAN HIEROGLYPHIC DICTIONARY, p. 79b */
218-
prefixLength
234+
prefixLength, writeAction
219235
)
220236

221237
// two consecutive high surrogates, replace with '?'
222-
buffer.assertUtf8StringEncoded("3f3f", "\ud801\uD801", prefixLength)
238+
buffer.assertUtf8StringEncoded("3f3f", "\ud801\uD801", prefixLength, writeAction)
223239
}
224240

225241
@Test
@@ -452,9 +468,10 @@ class Utf8Test {
452468
assertEquals(expectedCodePoint, readCodePointValue())
453469
}
454470

455-
private fun Buffer.assertUtf8StringEncoded(expectedHex: String, string: String, prefixLength: Int = 0) {
471+
private inline fun Buffer.assertUtf8StringEncoded(expectedHex: String, string: String, prefixLength: Int = 0,
472+
writeAction: (Buffer, String) -> Unit) {
456473
write(ByteArray(prefixLength))
457-
writeString(string)
474+
writeAction(this, string)
458475
skip(prefixLength.toLong())
459476
assertArrayEquals(expectedHex.decodeHex(), readByteArray())
460477
}

core/common/test/samples/samples.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,23 @@ class KotlinxIoCoreCommonSamples {
337337
assertContentEquals(byteArrayOf(0xce.toByte(), 0x94.toByte()), buffer.readByteArray())
338338
}
339339

340+
@Test
341+
fun writeUtf8SeqSample() {
342+
val buffer = Buffer()
343+
344+
buffer.writeString(StringBuilder("hello"), startIndex = 1, endIndex = 4)
345+
assertContentEquals(
346+
byteArrayOf(
347+
'e'.code.toByte(),
348+
'l'.code.toByte(),
349+
'l'.code.toByte()
350+
), buffer.readByteArray()
351+
)
352+
353+
buffer.writeString(StringBuilder("Δ"))
354+
assertContentEquals(byteArrayOf(0xce.toByte(), 0x94.toByte()), buffer.readByteArray())
355+
}
356+
340357
@Test
341358
fun readUtf8() {
342359
val buffer = Buffer()

0 commit comments

Comments
 (0)