From bf83f0e070037cb608f8be51fd15fd97e1472d50 Mon Sep 17 00:00:00 2001 From: Abduqodiri Qurbonzoda Date: Sat, 23 Feb 2019 00:51:36 +0300 Subject: [PATCH] Implement contains extension functions for URanges (KT-26378) --- .../stdlib/common/src/generated/_URanges.kt | 60 ++++- libraries/stdlib/test/ranges/RangeTest.kt | 4 - libraries/stdlib/test/ranges/URangeTest.kt | 244 ++++++++++++++++++ .../kotlin-stdlib-runtime-merged.txt | 6 + .../kotlin-stdlib-gen/src/templates/Ranges.kt | 69 +++-- .../src/templates/dsl/CommonTypes.kt | 1 + 6 files changed, 355 insertions(+), 29 deletions(-) create mode 100644 libraries/stdlib/test/ranges/URangeTest.kt diff --git a/libraries/stdlib/common/src/generated/_URanges.kt b/libraries/stdlib/common/src/generated/_URanges.kt index 827a47f7a57..dd975939bbf 100644 --- a/libraries/stdlib/common/src/generated/_URanges.kt +++ b/libraries/stdlib/common/src/generated/_URanges.kt @@ -93,6 +93,60 @@ public inline operator fun ULongRange.contains(element: ULong?): Boolean { return element != null && contains(element) } +/** + * Checks if the specified [value] belongs to this range. + */ +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +public operator fun UIntRange.contains(value: UByte): Boolean { + return contains(value.toUInt()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +public operator fun ULongRange.contains(value: UByte): Boolean { + return contains(value.toULong()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +public operator fun ULongRange.contains(value: UInt): Boolean { + return contains(value.toULong()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +public operator fun UIntRange.contains(value: ULong): Boolean { + return (value shr UInt.SIZE_BITS) == 0uL && contains(value.toUInt()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +public operator fun UIntRange.contains(value: UShort): Boolean { + return contains(value.toUInt()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +public operator fun ULongRange.contains(value: UShort): Boolean { + return contains(value.toULong()) +} + /** * Returns a progression from this value down to the specified [to] value with the step -1. * @@ -187,7 +241,8 @@ public infix fun ULongProgression.step(step: Long): ULongProgression { @SinceKotlin("1.3") @ExperimentalUnsignedTypes public infix fun UByte.until(to: UByte): UIntRange { - return this.toUInt() .. (to.toUInt() - 1u).toUInt() + if (to <= UByte.MIN_VALUE) return UIntRange.EMPTY + return this.toUInt() .. (to - 1u).toUInt() } /** @@ -222,6 +277,7 @@ public infix fun ULong.until(to: ULong): ULongRange { @SinceKotlin("1.3") @ExperimentalUnsignedTypes public infix fun UShort.until(to: UShort): UIntRange { - return this.toUInt() .. (to.toUInt() - 1u).toUInt() + if (to <= UShort.MIN_VALUE) return UIntRange.EMPTY + return this.toUInt() .. (to - 1u).toUInt() } diff --git a/libraries/stdlib/test/ranges/RangeTest.kt b/libraries/stdlib/test/ranges/RangeTest.kt index cb56ea5c158..4c79b9b1031 100644 --- a/libraries/stdlib/test/ranges/RangeTest.kt +++ b/libraries/stdlib/test/ranges/RangeTest.kt @@ -416,15 +416,11 @@ public class RangeTest { assertFailsWithIllegalArgument { CharProgression.fromClosedRange('a', 'b', Int.MIN_VALUE) } assertFailsWithIllegalArgument { IntProgression.fromClosedRange(0, 1, Int.MIN_VALUE) } assertFailsWithIllegalArgument { LongProgression.fromClosedRange(0, 1, Long.MIN_VALUE) } - assertFailsWithIllegalArgument { UIntProgression.fromClosedRange(0u, 1u, Int.MIN_VALUE) } - assertFailsWithIllegalArgument { ULongProgression.fromClosedRange(0u, 1u, Long.MIN_VALUE) } } @Test fun randomInEmptyRange() { assertFailsWith { IntRange.EMPTY.random() } assertFailsWith { LongRange.EMPTY.random() } assertFailsWith { CharRange.EMPTY.random() } - assertFailsWith { UIntRange.EMPTY.random() } - assertFailsWith { ULongRange.EMPTY.random() } } } diff --git a/libraries/stdlib/test/ranges/URangeTest.kt b/libraries/stdlib/test/ranges/URangeTest.kt new file mode 100644 index 00000000000..e0eba7f945b --- /dev/null +++ b/libraries/stdlib/test/ranges/URangeTest.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package ranges + +import kotlin.test.* + +public class URangeTest { + @Test + fun uintRange() { + val range = 9u..(-5).toUInt() + assertFalse((-4).toUInt() in range) + assertFalse((-1).toUInt() in range) + assertFalse(0u in range) + assertFalse(3u in range) + assertFalse(8u in range) + + assertTrue(9u in range) + assertTrue(10u in range) + assertTrue(9000u in range) + assertTrue((-1000).toUInt() in range) + assertTrue((-6).toUInt() in range) + assertTrue((-5).toUInt() in range) + + assertFalse(range.isEmpty()) + + assertTrue(9u in (range as ClosedRange)) + assertFalse((range as ClosedRange).isEmpty()) + + assertTrue(12.toUShort() in range) + assertTrue(12.toUByte() in range) + assertTrue(12.toULong() in range) + + assertFalse((-1).toULong() in range) + assertFalse((-1000).toULong() in range) + + assertFalse(null in range) + assertTrue(12u as UInt? in range) + assertFalse((-3).toUInt() as UInt? in range) + + val openRange = 1u until 10u + assertTrue(9u in openRange) + assertFalse(10u in openRange) + + assertTrue((1u until UInt.MIN_VALUE).isEmpty()) + } + + @Test + fun ubyteRange() { + val range = 9.toUByte()..(-5).toUByte() + assertFalse((-4).toUByte() in range) + assertFalse((-1).toUByte() in range) + assertFalse(0.toUByte() in range) + assertFalse(3.toUByte() in range) + assertFalse(8.toUByte() in range) + + assertTrue(9.toUByte() in range) + assertTrue(10.toUByte() in range) + assertTrue(111.toUByte() in range) + assertTrue((-100).toUByte() in range) + assertTrue((-6).toUByte() in range) + assertTrue((-5).toUByte() in range) + + assertFalse(range.isEmpty()) + + assertTrue(12.toUShort() in range) + assertTrue(12.toUInt() in range) + assertTrue(12.toULong() in range) + + assertFalse((-1).toUShort() in range) + assertFalse((-1000).toUInt() in range) + +// assertTrue(1.toUByte() as UByte? in range) // expected not to compile + + val openRange = 1.toUByte() until 10.toUByte() + assertTrue(9.toUByte() in openRange) + assertFalse(10.toUByte() in openRange) + + assertTrue((UByte.MAX_VALUE until UByte.MIN_VALUE).isEmpty()) + } + + @Test + fun ushortRange() { + val range = 9.toUShort()..(-5).toUShort() + assertFalse((-1).toUShort() in range) + assertFalse((-4).toUShort() in range) + assertFalse(0.toUShort() in range) + assertFalse(3.toUShort() in range) + assertFalse(8.toUShort() in range) + + assertTrue(9.toUShort() in range) + assertTrue(10.toUShort() in range) + assertTrue(239.toUShort() in range) + assertTrue((-1000).toUShort() in range) + assertTrue((-6).toUShort() in range) + assertTrue((-5).toUShort() in range) + + assertFalse(range.isEmpty()) + + assertTrue(12.toUByte() in range) + assertTrue(12.toUInt() in range) + assertTrue(12.toULong() in range) + + assertFalse((-1).toUInt() in range) + assertFalse((-1000).toULong() in range) + +// assertTrue(1.toUShort() as UShort? in range) // expected not to compile + + val openRange = 1.toUShort() until 10.toUShort() + assertTrue(9.toUShort() in openRange) + assertFalse(10.toUShort() in openRange) + + assertTrue((0.toUShort() until UShort.MIN_VALUE).isEmpty()) + } + + @Test + fun ulongRange() { + val range = 9uL..(-5).toULong() + assertFalse((-1).toULong() in range) + assertFalse((-4).toULong() in range) + assertFalse(0uL in range) + assertFalse(3uL in range) + assertFalse(8uL in range) + + assertTrue(9uL in range) + assertTrue(10uL in range) + assertTrue(10000000uL in range) + assertTrue((-10000000).toULong() in range) + assertTrue((-6).toULong() in range) + assertTrue((-5).toULong() in range) + + assertFalse(range.isEmpty()) + + assertTrue((-5).toULong() in (range as ClosedRange)) + assertFalse((range as ClosedRange).isEmpty()) + + assertTrue(12.toUByte() in range) + assertTrue(12.toUShort() in range) + assertTrue(12.toUInt() in range) + + assertFalse(null in range) + assertTrue(12uL as ULong? in range) + assertFalse((-3).toULong() as ULong? in range) + + val openRange = 1uL until 10uL + assertTrue(9uL in openRange) + assertFalse(10uL in openRange) + + assertTrue((0uL until ULong.MIN_VALUE).isEmpty()) + } + + @Suppress("EmptyRange") + @Test + fun isEmpty() { + assertTrue((2u..1u).isEmpty()) + assertTrue((2uL..0uL).isEmpty()) + assertTrue(((-1).toUShort()..1.toUShort()).isEmpty()) + assertTrue(((-5).toUByte()..0.toUByte()).isEmpty()) + + assertTrue((1u downTo 2u).isEmpty()) + assertTrue((0uL downTo 2uL).isEmpty()) + assertFalse((2u downTo 1u).isEmpty()) + assertFalse((2uL downTo 0uL).isEmpty()) + } + + @Suppress("ReplaceAssertBooleanWithAssertEquality", "EmptyRange") + @Test + fun emptyEquals() { + assertTrue(UIntRange.EMPTY == UIntRange.EMPTY) + assertEquals(UIntRange.EMPTY, UIntRange.EMPTY) + assertEquals(0uL..42uL, 0uL..42uL) + assertEquals(0uL..4200000042000000uL, 0uL..4200000042000000uL) + assertEquals(3u downTo 0u, 3u downTo 0u) + + assertEquals(2u..1u, 1u..0u) + assertEquals(2uL..1uL, 1uL..0uL) + assertEquals(2u.toUShort()..1u.toUShort(), 1u.toUShort()..0u.toUShort()) + assertEquals(2u.toUByte()..1u.toUByte(), 1u.toUByte()..0u.toUByte()) + + assertTrue(1u downTo 2u == 2u downTo 3u) + assertTrue(0uL downTo (-1).toULong() == (-2).toULong() downTo (-1).toULong()) + + assertFalse(0u..1u == UIntRange.EMPTY) + } + + @Suppress("EmptyRange") + @Test + fun emptyHashCode() { + assertEquals((0u..42u).hashCode(), (0u..42u).hashCode()) + + assertEquals(((-1).toUInt()..0u).hashCode(), UIntRange.EMPTY.hashCode()) + assertEquals((2uL..1uL).hashCode(), (1uL..0uL).hashCode()) + assertEquals(((-1).toUShort()..(-2).toUShort()).hashCode(), (42.toUShort()..0.toUShort()).hashCode()) + assertEquals(((-1).toUByte()..(-2).toUByte()).hashCode(), (42.toUByte()..0.toUByte()).hashCode()) + + assertEquals((1u downTo 2u).hashCode(), (2u downTo 3U).hashCode()) + assertEquals((1UL downTo 2uL).hashCode(), (2UL downTo 3UL).hashCode()) + } + + private fun assertFailsWithIllegalArgument(f: () -> Unit) = assertFailsWith { f() } + + @Test + fun illegalProgressionCreation() { + // create Progression explicitly with increment = 0 + assertFailsWithIllegalArgument { UIntProgression.fromClosedRange(0u, 5u, 0) } + assertFailsWithIllegalArgument { ULongProgression.fromClosedRange(0uL, 5uL, 0) } + + + assertFailsWithIllegalArgument { 0u..5u step 0 } + assertFailsWithIllegalArgument { 0.toUByte()..5.toUByte() step 0 } + assertFailsWithIllegalArgument { 0.toUShort()..5.toUShort() step 0 } + assertFailsWithIllegalArgument { 0uL..5uL step 0 } + + assertFailsWithIllegalArgument { (-5).toUInt() downTo 0u step 0 } + assertFailsWithIllegalArgument { (-5).toUByte() downTo 0.toUByte() step 0 } + assertFailsWithIllegalArgument { (-5).toUShort() downTo 0.toUShort() step 0 } + assertFailsWithIllegalArgument { (-5).toULong() downTo 0uL step 0L } + + assertFailsWithIllegalArgument { 0u..5u step -2 } + assertFailsWithIllegalArgument { 0.toUByte()..5.toUByte() step -2 } + assertFailsWithIllegalArgument { 0.toUShort()..5.toUShort() step -2 } + assertFailsWithIllegalArgument { 0uL..5uL step -2L } + + + assertFailsWithIllegalArgument { (-5).toUInt() downTo 0u step -2 } + assertFailsWithIllegalArgument { (-5).toUByte() downTo 0.toUByte() step -2 } + assertFailsWithIllegalArgument { (-5).toUShort() downTo 0.toUShort() step -2 } + assertFailsWithIllegalArgument { (-5).toULong() downTo 0uL step -2L } + } + + @Test + fun stepSizeIsTooLow() { + assertFailsWithIllegalArgument { UIntProgression.fromClosedRange(0u, 1u, Int.MIN_VALUE) } + assertFailsWithIllegalArgument { ULongProgression.fromClosedRange(0u, 1u, Long.MIN_VALUE) } + } + + @Test + fun randomInEmptyRange() { + assertFailsWith { UIntRange.EMPTY.random() } + assertFailsWith { ULongRange.EMPTY.random() } + } +} diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 92674d2d6e0..023b5ca58ce 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -4256,6 +4256,12 @@ public final class kotlin/ranges/ULongRange$Companion { } public final class kotlin/ranges/URangesKt { + public static final fun contains-68kG9v0 (Lkotlin/ranges/UIntRange;B)Z + public static final fun contains-Gab390E (Lkotlin/ranges/ULongRange;I)Z + public static final fun contains-ULb-yJY (Lkotlin/ranges/ULongRange;B)Z + public static final fun contains-ZsK3CEQ (Lkotlin/ranges/UIntRange;S)Z + public static final fun contains-fz5IDCE (Lkotlin/ranges/UIntRange;J)Z + public static final fun contains-uhHAxoY (Lkotlin/ranges/ULongRange;S)Z public static final fun downTo-5PvTz6A (SS)Lkotlin/ranges/UIntProgression; public static final fun downTo-J1ME1BU (II)Lkotlin/ranges/UIntProgression; public static final fun downTo-Kr8caGY (BB)Lkotlin/ranges/UIntProgression; diff --git a/libraries/tools/kotlin-stdlib-gen/src/templates/Ranges.kt b/libraries/tools/kotlin-stdlib-gen/src/templates/Ranges.kt index 006772b517a..dd4fb09564f 100644 --- a/libraries/tools/kotlin-stdlib-gen/src/templates/Ranges.kt +++ b/libraries/tools/kotlin-stdlib-gen/src/templates/Ranges.kt @@ -20,13 +20,18 @@ object RangeOps : TemplateGroupBase() { } } - private fun Collection.permutations(): List> = flatMap { a -> map { b -> a to b } } + private fun shouldCheckForConversionOverflow(fromType: PrimitiveType, toType: PrimitiveType): Boolean { + return toType.isIntegral() && fromType.capacity > toType.capacity || + toType.isUnsigned() && fromType.capacityUnsigned > toType.capacityUnsigned + } - private val numericPrimitives = PrimitiveType.numericPrimitives - private val numericPermutations = numericPrimitives.permutations() - private val primitivePermutations = numericPermutations + (PrimitiveType.Char to PrimitiveType.Char) - private val integralPermutations = primitivePermutations.filter { it.first.isIntegral() && it.second.isIntegral() } - private val unsignedPermutations = PrimitiveType.unsignedPrimitives.map { it to it } + private fun Collection.combinations(): List> = flatMap { a -> map { b -> a to b } } + + private val numericCombinations = PrimitiveType.numericPrimitives.combinations() + private val primitiveCombinations = numericCombinations + (PrimitiveType.Char to PrimitiveType.Char) + private val integralCombinations = primitiveCombinations.filter { it.first.isIntegral() && it.second.isIntegral() } + private val unsignedCombinations = PrimitiveType.unsignedPrimitives.combinations() + private val unsignedMappings = PrimitiveType.unsignedPrimitives.map { it to it } val PrimitiveType.stepType get() = when(this) { PrimitiveType.Char -> "Int" @@ -72,7 +77,7 @@ object RangeOps : TemplateGroupBase() { } val f_downTo = fn("downTo(to: Primitive)").byTwoPrimitives { - include(Primitives, integralPermutations + unsignedPermutations) + include(Primitives, integralCombinations + unsignedMappings) } builderWith { (fromType, toType) -> val elementType = rangeElementType(fromType, toType) val progressionType = elementType.name + "Progression" @@ -107,7 +112,7 @@ object RangeOps : TemplateGroupBase() { val f_until = fn("until(to: Primitive)").byTwoPrimitives { - include(Primitives, integralPermutations + unsignedPermutations) + include(Primitives, integralCombinations + unsignedMappings) } builderWith { (fromType, toType) -> infix() signature("until(to: $toType)") @@ -115,7 +120,6 @@ object RangeOps : TemplateGroupBase() { val elementType = rangeElementType(fromType, toType) val progressionType = elementType.name + "Range" returns(progressionType) - val minValue = if (elementType == PrimitiveType.Char) "'\\u0000'" else "$elementType.MIN_VALUE" doc { """ @@ -125,16 +129,15 @@ object RangeOps : TemplateGroupBase() { """ } + val minValue = when { + elementType == PrimitiveType.Char -> "'\\u0000'" + elementType.isUnsigned() -> "$toType.MIN_VALUE" + else -> "$elementType.MIN_VALUE" + } val fromExpr = if (elementType == fromType) "this" else "this.to$elementType()" - val u = if (elementType in PrimitiveType.unsignedPrimitives) "u" else "" + val u = if (elementType.isUnsigned()) "u" else "" - if (elementType == toType) { - // hack to work around incorrect char overflow behavior in JVM and int overflow behavior in JS - val toExpr = when (toType) { - PrimitiveType.Char -> "to.toInt()" - PrimitiveType.Int -> "to.toLong()" - else -> "to" - } + if (elementType == toType || elementType.isUnsigned()) { body { // <= instead of == for JS """ @@ -148,7 +151,7 @@ object RangeOps : TemplateGroupBase() { } val f_contains = fn("contains(value: Primitive)").byTwoPrimitives { - include(Ranges, numericPermutations) + include(Ranges, numericCombinations) filter { _, (rangeType, itemType) -> rangeType != itemType } } builderWith { (rangeType, itemType) -> operator() @@ -158,14 +161,15 @@ object RangeOps : TemplateGroupBase() { if (rangeType.isIntegral() != itemType.isIntegral()) { deprecate(Deprecation("This `contains` operation mixing integer and floating point arguments has ambiguous semantics and is going to be removed.", level = DeprecationLevel.WARNING)) } + platformName("${rangeType.name.decapitalize()}RangeContains") returns("Boolean") doc { "Checks if the specified [value] belongs to this range." } body { - if (rangeType.capacity > itemType.capacity || !rangeType.isIntegral()) - "return contains(value.to$rangeType())" - else + if (shouldCheckForConversionOverflow(fromType = itemType, toType = rangeType)) "return value.to${rangeType}ExactOrNull().let { if (it != null) contains(it) else false }" + else + "return contains(value.to$rangeType())" } } @@ -188,9 +192,28 @@ object RangeOps : TemplateGroupBase() { body { "return element != null && contains(element)" } } + val f_contains_unsigned = fn("contains(element: Primitive)").byTwoPrimitives { + include(RangesOfPrimitives, unsignedCombinations) + filter { _, (rangeType, itemType) -> rangeType in rangePrimitives && rangeType != itemType } + } builderWith { (rangeType, itemType) -> + operator() + signature("contains(value: $itemType)") + returns("Boolean") + + since("1.3") + doc { "Checks if the specified [value] belongs to this range." } + + body { + if (shouldCheckForConversionOverflow(fromType = itemType, toType = rangeType)) + "return (value shr $rangeType.SIZE_BITS) == ${itemType.zero()} && contains(value.to$rangeType())" + else + "return contains(value.to$rangeType())" + } + } + val f_toPrimitiveExactOrNull = fn("to{}ExactOrNull()").byTwoPrimitives { - include(Primitives, numericPermutations) - filter { _, (fromType, toType) -> fromType.capacity > toType.capacity && toType.isIntegral() } + include(Primitives, numericCombinations) + filter { _, (fromType, toType) -> shouldCheckForConversionOverflow(fromType, toType) } } builderWith { (fromType, toType) -> check(toType.isIntegral()) visibility("internal") diff --git a/libraries/tools/kotlin-stdlib-gen/src/templates/dsl/CommonTypes.kt b/libraries/tools/kotlin-stdlib-gen/src/templates/dsl/CommonTypes.kt index 7351f52e7f2..1b60b46199a 100644 --- a/libraries/tools/kotlin-stdlib-gen/src/templates/dsl/CommonTypes.kt +++ b/libraries/tools/kotlin-stdlib-gen/src/templates/dsl/CommonTypes.kt @@ -54,6 +54,7 @@ enum class PrimitiveType { ULong; val capacity by lazy { descendingByDomainCapacity.indexOf(this).let { if (it < 0) it else descendingByDomainCapacity.size - it } } + val capacityUnsigned by lazy { descendingByDomainCapacityUnsigned.indexOf(this).let { if (it < 0) it else descendingByDomainCapacityUnsigned.size - it } } companion object { val unsignedPrimitives = setOf(UInt, ULong, UByte, UShort)