for in range, range size and indexing

This commit is contained in:
Sergey Chernov 2025-05-31 23:43:35 +04:00
parent dcab60b7bb
commit d2c732ceef
6 changed files with 165 additions and 119 deletions

View File

@ -51,7 +51,6 @@ This might be confusing, but the range size and limits are used with for loops
so their meaning is special. so their meaning is special.
For open ranges, size throws and exception. For open ranges, size throws and exception.
For Int ranges, the `size` is `end` - `start` possibly + 1 for ind-inclusive ranges, and indexing getter returns all values from start to end, probably, inclusive: For Int ranges, the `size` is `end` - `start` possibly + 1 for ind-inclusive ranges, and indexing getter returns all values from start to end, probably, inclusive:
val r = 1..3 val r = 1..3
@ -86,6 +85,27 @@ but
>>> 2 >>> 2
>>> void >>> void
## Character ranges
You can use Char as both ends of the closed range:
val r = 'a' .. 'c'
assert( 'b' in r)
assert( 'e' !in r)
assert( 'c' == r[2] )
for( ch in r )
println(ch)
>>> a
>>> b
>>> c
>>> void
Exclusive end char ranges are supported too:
('a'..<'c').size
>>> 2
# Instance members # Instance members
| member | description | args | | member | description | args |

View File

@ -645,6 +645,9 @@ Are the same as in string literals with little difference:
### Char instance members ### Char instance members
assert( 'a'.code == 0x61 )
>>> void
| member | type | meaning | | member | type | meaning |
|--------|------|--------------------------------| |--------|------|--------------------------------|
| code | Int | Unicode code for the character | | code | Int | Unicode code for the character |

View File

@ -541,7 +541,7 @@ class Compiler(
// insofar we suggest source object is enumerable. Later we might need to add checks // insofar we suggest source object is enumerable. Later we might need to add checks
val sourceObj = source.execute(forContext) val sourceObj = source.execute(forContext)
val size = runCatching { sourceObj.callInstanceMethod(forContext, "size").toInt() } val size = runCatching { sourceObj.callInstanceMethod(forContext, "size").toInt() }
.getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size") } .getOrElse { throw ScriptError(tOp.pos, "object is not enumerable: no size", it) }
var result: Obj = ObjVoid var result: Obj = ObjVoid
var breakCaught = false var breakCaught = false
if (size > 0) { if (size > 0) {

View File

@ -277,9 +277,12 @@ fun Obj.toDouble(): Double =
@Suppress("unused") @Suppress("unused")
fun Obj.toLong(): Long = fun Obj.toLong(): Long =
(this as? Numeric)?.longValue when(this) {
?: (this as? ObjString)?.value?.toLong() is Numeric -> longValue
?: throw IllegalArgumentException("cannot convert to double $this") is ObjString -> value.toLong()
is ObjChar -> value.code.toLong()
else -> throw IllegalArgumentException("cannot convert to double $this")
}
fun Obj.toInt(): Int = toLong().toInt() fun Obj.toInt(): Int = toLong().toInt()
@ -287,118 +290,6 @@ fun Obj.toBool(): Boolean =
(this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this") (this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this")
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
override val objClass: ObjClass = type
override fun toString(): String {
val result = StringBuilder()
result.append("${start ?: '∞'} ..")
if (!isEndInclusive) result.append('<')
result.append(" ${end ?: '∞'}")
return result.toString()
}
suspend fun containsRange(context: Context, other: ObjRange): Boolean {
if (start != null) {
// our start is not -∞ so other start should be GTE or is not contained:
if (other.start != null && start.compareTo(context, other.start) > 0) return false
}
if (end != null) {
// same with the end: if it is open, it can't be contained in ours:
if (other.end == null) return false
// both exists, now there could be 4 cases:
return when {
other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0
!other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) >= 0
other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) > 0
!other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0
else -> throw IllegalStateException("unknown comparison")
}
}
return true
}
override suspend fun contains(context: Context, other: Obj): Boolean {
if (other is ObjRange)
return containsRange(context, other)
if (start == null && end == null) return true
if (start != null) {
if (start.compareTo(context, other) > 0) return false
}
if (end != null) {
val cmp = end.compareTo(context, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
}
return true
}
override suspend fun getAt(context: Context, index: Int): Obj {
if( !isIntRange ) {
return when (index) {
0 -> start ?: ObjNull
1 -> end ?: ObjNull
else -> context.raiseIndexOutOfBounds("index out of range: $index for max of 2 for non-int ranges")
}
}
// int range, should be finite
val r0 = start?.toInt() ?: context.raiseArgumentError("start is not integer")
var r1 = end?.toInt() ?: context.raiseArgumentError("end is not integer")
if( isEndInclusive ) r1++
val i = index + r0
if( i >= r1 ) context.raiseIndexOutOfBounds("index $index is not in range (${r1-r0})")
return ObjInt(i.toLong())
}
val isIntRange: Boolean by lazy {
start is ObjInt && end is ObjInt
}
companion object {
val type = ObjClass("Range").apply {
addFn("start") {
thisAs<ObjRange>().start ?: ObjNull
}
addFn("end") {
thisAs<ObjRange>().end ?: ObjNull
}
addFn("isOpen") {
thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj()
}
addFn("isIntRange") {
thisAs<ObjRange>().isIntRange.toObj()
}
addFn("isEndInclusive") {
thisAs<ObjRange>().isEndInclusive.toObj()
}
addFn("size") {
val self = thisAs<ObjRange>()
if (self.start == null || self.end == null)
raiseError("size is only available for finite ranges")
if (self.isIntRange) {
if (self.isEndInclusive)
ObjInt(self.end.toLong() - self.start.toLong() + 1)
else
ObjInt(self.end.toLong() - self.start.toLong())
} else {
ObjInt(2)
}
}
}
}
}
data class ObjNamespace(val name: String) : Obj() { data class ObjNamespace(val name: String) : Obj() {
override fun toString(): String { override fun toString(): String {
return "namespace ${name}" return "namespace ${name}"
@ -413,5 +304,5 @@ class ObjNullPointerError(context: Context) : ObjError(context, "object is null"
class ObjAssertionError(context: Context, message: String) : ObjError(context, message) class ObjAssertionError(context: Context, message: String) : ObjError(context, message)
class ObjClassCastError(context: Context, message: String) : ObjError(context, message) class ObjClassCastError(context: Context, message: String) : ObjError(context, message)
class ObjIndexOutOfBoundsError(context: Context,message: String="index out of bounds") : ObjError(context,message) class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message)
class ObjIllegalArgumentError(context: Context,message: String="illegal argument") : ObjError(context,message) class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message)

View File

@ -0,0 +1,120 @@
package net.sergeych.ling
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
override val objClass: ObjClass = type
override fun toString(): String {
val result = StringBuilder()
result.append("${start?.inspect() ?: '∞'} ..")
if (!isEndInclusive) result.append('<')
result.append(" ${end?.inspect() ?: '∞'}")
return result.toString()
}
suspend fun containsRange(context: Context, other: ObjRange): Boolean {
if (start != null) {
// our start is not -∞ so other start should be GTE or is not contained:
if (other.start != null && start.compareTo(context, other.start) > 0) return false
}
if (end != null) {
// same with the end: if it is open, it can't be contained in ours:
if (other.end == null) return false
// both exists, now there could be 4 cases:
return when {
other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0
!other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) >= 0
other.isEndInclusive && !isEndInclusive ->
end.compareTo(context, other.end) > 0
!other.isEndInclusive && isEndInclusive ->
end.compareTo(context, other.end) >= 0
else -> throw IllegalStateException("unknown comparison")
}
}
return true
}
override suspend fun contains(context: Context, other: Obj): Boolean {
if (other is ObjRange)
return containsRange(context, other)
if (start == null && end == null) return true
if (start != null) {
if (start.compareTo(context, other) > 0) return false
}
if (end != null) {
val cmp = end.compareTo(context, other)
if (isEndInclusive && cmp < 0 || !isEndInclusive && cmp <= 0) return false
}
return true
}
override suspend fun getAt(context: Context, index: Int): Obj {
if (!isIntRange && !isCharRange) {
return when (index) {
0 -> start ?: ObjNull
1 -> end ?: ObjNull
else -> context.raiseIndexOutOfBounds("index out of range: $index for max of 2 for non-int ranges")
}
}
// int range, should be finite
val r0 = start?.toInt() ?: context.raiseArgumentError("start is not integer")
var r1 = end?.toInt() ?: context.raiseArgumentError("end is not integer")
if (isEndInclusive) r1++
val i = index + r0
if (i >= r1) context.raiseIndexOutOfBounds("index $index is not in range (${r1 - r0})")
return if( isIntRange ) ObjInt(i.toLong()) else ObjChar(i.toChar())
}
val isIntRange: Boolean by lazy {
start is ObjInt && end is ObjInt
}
val isCharRange: Boolean by lazy {
start is ObjChar && end is ObjChar
}
companion object {
val type = ObjClass("Range").apply {
addFn("start") {
thisAs<ObjRange>().start ?: ObjNull
}
addFn("end") {
thisAs<ObjRange>().end ?: ObjNull
}
addFn("isOpen") {
thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj()
}
addFn("isIntRange") {
thisAs<ObjRange>().isIntRange.toObj()
}
addFn("isCharRange") {
thisAs<ObjRange>().isCharRange.toObj()
}
addFn("isEndInclusive") {
thisAs<ObjRange>().isEndInclusive.toObj()
}
addFn("size") {
val self = thisAs<ObjRange>()
if (self.start == null || self.end == null)
raiseError("size is only available for finite ranges")
if (self.isIntRange || self.isCharRange) {
if (self.isEndInclusive)
ObjInt(self.end.toLong() - self.start.toLong() + 1)
else
ObjInt(self.end.toLong() - self.start.toLong())
} else {
ObjInt(2)
}
}
}
}
}

View File

@ -975,6 +975,18 @@ class ScriptTest {
) )
} }
@Test
fun testCharacterRange() = runTest {
eval("""
val x = '0'..'9'
println(x)
assert( '5' in x)
assert( 'z' !in x)
for( ch in x )
println(ch)
""".trimIndent())
}
// @Test // @Test
// fun testLambda1() = runTest { // fun testLambda1() = runTest {
// val l = eval(""" // val l = eval("""