From b5a4ec5bc9c3aebbaddcec3196d24a030c3cf8ae Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 5 Sep 2025 19:46:58 +0100 Subject: [PATCH] ByteStrings impl add optimised lastIndexOf Update ByteString.scala Update ByteString.scala Update ByteString.scala --- .../apache/pekko/util/ByteStringSpec.scala | 51 ++++++- .../org/apache/pekko/util/ByteString.scala | 139 +++++++++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) diff --git a/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala b/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala index 35d96f80be..46c44c2acf 100644 --- a/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala +++ b/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringSpec.scala @@ -852,7 +852,6 @@ class ByteStringSpec extends AnyWordSpec with Matchers with Checkers { compact.indexOf('e'.toByte) should ===(3) compact.indexOf('f'.toByte) should ===(4) compact.indexOf('g'.toByte) should ===(5) - } "indexOf (specialized) from offset" in { ByteString.empty.indexOf(5.toByte, -1) should ===(-1) @@ -1001,6 +1000,56 @@ class ByteStringSpec extends AnyWordSpec with Matchers with Checkers { byteStringLong.indexOf('z', 2, 24) should ===(-1) byteStringLong.indexOf('a', 2, 24) should ===(-1) } + "lastIndexOf (specialized)" in { + ByteString.empty.lastIndexOf(5.toByte, -1) should ===(-1) + ByteString.empty.lastIndexOf(5.toByte, 0) should ===(-1) + ByteString.empty.lastIndexOf(5.toByte, 1) should ===(-1) + ByteString.empty.lastIndexOf(5.toByte) should ===(-1) + val byteString1 = ByteString1.fromString("abb") + byteString1.lastIndexOf('d'.toByte) should ===(-1) + byteString1.lastIndexOf('d'.toByte, -1) should ===(-1) + byteString1.lastIndexOf('d'.toByte, 4) should ===(-1) + byteString1.lastIndexOf('d'.toByte, 1) should ===(-1) + byteString1.lastIndexOf('d'.toByte, 0) should ===(-1) + byteString1.lastIndexOf('a'.toByte, -1) should ===(-1) + byteString1.lastIndexOf('a'.toByte) should ===(0) + byteString1.lastIndexOf('a'.toByte, 0) should ===(0) + byteString1.lastIndexOf('a'.toByte, 1) should ===(0) + byteString1.lastIndexOf('b'.toByte) should ===(2) + byteString1.lastIndexOf('b'.toByte, 2) should ===(2) + byteString1.lastIndexOf('b'.toByte, 1) should ===(1) + byteString1.lastIndexOf('b'.toByte, 0) should ===(-1) + + val byteStrings = ByteStrings(ByteString1.fromString("abb"), ByteString1.fromString("efg")) + byteStrings.lastIndexOf('e'.toByte) should ===(3) + byteStrings.lastIndexOf('e'.toByte, 6) should ===(3) + byteStrings.lastIndexOf('e'.toByte, 4) should ===(3) + byteStrings.lastIndexOf('e'.toByte, 1) should ===(-1) + byteStrings.lastIndexOf('e'.toByte, 0) should ===(-1) + byteStrings.lastIndexOf('e'.toByte, -1) should ===(-1) + + byteStrings.lastIndexOf('b'.toByte) should ===(2) + byteStrings.lastIndexOf('b'.toByte, 6) should ===(2) + byteStrings.lastIndexOf('b'.toByte, 4) should ===(2) + byteStrings.lastIndexOf('b'.toByte, 1) should ===(1) + byteStrings.lastIndexOf('b'.toByte, 0) should ===(-1) + byteStrings.lastIndexOf('b'.toByte, -1) should ===(-1) + + val compact = byteStrings.compact + compact.lastIndexOf('e'.toByte) should ===(3) + compact.lastIndexOf('e'.toByte, 6) should ===(3) + compact.lastIndexOf('e'.toByte, 4) should ===(3) + compact.lastIndexOf('e'.toByte, 1) should ===(-1) + compact.lastIndexOf('e'.toByte, 0) should ===(-1) + compact.lastIndexOf('e'.toByte, -1) should ===(-1) + + compact.lastIndexOf('b'.toByte) should ===(2) + compact.lastIndexOf('b'.toByte, 6) should ===(2) + compact.lastIndexOf('b'.toByte, 4) should ===(2) + compact.lastIndexOf('b'.toByte, 1) should ===(1) + compact.lastIndexOf('b'.toByte, 0) should ===(-1) + compact.lastIndexOf('b'.toByte, -1) should ===(-1) + } "copyToArray" in { val byteString = ByteString(1, 2) ++ ByteString(3) ++ ByteString(4) diff --git a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala index 849067dc2a..1bab5f3174 100644 --- a/actor/src/main/scala/org/apache/pekko/util/ByteString.scala +++ b/actor/src/main/scala/org/apache/pekko/util/ByteString.scala @@ -314,6 +314,32 @@ object ByteString { else -1 } + override def lastIndexOf[B >: Byte](elem: B, end: Int): Int = { + if (end < 0) -1 + else { + var found = -1 + var i = math.min(end, length - 1) + while (i >= 0 && found == -1) { + if (bytes(i) == elem) found = i + i -= 1 + } + found + } + } + + override def lastIndexOf(elem: Byte, end: Int): Int = { + if (end < 0) -1 + else { + var found = -1 + var i = math.min(end, length - 1) + while (i >= 0 && found == -1) { + if (bytes(i) == elem) found = i + i -= 1 + } + found + } + } + override def slice(from: Int, until: Int): ByteString = if (from <= 0 && until >= length) this else if (from >= length || until <= 0 || from >= until) ByteString.empty @@ -554,7 +580,6 @@ object ByteString { i += 1 } -1 - } // the calling code already adds the startIndex so this method does not need to @@ -575,6 +600,32 @@ object ByteString { else -1 } + override def lastIndexOf[B >: Byte](elem: B, end: Int): Int = { + if (end < 0) -1 + else { + var found = -1 + var i = math.min(end, length - 1) + while (i >= 0 && found == -1) { + if (bytes(startIndex + i) == elem) found = i + i -= 1 + } + found + } + } + + override def lastIndexOf(elem: Byte, end: Int): Int = { + if (end < 0) -1 + else { + var found = -1 + var i = math.min(end, length - 1) + while (i >= 0 && found == -1) { + if (bytes(startIndex + i) == elem) found = i + i -= 1 + } + found + } + } + override def copyToArray[B >: Byte](dest: Array[B], start: Int, len: Int): Int = { // min of the bytes available to copy, bytes there is room for in dest and the requested number of bytes val toCopy = math.min(math.min(len, length), dest.length - start) @@ -895,6 +946,67 @@ object ByteString { } } + override def lastIndexOf[B >: Byte](elem: B, end: Int): Int = { + if (end < 0) -1 + else { + val byteStringsLast = bytestrings.size - 1 + + @tailrec + def find(bsIdx: Int, relativeIndex: Int, len: Int): Int = { + if (bsIdx < 0) -1 + else { + val bs = bytestrings(bsIdx) + val bsStartIndex = len - bs.length + + if (relativeIndex < bsStartIndex || bs.isEmpty) { + if (bsIdx == 0) -1 + else find(bsIdx - 1, relativeIndex, bsStartIndex) + } else { + val subIndexOf = bs.lastIndexOf(elem, relativeIndex) + if (subIndexOf < 0) { + if (bsIdx == 0) -1 + else find(bsIdx - 1, relativeIndex, bsStartIndex) + } else subIndexOf + bsStartIndex + } + } + } + + find(byteStringsLast, math.min(end, length), length) + } + } + + override def lastIndexOf(elem: Byte, end: Int): Int = { + if (end < 0) -1 + else { + if (end < 0) -1 + else { + val byteStringsLast = bytestrings.size - 1 + + @tailrec + def find(bsIdx: Int, relativeIndex: Int, len: Int): Int = { + if (bsIdx < 0) -1 + else { + val bs = bytestrings(bsIdx) + val bsStartIndex = len - bs.length + + if (relativeIndex < bsStartIndex || bs.isEmpty) { + if (bsIdx == 0) -1 + else find(bsIdx - 1, relativeIndex, bsStartIndex) + } else { + val subIndexOf = bs.lastIndexOf(elem, relativeIndex) + if (subIndexOf < 0) { + if (bsIdx == 0) -1 + else find(bsIdx - 1, relativeIndex, bsStartIndex) + } else subIndexOf + bsStartIndex + } + } + } + + find(byteStringsLast, math.min(end, length), length) + } + } + } + override def copyToArray[B >: Byte](dest: Array[B], start: Int, len: Int): Int = { if (bytestrings.size == 1) bytestrings.head.copyToArray(dest, start, len) else { @@ -1056,6 +1168,31 @@ sealed abstract class ByteString */ def indexOf(elem: Byte): Int = indexOf(elem, 0) + /** + * Finds index of last occurrence of some byte in this ByteString before or at some end index. + * + * Similar to lastIndexOf, but it avoids boxing if the value is already a byte. + * + * @param elem the element value to search for. + * @param end the end index + * @return the index `<= end` of the last element of this ByteString that is equal (as determined by `==`) + * to `elem`, or `-1`, if none exists. + * @since 2.0.0 + */ + def lastIndexOf(elem: Byte, end: Int): Int = lastIndexOf[Byte](elem, end) + + /** + * Finds index of last occurrence of some byte in this ByteString. + * + * Similar to lastIndexOf, but it avoids boxing if the value is already a byte. + * + * @param elem the element value to search for. + * @return the index of the last element of this ByteString that is equal (as determined by `==`) + * to `elem`, or `-1`, if none exists. + * @since 2.0.0 + */ + def lastIndexOf(elem: Byte): Int = lastIndexOf(elem, length - 1) + override def contains[B >: Byte](elem: B): Boolean = indexOf(elem, 0) != -1 /**