diff --git a/docs/List.md b/docs/List.md index 474ff3e..c2b2401 100644 --- a/docs/List.md +++ b/docs/List.md @@ -20,11 +20,11 @@ indexing is zero-based, as in C/C++/Java/Kotlin, etc. list[1] >>> 20 -Using negative indexes has a special meaning: _offset from the end of the list_: +There is a shortcut for the last: val list = [10, 20, 30] - list[-1] - >>> 30 + [list.last, list.lastIndex] + >>> [30, 2] __Important__ negative indexes works wherever indexes are used, e.g. in insertion and removal methods too. @@ -38,7 +38,8 @@ You can concatenate lists or iterable objects: ## Appending -To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will concatenate [Iterable] objects instead of appending them. To append [Iterable] instance itself, use `list.add`: +To append to lists, use `+=` with elements, lists and any [Iterable] instances, but beware it will +concatenate [Iterable] objects instead of appending them. To append [Iterable] instance itself, use `list.add`: var list = [1, 2] val other = [3, 4] @@ -57,7 +58,28 @@ To append to lists, use `+=` with elements, lists and any [Iterable] instances, >>> void +## Removing elements +List is mutable, so it is possible to remove its contents. To remove a single element +by index use: + + assertEquals( [1,2,3].removeAt(1), [1,3] ) + assertEquals( [1,2,3].removeAt(0), [2,3] ) + assertEquals( [1,2,3].removeLast(), [1,2] ) + >>> void + +There is a way to remove a range (see [Range] for more on ranges): + + assertEquals( [1, 4], [1,2,3,4].removeRange(1..2)) + assertEquals( [1, 4], [1,2,3,4].removeRange(1..<3)) + >>> void + +Open end ranges remove head and tail elements: + + assertEquals( [3, 4, 5], [1,2,3,4,5].removeRange(..1)) + assertEquals( [3, 4, 5], [1,2,3,4,5].removeRange(..<2)) + assertEquals( [1, 2], [1,2,3,4,5].removeRange( (2..) )) + >>> void ## Comparisons @@ -72,19 +94,44 @@ To append to lists, use `+=` with elements, lists and any [Iterable] instances, ## Members -| name | meaning | type | -|-----------------------------------|-------------------------------------|----------| -| `size` | current size | Int | -| `add(elements...)` | add one or more elements to the end | Any | -| `addAt(index,elements...)` | insert elements at position | Int, Any | -| `removeAt(index)` | remove element at position | Int | -| `removeRangeInclusive(start,end)` | remove range, inclusive (1) | Int, Int | -| | | | +| name | meaning | type | +|-------------------------------|---------------------------------------|-------------| +| `size` | current size | Int | +| `add(elements...)` | add one or more elements to the end | Any | +| `insertAt(index,elements...)` | insert elements at position | Int, Any | +| `removeAt(index)` | remove element at position | Int | +| `remove(from,toNonInclusive)` | remove range from (incl) to (nonincl) | Int, Int | +| `remove(Range)` | remove range | Range | +| `removeLast()` | remove last element | | +| `removeLast(n)` | remove n last elements | Int | +| `contains(element)` | check the element is in the list (1) | | +| `[index]` | get or set element at index | Int | +| `[Range]` | get slice of the array (copy) | Range | +| `+=` | append element(s) | List or Obj | (1) -: end-inclisiveness allows to use negative indexes to, for exampe, remove several last elements, like `list.removeRangeInclusive(-2, -1)` will remove two last elements. +: optimized implementation that override `Array` one +(2) +: `+=` append either a single element, or all elements if the List or other Iterable +instance is appended. If you want to append an Iterable object itself, use `add` instead. + +## Member inherited from Array + +| name | meaning | type | +|------------------|--------------------------------|-------| +| `last` | last element (throws if empty) | | +| `lastOrNull` | last element or null | | +| `lastIndex` | | Int | +| `indices` | range of indexes | Range | +| `contains(item)` | test that item is in the list | | + +(1) +: end-inclisiveness allows to use negative indexes to, for exampe, remove several last elements, like +`list.removeRangeInclusive(-2, -1)` will remove two last elements. # Notes Could be rewritten using array as a class but List as the interface + +[Range]: Range.md \ No newline at end of file diff --git a/docs/tutorial.md b/docs/tutorial.md index dd76c1d..25ac917 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -514,62 +514,73 @@ To add elements to the list: assert( x == [1, 2, 3, "the", "end"]) >>> void -Self-modifying concatenation by `+=` also works: +Self-modifying concatenation by `+=` also works (also with single elements): val x = [1, 2] x += [3, 4] - assert( x == [1, 2, 3, 4]) + x += 5 + assert( x == [1, 2, 3, 4, 5]) >>> void -You can insert elements at any position using `addAt`: +You can insert elements at any position using `insertAt`: val x = [1,2,3] - x.addAt(1, "foo", "bar") + x.insertAt(1, "foo", "bar") assert( x == [1, "foo", "bar", 2, 3]) >>> void Using splat arguments can simplify inserting list in list: val x = [1, 2, 3] - x.addAt( 1, ...[0,100,0]) + x.insertAt( 1, ...[0,100,0]) x >>> [1, 0, 100, 0, 2, 3] -Using negative indexes can insert elements as offset from the end, for example: - - val x = [1,2,3] - x.addAt(-1, 10) - x - >>> [1, 2, 10, 3] Note that to add to the end you still need to use `add` or positive index of the after-last element: val x = [1,2,3] - x.addAt(3, 10) + x.insertAt(3, 10) x >>> [1, 2, 3, 10] +but it is much simpler, and we recommend to use '+=' + + val x = [1,2,3] + x += 10 + >>> [1, 2, 3, 10] + ## Removing list items val x = [1, 2, 3, 4, 5] x.removeAt(2) assert( x == [1, 2, 4, 5]) // or remove range (start inclusive, end exclusive): - x.removeRangeInclusive(1,2) + x.removeRange(1..2) assert( x == [1, 5]) >>> void -Again, you can use negative indexes. For example, removing last elements like: +There is a shortcut to remove the last elements: val x = [1, 2, 3, 4, 5] // remove last: - x.removeAt(-1) + x.removeLast() assert( x == [1, 2, 3, 4]) // remove 2 last: - x.removeRangeInclusive(-2,-1) - assert( x == [1, 2]) + x.removeLast(2) + assertEquals( [1, 2], x) + >>> void + +You can get ranges to extract a portion from a list: + + val list = [1, 2, 3, 4, 5] + assertEquals( [1,2,3], list[..2]) + assertEquals( [1,2,], list[..<2]) + assertEquals( [4,5], list[3..]) + assertEquals( [2,3], list[1..2]) + assertEquals( [2,3], list[1..<3]) >>> void # Flow control operators diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 535bc0c..faffd1d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -365,7 +365,7 @@ class Compiler( } ?: parseLambdaExpression(cc) } - Token.Type.RBRACKET -> { + Token.Type.RBRACKET, Token.Type.RPAREN -> { cc.previous() return operand } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index aad0024..7f500fe 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -161,13 +161,26 @@ 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, ObjInt(i.toLong())).compareTo(this, obj) == 0 ) return@addFn ObjTrue + for (i in 0.. = mutableListOf()) : Obj() { list.joinToString(separator = ", ") { it.inspect() } }]" - fun normalize(context: Context, index: Int, allowsEndInclusive: Boolean = false): Int { - val i = if (index < 0) list.size + index else index - 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: Obj): Obj { - val i = normalize(context, index.toInt()) - return list[i] + return when (index) { + is ObjInt -> { + list[index.toInt()] + } + + is ObjRange -> { + when { + index.start is ObjInt && index.end is ObjInt -> { + if (index.isEndInclusive) + ObjList(list.subList(index.start.toInt(), index.end.toInt() + 1).toMutableList()) + else + ObjList(list.subList(index.start.toInt(), index.end.toInt()).toMutableList()) + } + + index.isOpenStart && !index.isOpenEnd -> { + if (index.isEndInclusive) + ObjList(list.subList(0, index.end!!.toInt() + 1).toMutableList()) + else + ObjList(list.subList(0, index.end!!.toInt()).toMutableList()) + } + + index.isOpenEnd && !index.isOpenStart -> { + ObjList(list.subList(index.start!!.toInt(), list.size).toMutableList()) + } + + index.isOpenStart && index.isOpenEnd -> { + ObjList(list.toMutableList()) + } + + else -> { + throw RuntimeException("Can't apply range for index: $index") + } + } + } + + else -> context.raiseArgumentError("Illegal index object for a list: ${index.inspect()}") + } } override suspend fun putAt(context: Context, index: Int, newValue: Obj) { - val i = normalize(context, index) + val i = index list[i] = newValue } @@ -111,33 +139,74 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { ObjVoid } ) - createField("addAt", - statement { - if (args.size < 2) raiseError("addAt takes 2+ arguments") - val l = thisAs() - var index = l.normalize( - this, requiredArg(0).value.toInt(), - allowsEndInclusive = true - ) - for (i in 1..() + var index = requiredArg(0).value.toInt() + for (i in 1..() - val start = self.normalize(this, requiredArg(0).value.toInt()) + val start = requiredArg(0).value.toInt() if (args.size == 2) { val end = requireOnlyArg().value.toInt() - self.list.subList(start, self.normalize(this, end)).clear() + self.list.subList(start, end).clear() } else self.list.removeAt(start) self } - addFn("removeRangeInclusive") { + + addFn("removeLast") { val self = thisAs() - val start = self.normalize(this, requiredArg(0).value.toInt()) - val end = self.normalize(this, requiredArg(1).value.toInt()) + 1 - self.list.subList(start, end).clear() + if (args.isNotEmpty()) { + val count = requireOnlyArg().value.toInt() + val size = self.list.size + if (count >= size) self.list.clear() + else self.list.subList(size - count, size).clear() + } else self.list.removeLast() + self + } + + addFn("removeRange") { + val self = thisAs() + val list = self.list + val range = requiredArg(0) + if (range is ObjRange) { + val index = range + when { + index.start is ObjInt && index.end is ObjInt -> { + if (index.isEndInclusive) + list.subList(index.start.toInt(), index.end.toInt() + 1) + else + list.subList(index.start.toInt(), index.end.toInt()) + } + + index.isOpenStart && !index.isOpenEnd -> { + if (index.isEndInclusive) + list.subList(0, index.end!!.toInt() + 1) + else + list.subList(0, index.end!!.toInt()) + } + + index.isOpenEnd && !index.isOpenStart -> { + list.subList(index.start!!.toInt(), list.size) + } + + index.isOpenStart && index.isOpenEnd -> { + list + } + + else -> { + throw RuntimeException("Can't apply range for index: $index") + } + }.clear() + } else { + val start = range.toInt() + val end = requiredArg(1).value.toInt() + 1 + self.list.subList(start, end).clear() + } self } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjRange.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjRange.kt index a505239..e849607 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ObjRange.kt @@ -2,6 +2,9 @@ package net.sergeych.lyng class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() { + val isOpenStart by lazy { start == null || start.isNull } + val isOpenEnd by lazy { end == null || end.isNull } + override val objClass: ObjClass = type override fun toString(): String { diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index ca60393..fba52d5 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1137,7 +1137,8 @@ class ScriptTest { @Test fun testOpenStartRanges() = runTest { - eval(""" + eval( + """ var r = ..5 assert( r::class == Range) assert( r.start == null) @@ -1155,17 +1156,20 @@ class ScriptTest { assert( (-2..3) in r) assert( (-2..12) !in r) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testOpenEndRanges() = runTest { - eval(""" + eval( + """ var r = 5.. assert( r::class == Range) assert( r.end == null) assert( r.start == 5) - """.trimIndent()) + """.trimIndent() + ) } @Test @@ -1226,7 +1230,7 @@ class ScriptTest { val list = (1..1024).toList() assert(list.size == 1024) assert(list[0] == 1) - assert(list[-1] == 1024) + assert(list.last == 1024) } """.trimIndent() ) @@ -2149,7 +2153,8 @@ class ScriptTest { @Test fun testNull1() = runTest { - eval(""" + eval( + """ var s = null assertThrows { s.length } assertThrows { s.size() } @@ -2164,25 +2169,41 @@ class ScriptTest { s = "xx" assert(s.lower().size == 2) assert(s.length == 2) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testSprintf() = runTest { - eval(""" + 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()) + """.trimIndent() + ) } @Test fun testSubstringRangeFailure() = runTest { - eval(""" + eval( + """ assertEquals("pult", "catapult"[4..]) assertEquals("cat", "catapult"[..2]) """.trimIndent() ) } + @Test + fun passingOpenEndedRangeAsParam() = runTest { + eval( + """ + fun test(r) { + assert( r is Range ) + } + test( 1.. ) + """.trimIndent() + ) + } + } \ No newline at end of file