Skip to content

Commit

Permalink
Simplify Slice to be as small as possible.
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg committed Dec 18, 2023
1 parent 1b6d271 commit 1d77e6f
Show file tree
Hide file tree
Showing 5 changed files with 18 additions and 315 deletions.
249 changes: 14 additions & 235 deletions selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Slice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,18 @@
*/
package com.diffplug.selfie

import kotlin.jvm.JvmStatic
import kotlin.math.min

/**
* A [CharSequence] which can efficiently subdivide and append itself.
*
* Equal only to other [Slice] with the same [Slice.toString]. Use [Slice.sameAs] to compare with
* other kinds of [CharSequence].
*/
internal expect fun groupImpl(slice: Slice, matchResult: MatchResult, group: Int): Slice

internal class Slice
private constructor(val base: CharSequence, val startIndex: Int, val endIndex: Int) : CharSequence {
internal class Slice(val base: String, val startIndex: Int = 0, val endIndex: Int = base.length) :
CharSequence {
init {
require(base is StringBuilder || base is String)
require(0 <= startIndex)
require(startIndex <= endIndex)
require(endIndex <= base.length)
}
override val length: Int
get() = endIndex - startIndex
override fun get(index: Int): Char = base[startIndex + index]
override fun subSequence(start: Int, end: Int): Slice {
return Slice(base, startIndex + start, startIndex + end)
}

/** Returns a Slice representing the given group within the given match */
fun group(matchResult: MatchResult, group: Int): Slice = groupImpl(this, matchResult, group)
override fun subSequence(start: Int, end: Int): Slice =
Slice(base, startIndex + start, startIndex + end)

/** Same behavior as [String.trim]. */
fun trim(): Slice {
Expand All @@ -57,89 +41,6 @@ private constructor(val base: CharSequence, val startIndex: Int, val endIndex: I
return if (start > 0 || end < length) subSequence(start, end) else this
}
override fun toString() = base.subSequence(startIndex, endIndex).toString()
fun concat(other: Slice): Slice =
if (this.isEmpty()) {
other
} else if (other.isEmpty()) {
this
} else if (base === other.base && endIndex == other.startIndex) {
Slice(base, startIndex, other.endIndex)
} else {
val builder: StringBuilder
val start: Int
val end: Int
if (base is StringBuilder && endIndex == base.length) {
builder = base
start = startIndex
end = endIndex + other.length
} else {
builder = StringBuilder(length + other.length)
builder.append(this)
start = 0
end = length + other.length
}
other.appendThisTo(builder)
Slice(builder, start, end)
}

/** append(this) but taking advantage of fastpath where possible */
private fun appendThisTo(builder: StringBuilder) {
if (startIndex == 0 && endIndex == base.length) {
// there is a fastpath for adding a full string and for adding a full StringBuilder
if (base is String) {
builder.append(base)
} else {
builder.append(base as StringBuilder)
}
} else {
builder.append(this)
}
}
fun concat(other: String): Slice {
if (base is String && endIndex + other.length <= base.length) {
for (i in other.indices) {
if (base[i + endIndex] != other[i]) {
return concat(of(other))
}
}
return Slice(base, startIndex, endIndex + other.length)
}
return concat(of(other))
}
fun concatAnchored(other: Slice): Slice {
val result = concat(other)
if (result.base !== base) {
throw concatRootFailure(other)
}
return result
}
fun concatAnchored(other: String): Slice {
val result = concat(other)
if (result.base !== base) {
throw concatRootFailure(other)
}
return result
}
private fun concatRootFailure(other: CharSequence): IllegalArgumentException {
val maxChange = min(other.length, base.length - endIndex)
if (maxChange == 0) {
return IllegalArgumentException(
"Could not perform anchored concat because we are already at the end of the root ${visualize(base)}")
}
var firstChange = 0
while (firstChange < maxChange) {
if (base[endIndex + firstChange] != other[firstChange]) {
break
}
++firstChange
}
return IllegalArgumentException(
"""
This ends with '${visualize(base.subSequence(endIndex, endIndex + firstChange + 1))}'
cannot concat '${visualize(other.subSequence(firstChange, firstChange + 1))}
"""
.trimIndent())
}
fun sameAs(other: CharSequence): Boolean {
if (length != other.length) {
return false
Expand All @@ -151,39 +52,12 @@ private constructor(val base: CharSequence, val startIndex: Int, val endIndex: I
}
return true
}
fun startsWith(prefix: CharSequence): Boolean {
if (length < prefix.length) {
return false
}
for (i in 0 until prefix.length) {
if (get(i) != prefix[i]) {
return false
}
}
return true
}
fun endsWith(suffix: CharSequence): Boolean {
if (length < suffix.length) {
return false
}
val offset = length - suffix.length
for (i in 0 until suffix.length) {
if (get(i + offset) != suffix[i]) {
return false
}
}
return true
}
fun indexOf(lookingFor: String, startOffset: Int = 0): Int {
val result =
if (base is String) base.indexOf(lookingFor, startIndex + startOffset)
else (base as StringBuilder).indexOf(lookingFor, startIndex + startOffset)
val result = base.indexOf(lookingFor, startIndex + startOffset)
return if (result == -1 || result >= endIndex) -1 else result - startIndex
}
fun indexOf(lookingFor: Char, startOffset: Int = 0): Int {
val result =
if (base is String) base.indexOf(lookingFor, startIndex + startOffset)
else (base as StringBuilder).indexOf(lookingFor, startIndex + startOffset)
val result = base.indexOf(lookingFor, startIndex + startOffset)
return if (result == -1 || result >= endIndex) -1 else result - startIndex
}
/** Returns a slice at the nth line. Handy for expanding the slice from there. */
Expand All @@ -195,130 +69,35 @@ private constructor(val base: CharSequence, val startIndex: Int, val endIndex: I
require(lineStart >= 0) { "This string has only ${i - 1} lines, not $count" }
++lineStart
}
var lineEnd = indexOf('\n', lineStart)
val lineEnd = indexOf('\n', lineStart)
return if (lineEnd == -1) {
Slice(base, startIndex + lineStart, endIndex)
} else {
Slice(base, startIndex + lineStart, startIndex + lineEnd)
}
}

/**
* Returns a Slice which represents everything from the start of this string until `lookingFor` is
* found. If the string is never found, returns this.
*/
fun until(lookingFor: String): Slice {
val idx = indexOf(lookingFor)
return if (idx == -1) this else subSequence(0, idx)
}

/**
* Asserts that the other string was generated from a call to [.until], and then returns a new
* Slice representing everything after that.
*/
fun after(other: Slice): Slice {
if (other.isEmpty()) {
return this
}
require(other.base === base && other.startIndex == startIndex && other.endIndex <= endIndex) {
"'${visualize(other)}' was not generated by `until` on '${visualize(this)}'"
}
return Slice(base, other.endIndex, endIndex)
}

/**
* Returns the line number of the start of this string. Throws an exception if this isn't based on
* a string any longer, because non-contiguous StringPools have been concatenated.
*/
fun baseLineNumberStart(): Int {
return baseLineNumberOfOffset(startIndex)
}

/**
* Returns the line number of the end of this string. Throws an exception if this isn't based on a
* string any longer, because non-contiguous Slices have been concatenated.
*/
fun baseLineNumberEnd(): Int {
return baseLineNumberOfOffset(endIndex)
}
private fun baseLineNumberOfOffset(idx: Int): Int {
assertStringBased()
var lineNumber = 1
for (i in 0 until base.length) {
if (base[i] == '\n') {
++lineNumber
override fun equals(other: Any?) =
if (this === other) {
true
} else if (other is Slice) {
sameAs(other)
} else {
false
}
}
return lineNumber
}
private fun assertStringBased() {
check(base is String) {
"When you call concat on non-contiguous parts, you lose the connection to the original String."
}
}
override fun equals(anObject: Any?): Boolean {
if (this === anObject) {
return true
} else if (anObject is Slice) {
return sameAs(anObject)
}
return false
}
override fun hashCode(): Int {
var h = 0
for (i in indices) {
h = 31 * h + get(i).code
}
return h
}
fun endsAtSamePlace(other: Slice): Boolean {
check(base === other.base)
return endIndex == other.endIndex
}

/** Returns the underlying string with this slice replaced by the given string. */
fun replaceSelfWith(s: String): String {
check(base is String)
val deltaLength = s.length - length
val builder = StringBuilder(base.length + deltaLength)
builder.appendRange(base, 0, startIndex)
builder.append(s)
builder.appendRange(base, endIndex, base.length)
return builder.toString()
}

companion object {
@JvmStatic
fun of(base: String, startIndex: Int = 0, endIndex: Int = base.length): Slice {
return Slice(base, startIndex, endIndex)
}
fun concatAll(vararg poolStringsOrStrings: CharSequence): Slice {
if (poolStringsOrStrings.isEmpty()) {
return empty()
}
var total = asPool(poolStringsOrStrings[0])
for (i in 1 until poolStringsOrStrings.size) {
val next = poolStringsOrStrings[i]
total = if (next is String) total.concat(next) else total.concat(next as Slice)
}
return total
}
private fun visualize(input: CharSequence): String {
return input
.toString()
.replace("\n", "")
.replace("\r", "")
.replace(" ", "·")
.replace("\t", "»")
}
private fun asPool(sequence: CharSequence): Slice {
return if (sequence is Slice) sequence else of(sequence as String)
}

/** Returns the empty Slice. */
@JvmStatic
fun empty(): Slice {
return Slice("", 0, 0)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ package com.diffplug.selfie
*/
class SourceFile(val filename: String, content: String) {
private val unixNewlines = content.indexOf('\r') == -1
private var contentSlice = Slice.of(content.efficientReplace("\r\n", "\n"))
private var contentSlice = Slice(content.efficientReplace("\r\n", "\n"))
/**
* Returns the content of the file, possibly modified by
* [ToBeLiteral.setLiteralAndGetNewlineDelta].
Expand All @@ -44,7 +44,7 @@ class SourceFile(val filename: String, content: String) {
val encoded = literalValue.format.encode(literalValue.actual)
val existingNewlines = slice.count { it == '\n' }
val newNewlines = encoded.count { it == '\n' }
contentSlice = Slice.of(slice.replaceSelfWith(".toBe($encoded)"))
contentSlice = Slice(slice.replaceSelfWith(".toBe($encoded)"))
return newNewlines - existingNewlines
}

Expand Down
21 changes: 2 additions & 19 deletions selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SliceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,10 @@ import io.kotest.matchers.shouldBe
import kotlin.test.Test

class SliceTest {
@Test
fun afterTest() {
val abcdef = Slice.of("abcdef")
val untilA = abcdef.until("a")
untilA.toString() shouldBe ""
abcdef.after(untilA).toString() shouldBe "abcdef"
val untilC = abcdef.until("c")
untilC.toString() shouldBe "ab"
abcdef.after(untilC).toString() shouldBe "cdef"
val untilF = abcdef.until("f")
untilF.toString() shouldBe "abcde"
abcdef.after(untilF).toString() shouldBe "f"
val untilZ = abcdef.until("z")
untilZ.toString() shouldBe "abcdef"
abcdef.after(untilZ).toString() shouldBe ""
}

@Test
fun unixLine() {
Slice.of("A single line").unixLine(1).toString() shouldBe "A single line"
val oneTwoThree = Slice.of("\nI am the first\nI, the second\n\nFOURTH\n")
Slice("A single line").unixLine(1).toString() shouldBe "A single line"
val oneTwoThree = Slice("\nI am the first\nI, the second\n\nFOURTH\n")
oneTwoThree.unixLine(1).toString() shouldBe ""
oneTwoThree.unixLine(2).toString() shouldBe "I am the first"
oneTwoThree.unixLine(3).toString() shouldBe "I, the second"
Expand Down
29 changes: 0 additions & 29 deletions selfie-lib/src/jsMain/kotlin/com/diffplug/selfie/Slice.js.kt

This file was deleted.

Loading

0 comments on commit 1d77e6f

Please sign in to comment.