fix #23 string formatting and manipulations
This commit is contained in:
1 parent
1db1f12be3
commit
19eae213ec
13 files changed
+207
-40
No files matched your search
@@ -33,6 +33,13 @@ data class Arguments(val list: List<Obj>,val tailBlockMode: Boolean = false) : L
|
||||
return list.first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to list of kotlin objects, see [Obj.toKotlin].
|
||||
*/
|
||||
suspend fun toKotlinList(context: Context): List<Any?> {
|
||||
return list.map { it.toKotlin(context) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY = Arguments(emptyList())
|
||||
fun from(values: Collection<Obj>) = Arguments(values.toList())
|
||||
|
||||
@@ -59,7 +59,7 @@ class Compiler(
|
||||
parseBlock(cc)
|
||||
}
|
||||
|
||||
Token.Type.RBRACE -> {
|
||||
Token.Type.RBRACE, Token.Type.RBRACKET -> {
|
||||
cc.previous()
|
||||
return null
|
||||
}
|
||||
@@ -226,8 +226,7 @@ class Compiler(
|
||||
val index = parseStatement(cc) ?: throw ScriptError(t.pos, "Expecting index expression")
|
||||
cc.skipTokenOfType(Token.Type.RBRACKET, "missing ']' at the end of the list literal")
|
||||
operand = Accessor({ cxt ->
|
||||
val i = (index.execute(cxt) as? ObjInt)?.value?.toInt()
|
||||
?: cxt.raiseError("index must be integer")
|
||||
val i = index.execute(cxt)
|
||||
val x = left.getter(cxt).value
|
||||
if( x == ObjNull && isOptional) ObjNull.asReadonly
|
||||
else x.getAt(cxt, i).asMutable
|
||||
@@ -341,10 +340,10 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.DOTDOT, Token.Type.DOTDOTLT -> {
|
||||
// closed-range operator
|
||||
// range operator
|
||||
val isEndInclusive = t.type == Token.Type.DOTDOT
|
||||
val left = operand
|
||||
val right = parseStatement(cc)
|
||||
val right = parseExpression(cc)
|
||||
operand = Accessor {
|
||||
ObjRange(
|
||||
left?.getter?.invoke(it)?.value ?: ObjNull,
|
||||
@@ -366,11 +365,15 @@ class Compiler(
|
||||
} ?: parseLambdaExpression(cc)
|
||||
}
|
||||
|
||||
Token.Type.RBRACKET -> {
|
||||
cc.previous()
|
||||
return operand
|
||||
}
|
||||
|
||||
else -> {
|
||||
cc.previous()
|
||||
operand?.let { return it }
|
||||
operand = parseAccessor(cc) ?: throw ScriptError(t.pos, "Expecting expression")
|
||||
operand = parseAccessor(cc) ?: return null //throw ScriptError(t.pos, "Expecting expression")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1115,7 +1118,7 @@ class Compiler(
|
||||
var breakCaught = false
|
||||
|
||||
if (size > 0) {
|
||||
var current = runCatching { sourceObj.getAt(forContext, 0) }
|
||||
var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) }
|
||||
.getOrElse {
|
||||
throw ScriptError(
|
||||
tOp.pos,
|
||||
@@ -1142,7 +1145,7 @@ class Compiler(
|
||||
}
|
||||
} else result = body.execute(forContext)
|
||||
if (++index >= size) break
|
||||
current = sourceObj.getAt(forContext, index)
|
||||
current = sourceObj.getAt(forContext, ObjInt(index.toLong()))
|
||||
}
|
||||
}
|
||||
if (!breakCaught && elseStatement != null) {
|
||||
|
||||
@@ -42,6 +42,9 @@ data class Accessor(
|
||||
}
|
||||
|
||||
open class Obj {
|
||||
|
||||
val isNull by lazy { this === ObjNull }
|
||||
|
||||
var isFrozen: Boolean = false
|
||||
|
||||
private val monitor = Mutex()
|
||||
@@ -176,6 +179,13 @@ open class Obj {
|
||||
context.raiseNotImplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Lyng object to its Kotlin counterpart
|
||||
*/
|
||||
open suspend fun toKotlin(context: Context): Any? {
|
||||
return toString()
|
||||
}
|
||||
|
||||
fun willMutate(context: Context) {
|
||||
if (isFrozen) context.raiseError("attempt to mutate frozen object")
|
||||
}
|
||||
@@ -201,7 +211,7 @@ open class Obj {
|
||||
if (field.isMutable) field.value = newValue else context.raiseError("can't assign to read-only field: $name")
|
||||
}
|
||||
|
||||
open suspend fun getAt(context: Context, index: Int): Obj {
|
||||
open suspend fun getAt(context: Context, index: Obj): Obj {
|
||||
context.raiseNotImplemented("indexing")
|
||||
}
|
||||
|
||||
@@ -297,7 +307,7 @@ object ObjNull : Obj() {
|
||||
context.raiseNPE()
|
||||
}
|
||||
|
||||
override suspend fun getAt(context: Context, index: Int): Obj {
|
||||
override suspend fun getAt(context: Context, index: Obj): Obj {
|
||||
context.raiseNPE()
|
||||
}
|
||||
|
||||
@@ -310,6 +320,10 @@ object ObjNull : Obj() {
|
||||
}
|
||||
|
||||
override fun toString(): String = "null"
|
||||
|
||||
override suspend fun toKotlin(context: Context): Any? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface Numeric {
|
||||
|
||||
@@ -18,6 +18,10 @@ data class ObjBool(val value: Boolean) : Obj() {
|
||||
|
||||
override suspend fun logicalOr(context: Context, other: Obj): Obj = ObjBool(value || other.toBool())
|
||||
|
||||
override suspend fun toKotlin(context: Context): Any {
|
||||
return value
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("Bool")
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ val ObjArray by lazy {
|
||||
addFn("contains", isOpen = true) {
|
||||
val obj = args.firstAndOnly()
|
||||
for( i in 0..< thisObj.invokeInstanceMethod(this, "size").toInt()) {
|
||||
if( thisObj.getAt(this, i).compareTo(this, obj) == 0 ) return@addFn ObjTrue
|
||||
if( thisObj.getAt(this, ObjInt(i.toLong())).compareTo(this, obj) == 0 ) return@addFn ObjTrue
|
||||
}
|
||||
ObjFalse
|
||||
}
|
||||
|
||||
@@ -72,6 +72,10 @@ data class ObjInt(var value: Long) : Obj(), Numeric {
|
||||
} else null
|
||||
}
|
||||
|
||||
override suspend fun toKotlin(context: Context): Any {
|
||||
return value
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("Int")
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
list.joinToString(separator = ", ") { it.inspect() }
|
||||
}]"
|
||||
|
||||
fun normalize(context: Context, index: Int, allowisEndInclusive: Boolean = false): Int {
|
||||
fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
|
||||
val i = if (index < 0) list.size + index else index
|
||||
if (allowisEndInclusive && i == list.size) return i
|
||||
if (allowsEndInclusive && i == list.size) return i
|
||||
if (i !in list.indices) context.raiseError("index $index out of bounds for size ${list.size}")
|
||||
return i
|
||||
}
|
||||
|
||||
override suspend fun getAt(context: Context, index: Int): Obj {
|
||||
val i = normalize(context, index)
|
||||
override suspend fun getAt(context: Context, index: Obj): Obj {
|
||||
val i = normalize(context, index.toInt())
|
||||
return list[i]
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
override val objClass: ObjClass
|
||||
get() = type
|
||||
|
||||
override suspend fun toKotlin(context: Context): Any {
|
||||
return list.map { it.toKotlin(context) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("List", ObjArray).apply {
|
||||
|
||||
@@ -92,7 +96,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
)
|
||||
addFn("getAt") {
|
||||
requireExactCount(1)
|
||||
thisAs<ObjList>().getAt(this, requiredArg<ObjInt>(0).value.toInt())
|
||||
thisAs<ObjList>().getAt(this, requiredArg<Obj>(0))
|
||||
}
|
||||
addFn("putAt") {
|
||||
requireExactCount(2)
|
||||
@@ -113,7 +117,7 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
val l = thisAs<ObjList>()
|
||||
var index = l.normalize(
|
||||
this, requiredArg<ObjInt>(0).value.toInt(),
|
||||
allowisEndInclusive = true
|
||||
allowsEndInclusive = true
|
||||
)
|
||||
for (i in 1..<args.size) l.list.add(index++, args[i])
|
||||
ObjVoid
|
||||
|
||||
@@ -36,6 +36,13 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
||||
override suspend fun mod(context: Context, other: Obj): Obj =
|
||||
ObjReal(this.value % other.toDouble())
|
||||
|
||||
/**
|
||||
* Returns unboxed Double value
|
||||
*/
|
||||
override suspend fun toKotlin(context: Context): Any {
|
||||
return value
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type: ObjClass = ObjClass("Real").apply {
|
||||
createField(
|
||||
|
||||
@@ -2,11 +2,20 @@ package net.sergeych.lyng
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.sprintf.sprintf
|
||||
|
||||
@Serializable
|
||||
@SerialName("string")
|
||||
data class ObjString(val value: String) : Obj() {
|
||||
|
||||
// fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int {
|
||||
// val i = if (index < 0) value.length + index else index
|
||||
// if (allowsEndInclusive && i == value.length) return i
|
||||
// if (i !in value.indices) context.raiseError("index $index out of bounds for length ${value.length} of \"$value\"")
|
||||
// return i
|
||||
// }
|
||||
|
||||
|
||||
override suspend fun compareTo(context: Context, other: Obj): Int {
|
||||
if (other !is ObjString) return -2
|
||||
return this.value.compareTo(other.value)
|
||||
@@ -27,8 +36,21 @@ data class ObjString(val value: String) : Obj() {
|
||||
return ObjString(value + other.asStr.value)
|
||||
}
|
||||
|
||||
override suspend fun getAt(context: Context, index: Int): Obj {
|
||||
return ObjChar(value[index])
|
||||
override suspend fun getAt(context: Context, index: Obj): Obj {
|
||||
if( index is ObjInt ) return ObjChar(value[index.toInt()])
|
||||
if( index is ObjRange ) {
|
||||
val start = if(index.start == null || index.start.isNull) 0 else index.start.toInt()
|
||||
val end = if( index.end == null || index.end.isNull ) value.length else {
|
||||
val e = index.end.toInt()
|
||||
if( index.isEndInclusive) e + 1 else e
|
||||
}
|
||||
return ObjString(value.substring(start, end))
|
||||
}
|
||||
context.raiseArgumentError("String index must be Int or Range")
|
||||
}
|
||||
|
||||
override suspend fun callOn(context: Context): Obj {
|
||||
return ObjString(this.value.sprintf(*context.args.toKotlinList(context).toTypedArray()))
|
||||
}
|
||||
|
||||
override suspend fun contains(context: Context, other: Obj): Boolean {
|
||||
@@ -41,10 +63,12 @@ data class ObjString(val value: String) : Obj() {
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("String").apply {
|
||||
addConst("startsWith",
|
||||
statement {
|
||||
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
|
||||
})
|
||||
addFn("startsWith") {
|
||||
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
|
||||
}
|
||||
addFn("endsWith") {
|
||||
ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
|
||||
}
|
||||
addConst("length",
|
||||
statement { ObjInt(thisAs<ObjString>().value.length.toLong()) }
|
||||
)
|
||||
|
||||
@@ -1056,7 +1056,7 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIntOpenRangeInclusive() = runTest {
|
||||
fun testIntClosedRangeInclusive() = runTest {
|
||||
eval(
|
||||
"""
|
||||
val r = 10 .. 20
|
||||
@@ -1090,7 +1090,7 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIntOpenRangeExclusive() = runTest {
|
||||
fun testIntClosedRangeExclusive() = runTest {
|
||||
eval(
|
||||
"""
|
||||
val r = 10 ..< 20
|
||||
@@ -1126,7 +1126,7 @@ class ScriptTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIntOpenRangeInExclusive() = runTest {
|
||||
fun testIntClosedRangeInExclusive() = runTest {
|
||||
eval(
|
||||
"""
|
||||
assert( (1..3) !in (1..<3) )
|
||||
@@ -1135,6 +1135,39 @@ class ScriptTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOpenStartRanges() = runTest {
|
||||
eval("""
|
||||
var r = ..5
|
||||
assert( r::class == Range)
|
||||
assert( r.start == null)
|
||||
assert( r.end == 5)
|
||||
assert( r.isEndInclusive)
|
||||
|
||||
r = ..< 5
|
||||
assert( r::class == Range)
|
||||
assert( r.start == null)
|
||||
assert( r.end == 5)
|
||||
assert( !r.isEndInclusive)
|
||||
|
||||
assert( r.start == null)
|
||||
|
||||
assert( (-2..3) in r)
|
||||
assert( (-2..12) !in r)
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOpenEndRanges() = runTest {
|
||||
eval("""
|
||||
var r = 5..
|
||||
assert( r::class == Range)
|
||||
assert( r.end == null)
|
||||
assert( r.start == 5)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCharacterRange() = runTest {
|
||||
eval(
|
||||
@@ -2133,4 +2166,23 @@ class ScriptTest {
|
||||
assert(s.length == 2)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSprintf() = runTest {
|
||||
eval("""
|
||||
assertEquals( "123.45", "%3.2f"(123.451678) )
|
||||
assertEquals( "123.45: hello", "%3.2f: %s"(123.451678, "hello") )
|
||||
assertEquals( "123.45: true", "%3.2f: %s"(123.451678, true) )
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubstringRangeFailure() = runTest {
|
||||
eval("""
|
||||
assertEquals("pult", "catapult"[4..])
|
||||
assertEquals("cat", "catapult"[..2])
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user