Skip to content

Commit

Permalink
feat: objects (#13)
Browse files Browse the repository at this point in the history
* add support for objects properties

* resolved some TODOs

* added ObjectGetMethod in prep for object function support
  • Loading branch information
seborama authored May 11, 2024
1 parent b387e61 commit 2ba80ba
Show file tree
Hide file tree
Showing 15 changed files with 726 additions and 152 deletions.
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ linters-settings:
gocognit:
# Minimal code complexity to report
# Default: 30 (but we recommend 10-20)
min-complexity: 15
min-complexity: 20

gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks'.
Expand Down Expand Up @@ -300,7 +300,7 @@ linters-settings:
gocyclo:
# Minimal code complexity to report.
# Default: 30 (but we recommend 10-20)
min-complexity: 15
min-complexity: 20

godot:
# Comments to be checked: `declarations`, `toplevel`, or `all`.
Expand Down
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Escapes are supported:

## Bools

In addition to boolean expressions, sepcial contants `True` and `False` may be used.
In addition to boolean expressions, special contants `True` and `False` may be used.

Do not double-quote them, or they will become plain strings!

Expand All @@ -148,7 +148,7 @@ This is container `Value`. It can contain zero or any number of `Value`'s. Curre
* Go classifies bit shift operators with the higher `*`.
* `&&` is synonymous of `And`.
* `||` is synonymous of `Or`.
* Worded operators such as `And` and `Or` are **case-sensitive** and must be followed by a blank character. `True Or (False)` is a Bool expression with the `Or` operator but `True Or(False)` is an invalid expression attempting to call a user-defined function called `Or()`.
* Worded operators such as `And` and `Or` are **case-sensitive** and must be followed by a blank character. `True Or (False)` is a Bool expression with the `Or` operator but `True Or(False)` is an expression attempting to call a user-defined function called `Or()`.
* Types: String, Number, Bool, MultiValue
* Associativity with parentheses: `(` and `)`
* Functions:
Expand All @@ -158,6 +158,8 @@ This is container `Value`. It can contain zero or any number of `Value`'s. Curre

## Functions

(See also Objects)

A function is defined as a Go type: `type FunctionalValue func(...Value) Value`

Function names are case-insensitive.
Expand All @@ -172,12 +174,52 @@ This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multi

## Variables

(See also Objects)

Variable names are case-sensitive.

Values are passed as a `map[string]Value` using `WithVariables` when calling `Eval` from `Tree`.

This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multiple times with different variable values.

## Objects

Objects are Go `struct`'s which **properties** act as **gal variables** and **methods** as **gal functions**.

Object definitions are passed as a `map[string]Object` using `WithObjects` when calling `Eval` from `Tree`.

This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multiple times with different instances of an object.

Object methods generally follow the same rules as **gal Functions**:
- methods can optionally accept one or more arguments
- methods must return a single value (which can be a `MultiValue` to emulate multiple return values)
- arguments and return value are preferred to be of `gal.Value` type.
However, **gal** will attempt to convert Go types to `gal.Value` types on best endeavour:
- A method signature of `MyMethod(arg1 int64) bool` will translate the supplied `gal.Value`'s and attempt to map them to `int64` and `bool` using `gal.Numberer` and `gal.Booler`.
- Type conversion may lead to a panic when the type cannot be interpreted.

Example:

`type Car struct` has several properties and methods - one of which is `func (c *Car) CurrentSpeed() gal.Value`.

```go
expr := `aCar.MaxSpeed - aCar.CurrentSpeed()`
parsedExpr := gal.Parse(expr)

got := parsedExpr.Eval(
gal.WithObjects(map[string]gal.Object{
"aCar": Car{
Make: "Lotus Esprit",
Mileage: gal.NewNumberFromInt(2000),
Speed: 100,
MaxSpeed: 250,
},
}),
)
// result: 150 == 250 - 100

```

## High level design

Expressions are parsed in two stages:
Expand Down
13 changes: 4 additions & 9 deletions function.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ var builtInFunctions = map[string]FunctionalValue{
// It returns `nil` when no built-in function exists by the specified name.
// This signals the Evaluator to attempt to find a user defined function.
func BuiltInFunction(name string) FunctionalValue {
if builtInFunctions == nil {
return nil
}

// note: for now function names are arbitrarily case-insensitive
lowerName := strings.ToLower(name)

Expand All @@ -82,15 +86,6 @@ func BuiltInFunction(name string) FunctionalValue {
return nil
}

// UserDefinedFunction is a helper function that returns the definition of the
// provided function name from the supplied userFunctions.
func UserDefinedFunction(name string, userFunctions Functions) FunctionalValue {
// note: for now function names are arbitrarily case-insensitive
lowerName := strings.ToLower(name)

return userFunctions.Function(lowerName)
}

// Pi returns the Value of math.Pi.
func Pi(args ...Value) Value {
if len(args) != 0 {
Expand Down
24 changes: 12 additions & 12 deletions function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ func TestPiLong(t *testing.T) {
}

func TestFactorial(t *testing.T) {
val := gal.Factorial(gal.NewNumber(0))
assert.Equal(t, gal.NewNumber(1).String(), val.String())
val := gal.Factorial(gal.NewNumberFromInt(0))
assert.Equal(t, gal.NewNumberFromInt(1).String(), val.String())

val = gal.Factorial(gal.NewNumber(1))
assert.Equal(t, gal.NewNumber(1).String(), val.String())
val = gal.Factorial(gal.NewNumberFromInt(1))
assert.Equal(t, gal.NewNumberFromInt(1).String(), val.String())

val = gal.Factorial(gal.NewNumber(10))
assert.Equal(t, gal.NewNumber(3_628_800).String(), val.String())
val = gal.Factorial(gal.NewNumberFromInt(10))
assert.Equal(t, gal.NewNumberFromInt(3_628_800).String(), val.String())

val = gal.Factorial(gal.NewNumber(-10))
val = gal.Factorial(gal.NewNumberFromInt(-10))
assert.Equal(t, "undefined: Factorial: requires a positive integer, cannot accept -10", val.String())
}

Expand Down Expand Up @@ -60,21 +60,21 @@ func TestFloor(t *testing.T) {
}

func TestLn(t *testing.T) {
val := gal.Ln(gal.NewNumber(123456), gal.NewNumber(5))
val := gal.Ln(gal.NewNumberFromInt(123456), gal.NewNumberFromInt(5))
assert.Equal(t, "11.72364", val.String())

val = gal.Ln(gal.NewNumber(-123456), gal.NewNumber(5))
val = gal.Ln(gal.NewNumberFromInt(-123456), gal.NewNumberFromInt(5))
assert.Equal(t, "undefined: Ln:cannot calculate natural logarithm for negative decimals", val.String())
}

func TestLog(t *testing.T) {
val := gal.Log(gal.NewNumber(123456), gal.NewNumber(5))
val := gal.Log(gal.NewNumberFromInt(123456), gal.NewNumberFromInt(5))
assert.Equal(t, "5.09151", val.String())

val = gal.Log(gal.NewNumber(-123456), gal.NewNumber(5))
val = gal.Log(gal.NewNumberFromInt(-123456), gal.NewNumberFromInt(5))
assert.Equal(t, "undefined: Log:cannot calculate natural logarithm for negative decimals", val.String())

val = gal.Log(gal.NewNumber(10_000_000), gal.NewNumber(0))
val = gal.Log(gal.NewNumberFromInt(10_000_000), gal.NewNumberFromInt(0))
assert.Equal(t, "7", val.String())
}

Expand Down
Loading

0 comments on commit 2ba80ba

Please sign in to comment.