diff --git a/runtime/src/main/kotlin/kotlin/ranges/Ranges.kt b/runtime/src/main/kotlin/kotlin/ranges/Ranges.kt index 2e88dd173fa..753e80e49cc 100644 --- a/runtime/src/main/kotlin/kotlin/ranges/Ranges.kt +++ b/runtime/src/main/kotlin/kotlin/ranges/Ranges.kt @@ -477,3 +477,121 @@ public infix fun Byte.downTo(to: Short): IntProgression = */ public infix fun Short.downTo(to: Short): IntProgression = IntProgression.fromClosedRange(this.toInt(), to.toInt(), -1) + +/** + * Represents a range of floating point numbers. + * Extends [ClosedRange] interface providing custom operation [lessThanOrEquals] for comparing values of range domain type. + * + * This interface is implemented by floating point ranges returned by [Float.rangeTo] and [Double.rangeTo] operators to + * achieve IEEE-754 comparison order instead of total order of floating point numbers. + */ +public interface ClosedFloatingPointRange> : ClosedRange { + override fun contains(value: T): Boolean = lessThanOrEquals(start, value) && lessThanOrEquals(value, endInclusive) + override fun isEmpty(): Boolean = !lessThanOrEquals(start, endInclusive) + + /** + * Compares two values of range domain type and returns true if first is less than or equal to second. + */ + fun lessThanOrEquals(a: T, b: T): Boolean +} + +/** + * Represents a range of [Comparable] values. + */ +private open class ComparableRange> ( + override val start: T, + override val endInclusive: T +): ClosedRange { + + override fun equals(other: Any?): Boolean { + return other is ComparableRange<*> && (isEmpty() && other.isEmpty() || + start == other.start && endInclusive == other.endInclusive) + } + + override fun hashCode(): Int { + return if (isEmpty()) -1 else 31 * start.hashCode() + endInclusive.hashCode() + } + + override fun toString(): String = "$start..$endInclusive" +} + +/** + * A closed range of values of type `Double`. + * + * Numbers are compared with the ends of this range according to IEEE-754. + */ +private class ClosedDoubleRange ( + start: Double, + endInclusive: Double +) : ClosedFloatingPointRange { + private val _start = start + private val _endInclusive = endInclusive + override val start: Double get() = _start + override val endInclusive: Double get() = _endInclusive + + override fun lessThanOrEquals(a: Double, b: Double): Boolean = a <= b + + override fun contains(value: Double): Boolean = value >= _start && value <= _endInclusive + override fun isEmpty(): Boolean = !(_start <= _endInclusive) + + override fun equals(other: Any?): Boolean { + return other is ClosedDoubleRange && (isEmpty() && other.isEmpty() || + _start == other._start && _endInclusive == other._endInclusive) + } + + override fun hashCode(): Int { + return if (isEmpty()) -1 else 31 * _start.hashCode() + _endInclusive.hashCode() + } + override fun toString(): String = "$_start..$_endInclusive" +} + +/** + * A closed range of values of type `Float`. + * + * Numbers are compared with the ends of this range according to IEEE-754. + */ +private class ClosedFloatRange ( + start: Float, + endInclusive: Float +): ClosedFloatingPointRange { + private val _start = start + private val _endInclusive = endInclusive + override val start: Float get() = _start + override val endInclusive: Float get() = _endInclusive + + override fun lessThanOrEquals(a: Float, b: Float): Boolean = a <= b + + override fun contains(value: Float): Boolean = value >= _start && value <= _endInclusive + override fun isEmpty(): Boolean = !(_start <= _endInclusive) + + override fun equals(other: Any?): Boolean { + return other is ClosedFloatRange && (isEmpty() && other.isEmpty() || + _start == other._start && _endInclusive == other._endInclusive) + } + + override fun hashCode(): Int { + return if (isEmpty()) -1 else 31 * _start.hashCode() + _endInclusive.hashCode() + } + override fun toString(): String = "$_start..$_endInclusive" +} + +/** + * Creates a range from this [Comparable] value to the specified [that] value. + * + * This value needs to be smaller than [that] value, otherwise the returned range will be empty. + */ +public operator fun > T.rangeTo(that: T): ClosedRange = ComparableRange(this, that) + +/** + * Creates a range from this [Double] value to the specified [other] value. + * + * Numbers are compared with the ends of this range according to IEEE-754. + */ +public operator fun Double.rangeTo(that: Double): ClosedFloatingPointRange = ClosedDoubleRange(this, that) + +/** + * Creates a range from this [Float] value to the specified [other] value. + * + * Numbers are compared with the ends of this range according to IEEE-754. + */ +public operator fun Float.rangeTo(that: Float): ClosedFloatingPointRange = ClosedFloatRange(this, that) diff --git a/runtime/src/main/kotlin/kotlin/ranges/_Ranges.kt b/runtime/src/main/kotlin/kotlin/ranges/_Ranges.kt index cdf5221c91b..5d81d75c1b2 100644 --- a/runtime/src/main/kotlin/kotlin/ranges/_Ranges.kt +++ b/runtime/src/main/kotlin/kotlin/ranges/_Ranges.kt @@ -143,3 +143,307 @@ public infix fun Short.until(to: Short): IntRange { return this.toInt() .. (to.toInt() - 1).toInt() } +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Byte): Boolean { + return contains(value.toInt()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Byte): Boolean { + return contains(value.toLong()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Byte): Boolean { + return contains(value.toShort()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Byte): Boolean { + return contains(value.toDouble()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Byte): Boolean { + return contains(value.toFloat()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Double): Boolean { + return value.toIntExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Double): Boolean { + return value.toLongExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Double): Boolean { + return value.toByteExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Double): Boolean { + return value.toShortExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Double): Boolean { + return contains(value.toFloat()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Float): Boolean { + return value.toIntExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Float): Boolean { + return value.toLongExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Float): Boolean { + return value.toByteExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Float): Boolean { + return value.toShortExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Float): Boolean { + return contains(value.toDouble()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Int): Boolean { + return contains(value.toLong()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Int): Boolean { + return value.toByteExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Int): Boolean { + return value.toShortExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Int): Boolean { + return contains(value.toDouble()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Int): Boolean { + return contains(value.toFloat()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Long): Boolean { + return value.toIntExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Long): Boolean { + return value.toByteExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Long): Boolean { + return value.toShortExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Long): Boolean { + return contains(value.toDouble()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Long): Boolean { + return contains(value.toFloat()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Short): Boolean { + return contains(value.toInt()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Short): Boolean { + return contains(value.toLong()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Short): Boolean { + return value.toByteExactOrNull().let { if (it != null) contains(it) else false } +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Short): Boolean { + return contains(value.toDouble()) +} + +/** + * Checks if the specified [value] belongs to this range. + */ +public operator fun ClosedRange.contains(value: Short): Boolean { + return contains(value.toFloat()) +} + +/** + * Returns a progression from this value down to the specified [to] value with the step -1. + * + * The [to] value has to be less than this value. + */ +public infix fun Short.downTo(to: Long): LongProgression { + return LongProgression.fromClosedRange(this.toLong(), to, -1L) +} + +/** + * Returns a progression that goes over the same range in the opposite direction with the same step. + */ +public fun LongProgression.reversed(): LongProgression { + return LongProgression.fromClosedRange(last, first, -step) +} + +/** + * Returns a progression that goes over the same range in the opposite direction with the same step. + */ +public fun CharProgression.reversed(): CharProgression { + return CharProgression.fromClosedRange(last, first, -step) +} + +internal fun Int.toByteExactOrNull(): Byte? { + return if (this in Byte.MIN_VALUE.toInt()..Byte.MAX_VALUE.toInt()) this.toByte() else null +} + +internal fun Long.toByteExactOrNull(): Byte? { + return if (this in Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()) this.toByte() else null +} + +internal fun Short.toByteExactOrNull(): Byte? { + return if (this in Byte.MIN_VALUE.toShort()..Byte.MAX_VALUE.toShort()) this.toByte() else null +} + +internal fun Double.toByteExactOrNull(): Byte? { + return if (this in Byte.MIN_VALUE.toDouble()..Byte.MAX_VALUE.toDouble()) this.toByte() else null +} + +internal fun Float.toByteExactOrNull(): Byte? { + return if (this in Byte.MIN_VALUE.toFloat()..Byte.MAX_VALUE.toFloat()) this.toByte() else null +} + +internal fun Long.toIntExactOrNull(): Int? { + return if (this in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) this.toInt() else null +} + +internal fun Double.toIntExactOrNull(): Int? { + return if (this in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble()) this.toInt() else null +} + +internal fun Float.toIntExactOrNull(): Int? { + return if (this in Int.MIN_VALUE.toFloat()..Int.MAX_VALUE.toFloat()) this.toInt() else null +} + +internal fun Double.toLongExactOrNull(): Long? { + return if (this in Long.MIN_VALUE.toDouble()..Long.MAX_VALUE.toDouble()) this.toLong() else null +} + +internal fun Float.toLongExactOrNull(): Long? { + return if (this in Long.MIN_VALUE.toFloat()..Long.MAX_VALUE.toFloat()) this.toLong() else null +} + +internal fun Int.toShortExactOrNull(): Short? { + return if (this in Short.MIN_VALUE.toInt()..Short.MAX_VALUE.toInt()) this.toShort() else null +} + +internal fun Long.toShortExactOrNull(): Short? { + return if (this in Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()) this.toShort() else null +} + +internal fun Double.toShortExactOrNull(): Short? { + return if (this in Short.MIN_VALUE.toDouble()..Short.MAX_VALUE.toDouble()) this.toShort() else null +} + +internal fun Float.toShortExactOrNull(): Short? { + return if (this in Short.MIN_VALUE.toFloat()..Short.MAX_VALUE.toFloat()) this.toShort() else null +} + +/** + * Ensures that this value lies in the specified [range]. + * + * @return this value if it's in the [range], or `range.start` if this value is less than `range.start`, or `range.endInclusive` if this value is greater than `range.endInclusive`. + */ +public fun > T.coerceIn(range: ClosedFloatingPointRange): T { + if (range.isEmpty()) throw IllegalArgumentException("Cannot coerce value to an empty range: $range.") + return when { + // this < start equiv to this <= start && !(this >= start) + range.lessThanOrEquals(this, range.start) && !range.lessThanOrEquals(range.start, this) -> range.start + // this > end equiv to this >= end && !(this <= end) + range.lessThanOrEquals(range.endInclusive, this) && !range.lessThanOrEquals(this, range.endInclusive) -> range.endInclusive + else -> this + } +}