An extension or member to offset an `IntRange` wou...
# stdlib
l
An extension or member to offset an
IntRange
would be super useful. One use case is doing text/string replacement (without or with regexes). Such code:
Copy code
val offset = -matchResult.range.first
val rangeToReplaceInMatch = (urlGroup.range.first + offset)..(urlGroup.range.last + offset)
could become that one liner:
Copy code
val rangeToReplaceInMatch = urlGroup.range.offset(-matchResult.range.first)
BTW, I'm doing that in a Kotlin script, where, in a way, adding extra extensions is mostly adding noise to the signal, as scripts are better short for quick understanding.
👍 3
e
TBH I'd vote for an operator function
Copy code
operator fun IntRange.plus(offset: Int): IntRange
operator fun IntRange.minus(offset: Int): IntRange
although an infix function could work too (à la
skip
)
☝️ 1
I wonder if it should do bounds-checking, integer wraparound can lead to strange ranges.
Copy code
(-1 .. 1).count() // => 3
(-1 .. 1).offset(Int.MIN_VALUE).count() // => 0? 2? exception?
l
@ephemient I didn't suggest it because
IntRange
is an
IntProgression
which is
Iterable
, which already has a
plus
operator defined.
👍 1
e
oh that's true, it'll lead to confusion
l
Looks like we have the same needs. What do you think about my proposal of a function named
offset
? About expanding an
IntRange
on one or both sides, I'm wondering about your exact use cases and what names would work best for these potential new API members.
e
maybe like a data class?
Copy code
fun IntRange.copy(start: Int = start, endInclusive: Int = endInclusive): IntRange = start..endInclusive
doesn't seem more ergonomic though
I also sort of like the idea of a
Copy code
inline fun IntRange.mapMonotonic(block: (Int) -> Int): IntRange = block(start)..block(endInclusive)
range.mapMonotonic { it + offset }
but there's no way to verify that the transform is actually order-preserving, and it doesn't make sense on an
IntProgression
either
m
Copy code
operator fun IntRange.plus(offset: Int) =
    IntRange(start + offset, endInclusive + offset)

operator fun IntRange.minus(offset: Int) =
    IntRange(start - offset, endInclusive - offset)

fun main() {
    print((1..10) + 4) // 5..14
    print((1..10) - 4) // -3..6
}
e
as louiscad pointed out to me earlier, there already is an
operator fun <T> Iterable<T>.plus(element: T)
and
IntRange
implements
Iterable<Int>
l
This means such a change, as @marcinmoskala is now also suggesting, is technically source incompatible.
m
You are right, different meaning of plus can lead to a problematic misunderstandings and problems.
e
Kotlin has had source incompatible changes before but this one isn't worth it IMO.
z
What about naming that shl and shr? As int functions
e
not sure we need separate
shl
and
shr
when that could just be negated, but
shift
sounds like a reasonable name to me.
Copy code
infix fun Int.plusSaturated(other: Int) = (this + other).let { result ->
    if ((this < 0) != (other < 0) || (this < 0) == (result < 0)) result else if (this < 0) Int.MIN_VALUE else Int.MAX_VALUE
}
infix fun IntRange.shiftStart(x: Int) = (start plusSaturated x) .. endInclusive
infix fun IntRange.shiftEnd(x: Int) = start .. (endInclusive plusSaturated x)
infix fun IntRange.shift(x: Int) = (start plusSaturated x) .. (endInclusive plusSaturated x)
infix fun IntProgression.shiftFirst(x: Int) = (first plusSaturated x) .. last step step
infix fun IntProgression.shiftLast(x: Int) = first .. (last plusSaturated x) step step
infix fun IntProgression.shift(x: Int) = (first plusSaturated x) .. (last plusSaturated x) step step
(choosing to have
0..1 shift Int.MAX_VALUE
return
Int.MAX_VALUE..Int.MAX_VALUE
instead of
Int.MAX_VALUE..Int.MIN_VALUE
, neither is a great option but the latter is worse IMO)
z
I agree about shl and shr but that came from consistency with numbers. I suppose shift is a better name than offset because I assume offset to be non-negative (offsets are usually in some buffers, arrays, strings and mean the, start index which is ≥ 0), while shift is associated with the binary operation which can have argument of all sizes. But all that is imho, of course
To an addition to the proposal I would like to have some extending function that would add elements to the start or end: (0..10 step 2).extendEnd(2) = 0..14 step 2 (0..10 step 2).extendStart(2) = -4..10 step 2
e
extendStart(x)/extendEnd(x) = shiftStart(-x * step)/shiftEnd(x * step)?
z
@ephemient I'm not sure that all these functions are necessary enough to have them all in stdlib
Or we may name them somehow different like extendEndBy(n: elements) and extendEndUpTo(suggestedEnd: T)
e
I would absolutely love both exact and saturating arithmetic in stdlib. the other stuff… yeah, I dunno. louiscad and jw both wanted something of that form
1
extend*UpTo seems in line with my previous copy suggestion, but IMO copy would be a more typical name in Kotlin
z
But copy means another thing. Extend seems to have a closer meaning
l
IntRange
is not a binary concept, so
shr
or
shl
would be confusing and actually inconsistent.
shift
is too close to the binary meaning as well IMO. On the other hand,
offset
is not ambiguous AFAIK, and is used in things like array copying and in graphical UI dev for similar things.
z
Hmm, ok, I agree with @louiscad now