Skip to content

[KT-78172] Replace local StringWriter with StringBuilder #5473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion libraries/stdlib/jvm/src/kotlin/io/ReadWrite.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private class LinesSequence(private val reader: BufferedReader) : Sequence<Strin
* @return the string with corresponding file content.
*/
public fun Reader.readText(): String {
val buffer = StringWriter()
val buffer = StringBuilder().asWriter()
copyTo(buffer)
return buffer.toString()
}
Expand Down Expand Up @@ -154,3 +154,56 @@ public inline fun URL.readText(charset: Charset = Charsets.UTF_8): String = read
*/
public fun URL.readBytes(): ByteArray = openStream().use { it.readBytes() }

/**
* Returns a [Writer] that wraps the specified [Appendable]. If it's already a [Writer], it is returned as is.
* If it also implements [Flushable] or [Closeable], the returned [Writer] will delegate to it.
*
* @return a [Writer] wrapping the specified [Appendable], not thread-safe.
*/
internal fun Appendable.asWriter(): Writer =
this as? Writer ?: AppendableWriter(this)

private class AppendableWriter(private val appendable: Appendable) : Writer() {

// Closeable
override fun close() {
(appendable as? Closeable)?.close()
}

// Flushable
override fun flush() {
(appendable as? Flushable)?.flush()
}

// Appendable
override fun append(csq: CharSequence?) = apply { appendable.append(csq) }
override fun append(c: Char) = apply { appendable.append(c) }
override fun append(csq: CharSequence?, start: Int, end: Int) = apply { appendable.append(csq, start, end) }

// Writer
override fun write(c: Int) {
appendable.append(c.toChar())
}

override fun write(str: String, off: Int, len: Int) {
if (off < 0 || len < 0 || off + len !in 0..str.length)
throw IndexOutOfBoundsException("off=$off, len=$len, str.size=${str.length}")

appendable.append(str, off, len)
}

override fun write(str: String) {
appendable.append(str)
}

override fun write(cbuf: CharArray, off: Int, len: Int) {
if (off < 0 || len < 0 || off + len !in 0..cbuf.size)
throw IndexOutOfBoundsException("off=$off, len=$len, cbuf.size=${cbuf.size}")

appendable.append(cbuf.asCharSequence(off, off + len))
}

override fun write(cbuf: CharArray) {
appendable.append(cbuf.asCharSequence())
}
}
28 changes: 28 additions & 0 deletions libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,34 @@ public actual fun CharSequence.repeat(n: Int): String {
}
}

/**
* Returns a [CharSequence] that wraps the specified [CharArray].
* All modifications to the original [CharArray] are reflected in the [CharSequence] and subsequences derived from it.
*
* @param startIndex the start index (inclusive) of the [CharArray], defaults to 0.
* @param endIndex the end index (exclusive) of the [CharArray], defaults to [CharArray.size].
* @return a [CharSequence] representing given [CharArray].
* @throws IndexOutOfBoundsException if [startIndex] or [endIndex] is out of bounds, or [startIndex] is greater than [endIndex].
*/
@JvmOverloads
internal fun CharArray.asCharSequence(startIndex: Int = 0, endIndex: Int = this.size): CharSequence =
if (startIndex == endIndex) "" else CharArraySequenceView(this, startIndex, endIndex)

private class CharArraySequenceView(
val charArray: CharArray,
val startIndex: Int,
val endIndex: Int,
) : CharSequence {
init {
if (startIndex !in charArray.indices || endIndex !in charArray.indices || startIndex > endIndex)
throw IndexOutOfBoundsException("startIndex: $startIndex, endIndex: $endIndex, charArray.size: ${charArray.size}")
}

override val length: Int get() = endIndex - startIndex
override fun get(index: Int): Char = if (index !in 0 until length) throw IndexOutOfBoundsException() else charArray[index]
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
CharArraySequenceView(charArray, startIndex + this.startIndex, endIndex + this.startIndex)
}

/**
* A Comparator that orders strings ignoring character case.
Expand Down
7 changes: 3 additions & 4 deletions libraries/stdlib/jvm/src/kotlin/util/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package kotlin

import java.io.PrintStream
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.internal.*

/**
Expand Down Expand Up @@ -52,11 +51,11 @@ public val Throwable.stackTrace: Array<StackTraceElement>
*/
@SinceKotlin("1.4")
public actual fun Throwable.stackTraceToString(): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
val buf = StringBuilder()
val pw = PrintWriter(buf.asWriter())
printStackTrace(pw)
pw.flush()
return sw.toString()
return buf.toString()
}

/**
Expand Down
103 changes: 103 additions & 0 deletions libraries/stdlib/jvm/test/io/ReadWrite.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ package test.io

import test.collections.behaviors.sequenceBehavior
import test.collections.compare
import java.io.Closeable
import kotlin.test.*
import java.io.File
import java.io.Flushable
import java.io.Reader
import java.io.StringReader
import java.io.StringWriter
import java.net.URL
import java.util.ArrayList

Expand Down Expand Up @@ -193,3 +196,103 @@ class LineIteratorTest {
}
}
}


class AppendableWriterTest {
@Test fun returnSelfIfIsWriter() {
val writer: Appendable = StringWriter()
val wrapped = writer.asWriter()
assertSame(writer, wrapped)
}

@Test fun write() {
val builder = StringBuilder()
val writer = builder.asWriter()

writer.write(72) // 'H'
writer.write("ello")
writer.write(" World!".toCharArray(), 1, 5) // "World"
writer.write(" Test".toCharArray())

assertEquals("Hello World Test", builder.toString())
}

@Test fun append() {
val builder = StringBuilder()
val writer = builder.asWriter()

writer.append("Hello")
writer.append(' ')
writer.append("World", 1, 3) // "or"
writer.append('!')

assertEquals("Hello or!", builder.toString())
}

@Test fun close() {
val mockCloseable = object : Appendable, Closeable {
var closed = false

override fun close() {
closed = true
}

override fun append(csq: CharSequence?) = this
override fun append(csq: CharSequence?, start: Int, end: Int) = this
override fun append(c: Char) = this
}

val writer = mockCloseable.asWriter()
writer.close()

assertTrue(mockCloseable.closed)
}

@Test
fun flush() {
val mockFlushable = object : Appendable, Flushable {
var flushed = false

override fun flush() {
flushed = true
}

override fun append(csq: CharSequence?) = this
override fun append(csq: CharSequence?, start: Int, end: Int) = this
override fun append(c: Char) = this
}

val writer = mockFlushable.asWriter()
writer.flush()

assertTrue(mockFlushable.flushed)
}

@Test fun writeWithExceptions() {
val writer = StringBuilder().asWriter()

assertFailsWith<IndexOutOfBoundsException> {
writer.write("test", -1, 2)
}

assertFailsWith<IndexOutOfBoundsException> {
writer.write("test".toCharArray(), -1, 2)
}

assertFailsWith<IndexOutOfBoundsException> {
writer.write("test", 1, -1)
}

assertFailsWith<IndexOutOfBoundsException> {
writer.write("test".toCharArray(), 1, -1)
}

assertFailsWith<IndexOutOfBoundsException> {
writer.write("test", 2, 3)
}

assertFailsWith<IndexOutOfBoundsException> {
writer.write("test".toCharArray(), 2, 3)
}
}
}
104 changes: 104 additions & 0 deletions libraries/stdlib/jvm/test/text/StringJVMTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,107 @@ class StringJVMTest {
assertTrue(platformNull<String>().equals(platformNull<String>(), ignoreCase = false))
}
}


class CharArraySequenceViewTest {

private val testArray = charArrayOf('a', 'b', 'c', 'd', 'e')

@Test
fun normalInit() {
val sequence = testArray.asCharSequence()

assertEquals(5, sequence.length)
assertEquals('a', sequence[0])
assertEquals('e', sequence[4])
}

@Test
fun normalInitWithRange() {
val sequence = testArray.asCharSequence(1, 4)

assertEquals(3, sequence.length)
assertEquals('b', sequence[0])
assertEquals('d', sequence[2])
}

@Test
fun reflectChanges() {
val array = charArrayOf('x', 'y', 'z')
val sequence = array.asCharSequence()

array[1] = 'w'

assertEquals('w', sequence[1])
}

@Test
fun illegalInit() {
assertFailsWith<IndexOutOfBoundsException> {
testArray.asCharSequence(startIndex = -1)
}

assertFailsWith<IndexOutOfBoundsException> {
testArray.asCharSequence(endIndex = 6)
}

assertFailsWith<IndexOutOfBoundsException> {
testArray.asCharSequence(startIndex = 3, endIndex = 2)
}
}

@Test
fun subSequenceRelatesToOriginal() {
val parent = testArray.asCharSequence(1, 5) // 'b', 'c', 'd', 'e'
val sub = parent.subSequence(1, 3) // 'c', 'd'

assertEquals(2, sub.length)
assertEquals('c', sub[0])
assertEquals('d', sub[1])
}

@Test
fun illegalSubSequence() {
val parent = testArray.asCharSequence(1, 4)

assertFailsWith<IndexOutOfBoundsException> {
parent.subSequence(-1, 2)
}

assertFailsWith<IndexOutOfBoundsException> {
parent.subSequence(1, 4)
}

assertFailsWith<IndexOutOfBoundsException> {
parent.subSequence(2, 1)
}
}

@Test
fun illegalCharAt() {
val sequence = testArray.asCharSequence(1, 3)

assertFailsWith<IndexOutOfBoundsException> {
sequence[-1]
}

assertFailsWith<IndexOutOfBoundsException> {
sequence[2]
}
}

@Test
fun emptyArrayInit() {
val emptyArray = charArrayOf()
val sequence = emptyArray.asCharSequence()

assertEquals(0, sequence.length)
}

@Test
fun emptyRangeInit() {
val sequence = testArray.asCharSequence(2, 2)

assertEquals(0, sequence.length)
}
}