|
| 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 | +} |
0 commit comments