From ad1dbe9a84fa33d9d6e59ae8aa5e529c38b9b395 Mon Sep 17 00:00:00 2001 From: odino Date: Sun, 25 Aug 2019 21:08:31 +0400 Subject: [PATCH] Negative indexes, closes #274 This PR adds support for negative indexes in arrays / strings. ``` bash [1,2,3][-1] #3 ``` There is one change that might alter existing ABS scripts, and that's non-existing indexes, for strings, returning an empty string rather than `null`. I believe it's an ok thing to break as both will evaluate to `false` when casted to boolean, and to check whether an index exists one can simply: * check the length of the string (`s.len()`) * check the boolean value of the index (`!!s[idx]`) both these examples do not break with these changes. I instead admit that it would be weird to see code such as: ``` bash if s[idx] == null { ... } ``` rather than ``` if !s[idx] { ... } ``` So I guess this is a change that can go through, as it really shouldn't impact much of the userbase. --- docs/types/array.md | 19 ++++++++++++------ docs/types/string.md | 12 +++++++++-- evaluator/evaluator.go | 40 ++++++++++++++++++++++++++++++++----- evaluator/evaluator_test.go | 40 +++++++++++++++++++++++++++++-------- 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/docs/types/array.md b/docs/types/array.md index 88cb9e89..c5aef90d 100644 --- a/docs/types/array.md +++ b/docs/types/array.md @@ -22,14 +22,21 @@ notation: array[3] ``` -Accessing an index that does not exist returns null. +Accessing an index that does not exist returns `null`. + +You can also access the Nth last element of an array by +using a negative index: + +``` bash +["a", "b", "c", "d"][-2] # "c" +``` You can also access a range of indexes with the `[start:end]` notation: ``` bash array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -array[0:2] // [0, 1, 2] +array[0:2] # [0, 1, 2] ``` where `start` is the starting position in the array, and `end` is @@ -38,14 +45,14 @@ and if `end` is omitted it is assumed to be the last index in the array: ``` bash -array[:2] // [0, 1, 2] -array[7:] // [7, 8, 9] +array[:2] # [0, 1, 2] +array[7:] # [7, 8, 9] ``` If `end` is negative, it will be converted to `length of array - end`: ``` bash -array[:-3] // [0, 1, 2, 3, 4, 5, 6] +array[:-3] # [0, 1, 2, 3, 4, 5, 6] ``` To concatenate arrays, "sum" them: @@ -113,7 +120,7 @@ a # [1, 2, 3, 4, 99, 55, 66] An array is defined as "homogeneous" when all its elements are of a single type: -``` +``` bash [1, 2, 3] # homogeneous [null, 0, "", {}] # heterogeneous ``` diff --git a/docs/types/string.md b/docs/types/string.md index 91de024a..ca1adc43 100644 --- a/docs/types/string.md +++ b/docs/types/string.md @@ -32,7 +32,14 @@ with the index notation: "hello world"[1] # e ``` -Accessing an index that does not exist returns null. +Accessing an index that does not exist returns an empty string. + +You can access the Nth last character of the string using a +negative index: + +``` bash +"string"[-2] # "n" +``` You can also access a range of the string with the `[start:end]` notation: @@ -42,7 +49,8 @@ You can also access a range of the string with the `[start:end]` notation: where `start` is the starting position in the array, and `end` is the ending one. If `start` is not specified, it is assumed to be 0, -and if `end` is omitted it is assumed to be the character in the string: +and if `end` is omitted it is assumed to be the last character in the +string: ``` bash "string"[0:3] // "str" diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 9bcac135..c6f8bb0d 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -1096,7 +1096,7 @@ func evalStringIndexExpression(tok token.Token, array, index object.Object, end max := len(stringObject.Value) - 1 if isRange { - max += 1 + max++ // A range's minimum value is 0 if idx < 0 { idx = 0 @@ -1127,8 +1127,23 @@ func evalStringIndexExpression(tok token.Token, array, index object.Object, end return &object.String{Token: tok, Value: string(stringObject.Value[idx:max])} } - if idx < 0 || idx > max { - return NULL + // Out of bounds? Return an empty string + if idx > max { + return &object.String{Token: tok, Value: ""} + } + + if idx < 0 { + length := max + 1 + + // Negative out of bounds? Return an empty string + if math.Abs(float64(idx)) > float64(length) { + return &object.String{Token: tok, Value: ""} + } + + // Our index was negative, so the actual index is length of the string + the index + // eg 3 + (-2) = 1 + // "123"[-2] = "2" + idx = length + idx } return &object.String{Token: tok, Value: string(stringObject.Value[idx])} @@ -1140,7 +1155,7 @@ func evalArrayIndexExpression(tok token.Token, array, index object.Object, end o max := len(arrayObject.Elements) - 1 if isRange { - max += 1 + max++ // A range's minimum value is 0 if idx < 0 { idx = 0 @@ -1171,10 +1186,25 @@ func evalArrayIndexExpression(tok token.Token, array, index object.Object, end o return &object.Array{Token: tok, Elements: arrayObject.Elements[idx:max]} } - if idx < 0 || idx > max { + // Out of bounds? Return a null element + if idx > max { return NULL } + if idx < 0 { + length := max + 1 + + // Negative out of bounds? Return a null element + if math.Abs(float64(idx)) > float64(length) { + return NULL + } + + // Our index was negative, so the actual index is length of the string + the index + // eg 3 + (-2) = 1 + // [1,2,3][-2] = 2 + idx = length + idx + } + return arrayObject.Elements[idx] } diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index b9c5e344..e2adb047 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -1377,9 +1377,25 @@ func TestArrayIndexExpressions(t *testing.T) { nil, }, { - "[1, 2, 3][-1]", + "[1, 2, 3][-2]", + 2, + }, + { + "[1, 2, 3][-10]", nil, }, + { + "[1, 2, 3][-3]", + 1, + }, + { + "[1, 2, 3][-4]", + nil, + }, + { + "[1, 2, 3][-0]", + 1, + }, { "a = [1, 2, 3, 4, 5, 6, 7, 8, 9][1:-300]; a[0]", nil, @@ -1512,7 +1528,7 @@ func TestStringIndexExpressions(t *testing.T) { }{ { `"123"[10]`, - nil, + "", }, { `"123"[1]`, @@ -1534,6 +1550,18 @@ func TestStringIndexExpressions(t *testing.T) { `"123"[:-1]`, "12", }, + { + `"123"[-2]`, + "2", + }, + { + `"123"[-1]`, + "3", + }, + { + `"123"[-10]`, + "", + }, { `"123"[2:-10]`, "", @@ -1558,10 +1586,6 @@ func TestStringIndexExpressions(t *testing.T) { `"123"[-10:{}]`, `index ranges can only be numerical: got "{}" (type HASH)`, }, - { - `"123"[-2]`, - "", - }, { `"123"[3]`, "", @@ -1575,12 +1599,12 @@ func TestStringIndexExpressions(t *testing.T) { for _, tt := range tests { evaluated := testEval(tt.input) switch result := evaluated.(type) { - case *object.Null: - testNullObject(t, evaluated) case *object.String: testStringObject(t, evaluated, tt.expected.(string)) case *object.Error: logErrorWithPosition(t, result.Message, tt.expected) + default: + t.Errorf("object is not the right result. got=%s ('%+v' expected)", result.Inspect(), tt.expected) } } }