Skip to content

Commit

Permalink
Negative indexes, closes #274
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odino committed Aug 25, 2019
1 parent 5fb8d57 commit ad1dbe9
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 21 deletions.
19 changes: 13 additions & 6 deletions docs/types/array.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
```
Expand Down
12 changes: 10 additions & 2 deletions docs/types/string.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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"
Expand Down
40 changes: 35 additions & 5 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])}
Expand All @@ -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
Expand Down Expand Up @@ -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]
}

Expand Down
40 changes: 32 additions & 8 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1512,7 +1528,7 @@ func TestStringIndexExpressions(t *testing.T) {
}{
{
`"123"[10]`,
nil,
"",
},
{
`"123"[1]`,
Expand All @@ -1534,6 +1550,18 @@ func TestStringIndexExpressions(t *testing.T) {
`"123"[:-1]`,
"12",
},
{
`"123"[-2]`,
"2",
},
{
`"123"[-1]`,
"3",
},
{
`"123"[-10]`,
"",
},
{
`"123"[2:-10]`,
"",
Expand All @@ -1558,10 +1586,6 @@ func TestStringIndexExpressions(t *testing.T) {
`"123"[-10:{}]`,
`index ranges can only be numerical: got "{}" (type HASH)`,
},
{
`"123"[-2]`,
"",
},
{
`"123"[3]`,
"",
Expand All @@ -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)
}
}
}
Expand Down

0 comments on commit ad1dbe9

Please sign in to comment.