Skip to content

Commit 21d22e7

Browse files
authored
Add Slice (#22)
2 parents 036f609 + c8f95ab commit 21d22e7

File tree

4 files changed

+398
-0
lines changed

4 files changed

+398
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/*
2+
* Copyright (C) 2019-2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.selfie
17+
18+
import kotlin.jvm.JvmStatic
19+
import kotlin.math.min
20+
21+
/**
22+
* A [CharSequence] which can efficiently subdivide and append itself.
23+
*
24+
* Equal only to other [Slice] with the same [Slice.toString]. Use [Slice.sameAs] to compare with
25+
* other kinds of [CharSequence].
26+
*/
27+
internal expect fun groupImpl(slice: Slice, matchResult: MatchResult, group: Int): Slice
28+
29+
class Slice private constructor(val base: CharSequence, val startIndex: Int, val endIndex: Int) :
30+
CharSequence {
31+
init {
32+
require(base is StringBuilder || base is String)
33+
require(0 <= startIndex)
34+
require(startIndex <= endIndex)
35+
require(endIndex <= base.length)
36+
}
37+
override val length: Int
38+
get() = endIndex - startIndex
39+
override fun get(index: Int): Char = base[startIndex + index]
40+
override fun subSequence(start: Int, end: Int): Slice {
41+
return Slice(base, startIndex + start, startIndex + end)
42+
}
43+
44+
/** Returns a Slice representing the given group within the given match */
45+
fun group(matchResult: MatchResult, group: Int): Slice = groupImpl(this, matchResult, group)
46+
47+
/** Same behavior as [String.trim]. */
48+
fun trim(): Slice {
49+
var end = length
50+
var start = 0
51+
while (start < end && get(start).isWhitespace()) {
52+
++start
53+
}
54+
while (start < end && get(end - 1).isWhitespace()) {
55+
--end
56+
}
57+
return if (start > 0 || end < length) subSequence(start, end) else this
58+
}
59+
override fun toString() = base.subSequence(startIndex, endIndex).toString()
60+
fun concat(other: Slice): Slice =
61+
if (this.isEmpty()) {
62+
other
63+
} else if (other.isEmpty()) {
64+
this
65+
} else if (base === other.base && endIndex == other.startIndex) {
66+
Slice(base, startIndex, other.endIndex)
67+
} else {
68+
val builder: StringBuilder
69+
val start: Int
70+
val end: Int
71+
if (base is StringBuilder && endIndex == base.length) {
72+
builder = base
73+
start = startIndex
74+
end = endIndex + other.length
75+
} else {
76+
builder = StringBuilder(length + other.length)
77+
builder.append(this)
78+
start = 0
79+
end = length + other.length
80+
}
81+
other.appendThisTo(builder)
82+
Slice(builder, start, end)
83+
}
84+
85+
/** append(this) but taking advantage of fastpath where possible */
86+
private fun appendThisTo(builder: StringBuilder) {
87+
if (startIndex == 0 && endIndex == base.length) {
88+
// there is a fastpath for adding a full string and for adding a full StringBuilder
89+
if (base is String) {
90+
builder.append(base)
91+
} else {
92+
builder.append(base as StringBuilder)
93+
}
94+
} else {
95+
builder.append(this)
96+
}
97+
}
98+
fun concat(other: String): Slice {
99+
if (base is String && endIndex + other.length <= base.length) {
100+
for (i in other.indices) {
101+
if (base[i + endIndex] != other[i]) {
102+
return concat(of(other))
103+
}
104+
}
105+
return Slice(base, startIndex, endIndex + other.length)
106+
}
107+
return concat(of(other))
108+
}
109+
fun concatAnchored(other: Slice): Slice {
110+
val result = concat(other)
111+
if (result.base !== base) {
112+
throw concatRootFailure(other)
113+
}
114+
return result
115+
}
116+
fun concatAnchored(other: String): Slice {
117+
val result = concat(other)
118+
if (result.base !== base) {
119+
throw concatRootFailure(other)
120+
}
121+
return result
122+
}
123+
private fun concatRootFailure(other: CharSequence): IllegalArgumentException {
124+
val maxChange = min(other.length, base.length - endIndex)
125+
if (maxChange == 0) {
126+
return IllegalArgumentException(
127+
"Could not perform anchored concat because we are already at the end of the root ${visualize(base)}")
128+
}
129+
var firstChange = 0
130+
while (firstChange < maxChange) {
131+
if (base[endIndex + firstChange] != other[firstChange]) {
132+
break
133+
}
134+
++firstChange
135+
}
136+
return IllegalArgumentException(
137+
"""
138+
This ends with '${visualize(base.subSequence(endIndex, endIndex + firstChange + 1))}'
139+
cannot concat '${visualize(other.subSequence(firstChange, firstChange + 1))}
140+
"""
141+
.trimIndent())
142+
}
143+
fun sameAs(other: CharSequence): Boolean {
144+
if (length != other.length) {
145+
return false
146+
}
147+
for (i in 0 until length) {
148+
if (get(i) != other[i]) {
149+
return false
150+
}
151+
}
152+
return true
153+
}
154+
fun startsWith(prefix: CharSequence): Boolean {
155+
if (length < prefix.length) {
156+
return false
157+
}
158+
for (i in 0 until prefix.length) {
159+
if (get(i) != prefix[i]) {
160+
return false
161+
}
162+
}
163+
return true
164+
}
165+
fun endsWith(suffix: CharSequence): Boolean {
166+
if (length < suffix.length) {
167+
return false
168+
}
169+
val offset = length - suffix.length
170+
for (i in 0 until suffix.length) {
171+
if (get(i + offset) != suffix[i]) {
172+
return false
173+
}
174+
}
175+
return true
176+
}
177+
fun indexOf(lookingFor: String): Int {
178+
val result =
179+
if (base is String) base.indexOf(lookingFor, startIndex)
180+
else {
181+
(base as StringBuilder).indexOf(lookingFor, startIndex)
182+
}
183+
return if (result == -1 || result >= endIndex) -1 else result - startIndex
184+
}
185+
fun indexOf(lookingFor: Char): Int {
186+
val result =
187+
if (base is String) base.indexOf(lookingFor, startIndex)
188+
else {
189+
(base as StringBuilder).indexOf(lookingFor.toString(), startIndex)
190+
}
191+
return if (result == -1 || result >= endIndex) -1 else result - startIndex
192+
}
193+
194+
/**
195+
* Returns a Slice which represents everything from the start of this string until `lookingFor` is
196+
* found. If the string is never found, returns this.
197+
*/
198+
fun until(lookingFor: String): Slice {
199+
val idx = indexOf(lookingFor)
200+
return if (idx == -1) this else subSequence(0, idx)
201+
}
202+
203+
/**
204+
* Asserts that the other string was generated from a call to [.until], and then returns a new
205+
* Slice representing everything after that.
206+
*/
207+
fun after(other: Slice): Slice {
208+
if (other.isEmpty()) {
209+
return this
210+
}
211+
require(other.base === base && other.startIndex == startIndex && other.endIndex <= endIndex) {
212+
"'${visualize(other)}' was not generated by `until` on '${visualize(this)}'"
213+
}
214+
return Slice(base, other.endIndex, endIndex)
215+
}
216+
217+
/**
218+
* Returns the line number of the start of this string. Throws an exception if this isn't based on
219+
* a string any longer, because non-contiguous StringPools have been concatenated.
220+
*/
221+
fun baseLineNumberStart(): Int {
222+
return baseLineNumberOfOffset(startIndex)
223+
}
224+
225+
/**
226+
* Returns the line number of the end of this string. Throws an exception if this isn't based on a
227+
* string any longer, because non-contiguous Slices have been concatenated.
228+
*/
229+
fun baseLineNumberEnd(): Int {
230+
return baseLineNumberOfOffset(endIndex)
231+
}
232+
private fun baseLineNumberOfOffset(idx: Int): Int {
233+
assertStringBased()
234+
var lineNumber = 1
235+
for (i in 0 until base.length) {
236+
if (base[i] == '\n') {
237+
++lineNumber
238+
}
239+
}
240+
return lineNumber
241+
}
242+
private fun assertStringBased() {
243+
check(base is String) {
244+
"When you call concat on non-contiguous parts, you lose the connection to the original String."
245+
}
246+
}
247+
override fun equals(anObject: Any?): Boolean {
248+
if (this === anObject) {
249+
return true
250+
} else if (anObject is Slice) {
251+
return sameAs(anObject)
252+
}
253+
return false
254+
}
255+
override fun hashCode(): Int {
256+
var h = 0
257+
for (i in indices) {
258+
h = 31 * h + get(i).code
259+
}
260+
return h
261+
}
262+
fun endsAtSamePlace(other: Slice): Boolean {
263+
check(base === other.base)
264+
return endIndex == other.endIndex
265+
}
266+
267+
companion object {
268+
@JvmStatic
269+
fun of(base: String, startIndex: Int = 0, endIndex: Int = base.length): Slice {
270+
return Slice(base, startIndex, endIndex)
271+
}
272+
fun concatAll(vararg poolStringsOrStrings: CharSequence): Slice {
273+
if (poolStringsOrStrings.isEmpty()) {
274+
return empty()
275+
}
276+
var total = asPool(poolStringsOrStrings[0])
277+
for (i in 1 until poolStringsOrStrings.size) {
278+
val next = poolStringsOrStrings[i]
279+
total = if (next is String) total.concat(next) else total.concat(next as Slice)
280+
}
281+
return total
282+
}
283+
private fun visualize(input: CharSequence): String {
284+
return input
285+
.toString()
286+
.replace("\n", "")
287+
.replace("\r", "")
288+
.replace(" ", "·")
289+
.replace("\t", "»")
290+
}
291+
private fun asPool(sequence: CharSequence): Slice {
292+
return if (sequence is Slice) sequence else of(sequence as String)
293+
}
294+
295+
/** Returns the empty Slice. */
296+
@JvmStatic
297+
fun empty(): Slice {
298+
return Slice("", 0, 0)
299+
}
300+
}
301+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2020-2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.selfie
17+
18+
import io.kotest.matchers.shouldBe
19+
import kotlin.test.Test
20+
21+
class SliceTest {
22+
@Test
23+
fun afterTest() {
24+
val abcdef = Slice.of("abcdef")
25+
val untilA = abcdef.until("a")
26+
untilA.toString() shouldBe ""
27+
abcdef.after(untilA).toString() shouldBe "abcdef"
28+
val untilC = abcdef.until("c")
29+
untilC.toString() shouldBe "ab"
30+
abcdef.after(untilC).toString() shouldBe "cdef"
31+
val untilF = abcdef.until("f")
32+
untilF.toString() shouldBe "abcde"
33+
abcdef.after(untilF).toString() shouldBe "f"
34+
val untilZ = abcdef.until("z")
35+
untilZ.toString() shouldBe "abcdef"
36+
abcdef.after(untilZ).toString() shouldBe ""
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (C) 2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.selfie
17+
18+
/**
19+
* A CharSequence which can efficiently subdivide and append itself.
20+
*
21+
* Equal only to other PoolString with the same `toString()`. Use [.sameAs] to compare with other
22+
* kinds of [CharSequence].
23+
*
24+
* Would be cool to have PoolString.Root which differentiates the String-based ones from
25+
* StringBuilder-based ones.
26+
*/
27+
actual fun groupImpl(slice: Slice, matchResult: MatchResult, group: Int): Slice {
28+
TODO("Not yet implemented")
29+
}

0 commit comments

Comments
 (0)