diff --git a/docs/Range.md b/docs/Range.md index 07215c3..7409204 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -51,7 +51,6 @@ This might be confusing, but the range size and limits are used with for loops so their meaning is special. 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: val r = 1..3 @@ -86,6 +85,27 @@ but >>> 2 >>> 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 | member | description | args | diff --git a/docs/tutorial.md b/docs/tutorial.md index 5db663c..98bc092 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -645,6 +645,9 @@ Are the same as in string literals with little difference: ### Char instance members + assert( 'a'.code == 0x61 ) + >>> void + | member | type | meaning | |--------|------|--------------------------------| | code | Int | Unicode code for the character | diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 917afdd..20eba2a 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -541,7 +541,7 @@ class Compiler( // insofar we suggest source object is enumerable. Later we might need to add checks val sourceObj = source.execute(forContext) 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 breakCaught = false if (size > 0) { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index fa5d295..9bc2be2 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -277,9 +277,12 @@ fun Obj.toDouble(): Double = @Suppress("unused") fun Obj.toLong(): Long = - (this as? Numeric)?.longValue - ?: (this as? ObjString)?.value?.toLong() - ?: throw IllegalArgumentException("cannot convert to double $this") + when(this) { + is Numeric -> longValue + is ObjString -> value.toLong() + is ObjChar -> value.code.toLong() + else -> throw IllegalArgumentException("cannot convert to double $this") + } 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") -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().start ?: ObjNull - } - addFn("end") { - thisAs().end ?: ObjNull - } - addFn("isOpen") { - thisAs().let { it.start == null || it.end == null }.toObj() - } - addFn("isIntRange") { - thisAs().isIntRange.toObj() - } - addFn("isEndInclusive") { - thisAs().isEndInclusive.toObj() - } - addFn("size") { - val self = thisAs() - 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() { override fun toString(): String { 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 ObjClassCastError(context: Context, message: String) : 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) \ No newline at end of file +class ObjIndexOutOfBoundsError(context: Context, message: String = "index out of bounds") : ObjError(context, message) +class ObjIllegalArgumentError(context: Context, message: String = "illegal argument") : ObjError(context, message) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt new file mode 100644 index 0000000..7d58b6e --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjRange.kt @@ -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().start ?: ObjNull + } + addFn("end") { + thisAs().end ?: ObjNull + } + addFn("isOpen") { + thisAs().let { it.start == null || it.end == null }.toObj() + } + addFn("isIntRange") { + thisAs().isIntRange.toObj() + } + addFn("isCharRange") { + thisAs().isCharRange.toObj() + } + addFn("isEndInclusive") { + thisAs().isEndInclusive.toObj() + } + addFn("size") { + val self = thisAs() + 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) + } + } + } + } +} \ No newline at end of file diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 3d35c41..ddd9c38 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -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 // fun testLambda1() = runTest { // val l = eval("""