Skip to content

Commit

Permalink
Barebones of int selfie (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Dec 18, 2023
2 parents 21d22e7 + 1d77e6f commit 8e73bad
Show file tree
Hide file tree
Showing 14 changed files with 409 additions and 322 deletions.
50 changes: 50 additions & 0 deletions selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Literals.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.selfie

class LiteralValue<T : Any>(val expected: T?, val actual: T, val format: LiteralFormat<T>) {
fun encodedActual(): String = format.encode(actual)
}

interface LiteralFormat<T : Any> {
fun encode(value: T): String
fun parse(str: String): T
}

class IntFormat : LiteralFormat<Int> {
override fun encode(value: Int): String {
// TODO: 1000000 is hard to read, 1_000_000 is much much better
return value.toString()
}
override fun parse(str: String): Int {
return str.replace("_", "").toInt()
}
}

class StrFormat : LiteralFormat<String> {
override fun encode(value: String): String {
if (!value.contains("\n")) {
// TODO: replace \t, maybe others...
return "\"" + value.replace("\"", "\\\"") + "\""
} else {
// TODO: test! It's okay to assume Java 15+ for now
return "\"\"\"\n" + value + "\"\"\""
}
}
override fun parse(str: String): String {
TODO("Harder than it seems!")
}
}
270 changes: 36 additions & 234 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

class Slice private constructor(val base: CharSequence, val startIndex: Int, val endIndex: Int) :
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 @@ class Slice private constructor(val base: CharSequence, val startIndex: Int, val
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,151 +52,52 @@ class Slice private constructor(val base: CharSequence, val startIndex: Int, val
}
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): Int {
val result =
if (base is String) base.indexOf(lookingFor, startIndex)
else {
(base as StringBuilder).indexOf(lookingFor, startIndex)
}
fun indexOf(lookingFor: String, startOffset: Int = 0): Int {
val result = base.indexOf(lookingFor, startIndex + startOffset)
return if (result == -1 || result >= endIndex) -1 else result - startIndex
}
fun indexOf(lookingFor: Char): Int {
val result =
if (base is String) base.indexOf(lookingFor, startIndex)
else {
(base as StringBuilder).indexOf(lookingFor.toString(), startIndex)
}
fun indexOf(lookingFor: Char, startOffset: Int = 0): Int {
val result = base.indexOf(lookingFor, startIndex + startOffset)
return if (result == -1 || result >= endIndex) -1 else result - startIndex
}

/**
* 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)}'"
/** Returns a slice at the nth line. Handy for expanding the slice from there. */
fun unixLine(count: Int): Slice {
check(count > 0)
var lineStart = 0
for (i in 1 until count) {
lineStart = indexOf('\n', lineStart)
require(lineStart >= 0) { "This string has only ${i - 1} lines, not $count" }
++lineStart
}
val lineEnd = indexOf('\n', lineStart)
return if (lineEnd == -1) {
Slice(base, startIndex + lineStart, endIndex)
} else {
Slice(base, startIndex + lineStart, startIndex + lineEnd)
}
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
}

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)
}
/** Returns the underlying string with this slice replaced by the given string. */
fun replaceSelfWith(s: String): 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ data class Snapshot(
interface Snapshotter<T> {
fun snapshot(value: T): Snapshot
}
private fun String.efficientReplace(find: String, replaceWith: String): String {
internal fun String.efficientReplace(find: String, replaceWith: String): String {
val idx = this.indexOf(find)
return if (idx == -1) this else this.replace(find, replaceWith)
}
Expand Down
Loading

0 comments on commit 8e73bad

Please sign in to comment.