Skip to content
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

Barebones of int selfie #23

Merged
merged 18 commits into from
Dec 18, 2023
Merged
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
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