Skip to content

Commit

Permalink
Merge 81c1d5b into e86b550
Browse files Browse the repository at this point in the history
  • Loading branch information
odino committed Feb 15, 2019
2 parents e86b550 + 81c1d5b commit 8a9f63b
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 8 deletions.
30 changes: 29 additions & 1 deletion docs/types/number.md
Expand Up @@ -42,12 +42,40 @@ Identity:

### int()

Rounds down the number to the closest integer:
Rounds **down** the number to the closest integer:

``` bash
10.3.int() # 10
```

### round(precision?)

Rounds the number with the given precision.
The precision argument is optional, and set to `0`
by default:

``` bash
10.3.round() # 10
10.6.round() # 11
10.333.round(1) # 10.3
```

### ceil()

Rounds the number up to the closest integer:

``` bash
10.3.ceil() # 11
```

### floor()

Rounds the number down to the closest integer:

``` bash
10.9.floor() # 10
```

### str()

Returns a string containing the number:
Expand Down
47 changes: 46 additions & 1 deletion docs/types/string.md
Expand Up @@ -83,7 +83,9 @@ a\\nb
c
```

### Working with special characters in string functions

Special characters also work with `split()` and `join()` and other string functions as well.

1) Double quoted expanded special characters:
Expand Down Expand Up @@ -160,13 +162,56 @@ Use this function when `"...".number()` might return an error.

### int()

Converts a string to integer, if possible:
Converts a string to a number, and then rounds it
**down** to the closest integer.
The string must represent a number.

``` bash
"99.5".int() # 99
"a".int() # ERROR: int(...) can only be called on strings which represent numbers, 'a' given
```

### round(precision?)

Converts a string to a number, and then rounds
the number with the given precision.

The precision argument is optional, and set to `0`
by default.

The string must represent a number.

``` bash
"10.3".round() # 10
"10.6".round() # 11
"10.333".round(1) # 10.3
"a".round() # ERROR: round(...) can only be called on strings which represent numbers, 'a' given
```

### ceil()

Converts a string to a number, and then rounds the
number up to the closest integer.

The string must represent a number.

``` bash
"10.3".ceil() # 11
"a".ceil() # ERROR: ceil(...) can only be called on strings which represent numbers, 'a' given
```

### floor()

Converts a string to a number, and then rounds the
number down to the closest integer.

The string must represent a number.

``` bash
"10.9".floor() # 10
"a".floor() # ERROR: floor(...) can only be called on strings which represent numbers, 'a' given
```

### split(separator)

Splits a string by separator:
Expand Down
18 changes: 18 additions & 0 deletions evaluator/evaluator_test.go
Expand Up @@ -782,6 +782,24 @@ c")`, []string{"a", "b", "c"}},
{`" A great movie ".trim()`, "A great movie"},
{`" A great movie ".trim_by(" A")`, "great movie"},
{`sleep(1000)`, nil},
{`1.round()`, 1},
{`1.round(2)`, 1.00},
{`1.23.round(1)`, 1.2},
{`1.66.round(1)`, 1.7},
{`"1.23".round(1)`, 1.2},
{`"1.66".round(1)`, 1.7},
{`1.floor()`, 1},
{`1.floor()`, 1},
{`1.23.floor()`, 1},
{`1.66.floor()`, 1},
{`"1.23".floor()`, 1},
{`"1.66".floor()`, 1},
{`1.ceil()`, 1},
{`1.ceil()`, 1},
{`1.23.ceil()`, 2},
{`1.66.ceil()`, 2},
{`"1.23".ceil()`, 2},
{`"1.66".ceil()`, 2},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
Expand Down
93 changes: 87 additions & 6 deletions evaluator/functions.go
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"crypto/rand"
"fmt"
"math"
"math/big"
"os"
"os/user"
Expand Down Expand Up @@ -70,10 +71,29 @@ func getFns() map[string]*object.Builtin {
Fn: echoFn,
},
// int(string:"123")
// int(number:"123")
"int": &object.Builtin{
Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
Fn: intFn,
},
// round(string:"123.1")
// round(number:"123.1", 2)
"round": &object.Builtin{
Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
Fn: roundFn,
},
// floor(string:"123.1")
// floor(number:123.1)
"floor": &object.Builtin{
Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
Fn: floorFn,
},
// ceil(string:"123.1")
// ceil(number:123.1)
"ceil": &object.Builtin{
Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
Fn: ceilFn,
},
// number(string:"1.23456")
"number": &object.Builtin{
Types: []string{object.STRING_OBJ, object.NUMBER_OBJ},
Expand Down Expand Up @@ -470,26 +490,87 @@ func echoFn(tok token.Token, args ...object.Object) object.Object {
}

// int(string:"123")
// int(number:123)
func intFn(tok token.Token, args ...object.Object) object.Object {
err := validateArgs(tok, "int", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
if err != nil {
return err
}

switch arg := args[0].(type) {
return applyMathFunction(args[0], func(n float64) float64 {
return float64(int64(n))
}, "int")
}

// round(string:"123.1")
// round(number:123.1)
func roundFn(tok token.Token, args ...object.Object) object.Object {
// Validate first argument
err := validateArgs(tok, "round", args[:1], 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
if err != nil {
return err
}

decimal := float64(1)

// If we have a second argument, let's validate it
if len(args) > 1 {
err := validateArgs(tok, "round", args[1:], 1, [][]string{{object.NUMBER_OBJ}})
if err != nil {
return err
}

decimal = float64(math.Pow(10, args[1].(*object.Number).Value))
}

return applyMathFunction(args[0], func(n float64) float64 {
return math.Round(n*decimal) / decimal
}, "round")
}

// floor(string:"123.1")
// floor(number:123.1)
func floorFn(tok token.Token, args ...object.Object) object.Object {
err := validateArgs(tok, "floor", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
if err != nil {
return err
}

return applyMathFunction(args[0], math.Floor, "floor")
}

// ceil(string:"123.1")
// ceil(number:123.1)
func ceilFn(tok token.Token, args ...object.Object) object.Object {
err := validateArgs(tok, "ceil", args, 1, [][]string{{object.NUMBER_OBJ, object.STRING_OBJ}})
if err != nil {
return err
}

return applyMathFunction(args[0], math.Ceil, "ceil")
}

// Base function to do math operations. This is here
// so that we abstract away some of the common logic
// between all math functions, for example:
// - allowing to be called on strings as well ("1.23".ceil())
// - handling errors
func applyMathFunction(arg object.Object, fn func(float64) float64, fname string) object.Object {
switch arg := arg.(type) {
case *object.Number:
return &object.Number{Token: tok, Value: float64(int64(arg.Value))}
return &object.Number{Token: tok, Value: float64(fn(arg.Value))}
case *object.String:
i, err := strconv.ParseFloat(arg.Value, 64)

if err != nil {
return newError(tok, "int(...) can only be called on strings which represent numbers, '%s' given", arg.Value)
return newError(tok, "%s(...) can only be called on strings which represent numbers, '%s' given", fname, arg.Value)
}

return &object.Number{Token: tok, Value: float64(int64(i))}
return &object.Number{Token: tok, Value: float64(fn(i))}
default:
// we will never reach here
return newError(tok, "argument to `int` not supported, got %s", args[0].Type())
// we should never reach here since our callers should validate
// the type of the arguments
return newError(tok, "argument to `%s` not supported, got %s", fname, arg.Type())
}
}

Expand Down

0 comments on commit 8a9f63b

Please sign in to comment.