From 7908b616af05411972dfa457b30182977fad035c Mon Sep 17 00:00:00 2001 From: Veeti Paananen Date: Sun, 24 May 2026 01:01:11 +0300 Subject: [PATCH] Fix NumberFormatException with very large numbers in ArrayMap Test case names containing long numbers could result in a NumberFormatException being thrown. Support arbitrarily large numbers by comparing strings. --- .../kotlin/com/diffplug/selfie/ArrayMap.kt | 23 +++++++++++++++---- .../com/diffplug/selfie/ArrayMapTest.kt | 21 ++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/ArrayMap.kt b/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/ArrayMap.kt index cc1afcb0..54797e6a 100644 --- a/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/ArrayMap.kt +++ b/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/ArrayMap.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2025 DiffPlug + * Copyright (C) 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ private val STRING_SLASHFIRST = val numB = extractNumber(b, i) // Compare the numeric values - val numCompare = numA.first.compareTo(numB.first) + val numCompare = compareDigitStrings(numA.first, numB.first) if (numCompare != 0) return@Comparator numCompare // If the numbers are equal, adjust index to after the numbers @@ -55,16 +55,24 @@ private val STRING_SLASHFIRST = /** * Extracts a numeric substring starting at the given index. * - * @return Pair of (numeric value, index after the last digit) + * @return Pair of (numeric substring, index after the last digit) */ -private fun extractNumber(s: String, startIndex: Int): Pair { +private fun extractNumber(s: String, startIndex: Int): Pair { var endIndex = startIndex while (endIndex < s.length && s[endIndex].isDigit()) { endIndex++ } - val number = s.substring(startIndex, endIndex).toInt() + val number = s.substring(startIndex, endIndex) return Pair(number, endIndex) } + +/** Compares two digit-only strings numerically without parsing to a numeric type. */ +private fun compareDigitStrings(a: String, b: String): Int { + val aStripped = a.trimStart('0').ifEmpty { "0" } + val bStripped = b.trimStart('0').ifEmpty { "0" } + if (aStripped.length != bStripped.length) return aStripped.length.compareTo(bStripped.length) + return aStripped.compareTo(bStripped) +} private val PAIR_STRING_SLASHFIRST = Comparator> { a, b -> STRING_SLASHFIRST.compare(a.first, b.first) } @@ -138,6 +146,7 @@ class ArrayMap, V : Any>(private val data: Array) : Map, V : Any>(private val data: Array) : Map, V : Any>(private val data: Array) : Map() { @@ -200,6 +211,7 @@ class ArrayMap, V : Any>(private val data: Array) : Map= 0 + /** list-backed collection of values at odd indices. */ override val values: List get() = @@ -209,6 +221,7 @@ class ArrayMap, V : Any>(private val data: Array) : Map> get() = diff --git a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/ArrayMapTest.kt b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/ArrayMapTest.kt index bb259cf9..553a8e65 100644 --- a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/ArrayMapTest.kt +++ b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/ArrayMapTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2025 DiffPlug + * Copyright (C) 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -172,6 +172,25 @@ class ArrayMapTest { val map = ArrayMap.of(0.rangeTo(8).map { it to it.toString() }.toMutableList()) map.minusSortedIndices(listOf(0, 2, 3, 6, 7, 8)).toString() shouldBe "{1=1, 4=4, 5=5}" } + + @Test + fun wasBrokenWithArbitrarilyLargeNumbersInStrings() { + val map = + ArrayMap.empty() + // Numbers larger than Long.MAX_VALUE (9223372036854775807) + .plus("1234567890123456789012345678901234567890", "first") + .plus("9999999999999999999999999999999999999999", "second") + .plus("5000000000000000000000000000000000000000", "third") + // Varying-length digit strings sorted by number of digits (then lexicographic) + .plus("item100", "hundred") + .plus("item8", "eight") + .plus("item42", "forty-two") + // Leading zeros: strip so 007 == 7, then fall through to string-length tiebreak + .plus("key007", "seven") + .plus("key7", "also-seven") + map.toString() shouldBe + "{1234567890123456789012345678901234567890=first, 5000000000000000000000000000000000000000=third, 9999999999999999999999999999999999999999=second, item8=eight, item42=forty-two, item100=hundred, key7=also-seven, key007=seven}" + } } class ArraySetTest() {