fix #23 string formatting and manipulations
This commit is contained in:
parent
1db1f12be3
commit
19eae213ec
0
docs/String.md
Normal file
0
docs/String.md
Normal file
@ -1051,15 +1051,56 @@ Are the same as in string literals with little difference:
|
||||
|
||||
## String details
|
||||
|
||||
Strings are much like Kotlin ones:
|
||||
Strings are arrays of Unicode characters. It can be indexed, and indexing will
|
||||
return a valid Unicode character at position. No utf hassle:
|
||||
|
||||
"Hello".length
|
||||
"Парашют"[5]
|
||||
>>> 'ю'
|
||||
|
||||
Its length is, of course, in characters:
|
||||
|
||||
"разум".length
|
||||
>>> 5
|
||||
|
||||
And supports growing set of kotlin-borrowed operations, see below, for example:
|
||||
|
||||
assertEquals("Hell", "Hello".dropLast(1))
|
||||
>>> void
|
||||
|
||||
To format a string use sprintf-style modifiers like:
|
||||
|
||||
val a = "hello"
|
||||
val b = 11
|
||||
assertEquals( "hello:11", "%s:%d"(a, b) )
|
||||
assertEquals( " hello: 11", "%6s:%6d"(a, b) )
|
||||
assertEquals( "hello :11 ", "%-6s:%-6d"(a, b) )
|
||||
>>> void
|
||||
|
||||
List of format specifiers closely resembles C sprintf() one. See [format specifiers](https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary), this is doe using [mp_stools kotlin multiplatform library](https://github.com/sergeych/mp_stools). Currently supported Lyng types are `String`, `Int`, `Real`, `Bool`, the rest are displayed using their `toString()` representation.
|
||||
|
||||
This list will be extended.
|
||||
|
||||
To get the substring use:
|
||||
|
||||
assertEquals("pult", "catapult".takeLast(4))
|
||||
assertEquals("cat", "catapult".take(3))
|
||||
assertEquals("cat", "catapult".dropLast(5))
|
||||
assertEquals("pult", "catapult".drop(4))
|
||||
>>> void
|
||||
|
||||
And to get a portion you can slice it with range as the index:
|
||||
|
||||
assertEquals( "tap", "catapult"[ 2 .. 4 ])
|
||||
assertEquals( "tap", "catapult"[ 2 ..< 5 ])
|
||||
>>> void
|
||||
|
||||
Open-ended ranges could be used to get start and end too:
|
||||
|
||||
assertEquals( "cat", "catapult"[ ..< 3 ])
|
||||
assertEquals( "cat", "catapult"[ .. 2 ])
|
||||
assertEquals( "pult", "catapult"[ 4.. ])
|
||||
>>> void
|
||||
|
||||
### String operations
|
||||
|
||||
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
||||
@ -1067,15 +1108,22 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
|
||||
Typical set of String functions includes:
|
||||
|
||||
| fun/prop | description / notes |
|
||||
|------------------|------------------------------------------------------------|
|
||||
|--------------------|------------------------------------------------------------|
|
||||
| lower() | change case to unicode upper |
|
||||
| upper() | change case to unicode lower |
|
||||
| startsWith(prefix) | true if starts with a prefix |
|
||||
| endsWith(prefix) | true if ends with a prefix |
|
||||
| take(n) | get a new string from up to n first characters |
|
||||
| takeLast(n) | get a new string from up to n last characters |
|
||||
| drop(n) | get a new string dropping n first chars, or empty string |
|
||||
| dropLast(n) | get a new string dropping n last chars, or empty string |
|
||||
| size | size in characters like `length` because String is [Array] |
|
||||
| (args...) | sprintf-like formatting, see [string formatting] |
|
||||
| [index] | character at index |
|
||||
| [Range] | substring at range |
|
||||
| s1 + s2 | concatenation |
|
||||
| s1 += s2 | self-modifying concatenation |
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1111,5 +1159,5 @@ See [math functions](math.md). Other general purpose functions are:
|
||||
[Iterator]: Iterator.md
|
||||
[Real]: Real.md
|
||||
[Range]: Range.md
|
||||
|
||||
|
||||
[String]: String.md
|
||||
[string formatting]: https://github.com/sergeych/mp_stools?tab=readme-ov-file#sprintf-syntax-summary
|
||||
|
@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.6.-SNAPSHOT"
|
||||
version = "0.6.4-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
@ -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 {
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user