Browse Source

Added new way to format expressions

master
Remi Reuvekamp 4 years ago
parent
commit
e5841bbfbb
7 changed files with 535 additions and 438 deletions
  1. +44
    -20
      background.go
  2. +23
    -100
      config.go
  3. +0
    -186
      date.go
  4. +0
    -132
      date_test.go
  5. +141
    -0
      expression/expression.go
  6. +131
    -0
      expression/expression_test.go
  7. +196
    -0
      expression/structures.go

+ 44
- 20
background.go View File

@@ -1,35 +1,60 @@
package main

type backgroundError struct {
fileName string
import (
"fmt"
"time"
)

var now = time.Now()

// bgError is used by determineBackground
type bgError struct {
msg string
bg background
errString string
}

// Error messages
const (
bgErrorMsgInvalidExpr = "Invalid expression for file %s. Expressions should return a boolean.\nDate expression: %s. This may be of help: %s."
bgErrorOccured = "An error occured while processing the expression for file %s. Date expression: %s. This may be of help: %s."
)

// determineBackground gives a list of backgrounds for today.
// All errors in the []error returned are of type bgError.
func determineBackground(cfg config) ([]background, []error) {

year, week := now.ISOWeek()

params := map[string]int{
"dayOfWeek": int(now.Weekday()),
"dayOfMonth": now.Day(),
"dayOfYear": now.YearDay(),
"week": week,
"month": int(now.Month()),
"year": year,
}

var errs []error
var backgrounds []background

var err error

for _, bg := range cfg.Backgrounds {
var match bool
for _, d := range bg.Dates {
isNow, err := dateIsNow(d)
result := true

if !bg.Always {
result, err = bg.expr.Eval(params)

fmt.Printf("%#v\n", bg.expr.Expression())

if err != nil {
switch err {
case errDateIsEmpty:
errs = append(errs, backgroundError{bg.FileName})
default:
errs = append(errs, err)
}
errs = append(errs, err)
continue
}

if isNow {
match = true
break
}
}
if match || len(bg.Dates) == 0 {

if result {
backgrounds = append(backgrounds, bg)
}
}
@@ -37,7 +62,6 @@ func determineBackground(cfg config) ([]background, []error) {
return backgrounds, errs
}

func (err backgroundError) Error() string {
return "One or more date values for " + err.fileName + " is empty. Please check " +
"the configuration file for spelling mistakes and empty date values."
func (err bgError) Error() string {
return fmt.Sprintf(err.msg, err.bg.FileName, err.bg.DateExpr, err.errString)
}

+ 23
- 100
config.go View File

@@ -4,10 +4,11 @@ import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"strings"

"remi.im/misc/background/expression"

"gopkg.in/yaml.v2"
)

@@ -25,47 +26,20 @@ var errCannotReadFile = errors.New("Cannot read the file, does it exist?")

// config is the format of the configuration file.
type config struct {
FilePathCurBg string `yaml:"filePathCurrentBackground",json:"filePathCurrentBackground"`
DirPathBgs string `yaml:"directoryPathAllBackgrounds",json:"directoryPathAllBackgrounds"`
Backgrounds []background `yaml:"backgrounds",json:"backgrounds"`
FilePathCurBg string `json:"filePathCurrentBackground"`
DirPathBgs string `json:"directoryPathAllBackgrounds"`
Backgrounds []background `json:"backgrounds"`
}

// background pairs a background filename with a []date the background should be active.
type background struct {
FileName string `yaml:"filename",json:"filename"`
Note string `yaml:"note,omitempty",json:"note,omitempty"`
Dates []date `yaml:"dates,omitempty",json:"dates,omitempty"`
}

// date contains options for matching dates.
type date struct {
WeekDays []dateWeekDay `yaml:"weekDays,omitempty",json:"weekDays,omitempty"`
MonthDays []dateMonthDay `yaml:"days,omitempty",json:"days,omitempty"`
YearDays []dateYearDay `yaml:"yearDays,omitempty",json:"yearDays,omitempty"`
Weeks []dateWeek `yaml:"weeks,omitempty",json:"weeks,omitempty"`
Months []dateMonth `yaml:"months,omitempty",json:"months,omitempty"`
Years []dateYear `yaml:"years,omitempty",json:"years,omitempty"`
}

// dateType can specify an integer by an exact number, a range and/or an interval.
type dateType struct {
// A maximum of one of each row/type (e.g. dayMin+dayMax or dayInterval) is used.
Exact int `yaml:"exact,omitempty",json:"exact,omitempty"` // 5 (only the fifth {type})

// Range. 5 - 10 (5, 6, 7, 8, 9, 10 {type}t)
Min int `yaml:"min,omitempty",json:"min,omitempty"`
Max int `yaml:"max,omitempty",json:"max,omitempty"`

Interval int `yaml:"interval,omitempty",json:"interval,omitempty"` // Every x of {type}. 3 (3, 6, 9, etc)
expr expression.Expression
DateExpr *json.RawMessage `json:"date,omitempty"`
Always bool `json:"always,omitempty"`
}

type dateWeekDay dateType
type dateMonthDay dateType
type dateYearDay dateType
type dateWeek dateType
type dateMonth dateType
type dateYear dateType

// loadConfig loads the configuration of type config with the filename in
// cfgFilePath. cfgFormat is used for determining the format which the configuration
// written, which could be either yaml or json.
@@ -78,17 +52,25 @@ func loadConfig() (config, error) {
}

switch cfgFormat {
case cfgTypeYAML:
// Too bad a fairly large library has to be imported.
err = yaml.Unmarshal(data, &cfg)
case cfgTypeJSON:
err = json.Unmarshal(data, &cfg)
}

for i, bg := range cfg.Backgrounds {
if bg.Always {
continue
}

if bg.DateExpr == nil {
return config{}, errors.New("date expression is empty but 'always' is not set")
}

err := json.Unmarshal(*bg.DateExpr, &cfg.Backgrounds[i].expr)
if err != nil {
err = fmt.Errorf("json: %v", err)
return config{}, err
}
// (Yaml library also prefixes the error with 'yaml: ', it might make the
// user realise he/she set an incorrect config format.)
}

return cfg, err
}

@@ -123,66 +105,7 @@ func initializeConfig() config {
background{
FileName: "special1.jpg",
Note: "Matches if day in the month is (14-20 and even) and month is 5-8 and year is 2015. Or if it is Wednesday. Or the week number is 40. Or the day of year is <= 10 or >= 360.",
Dates: []date{
date{
// Day of month: 14, 16, 18, 20
// In months 5, 6, 7, 8
// In year 2015
MonthDays: []dateMonthDay{
dateMonthDay{
Min: 14,
Max: 20,
},
// AND
dateMonthDay{
Interval: 2,
},
},
// AND
Months: []dateMonth{
dateMonth{
Min: 5,
Max: 8,
},
},
// AND
Years: []dateYear{
dateYear{
Exact: 2015,
},
},
},
// OR
date{
// Every wednesday
WeekDays: []dateWeekDay{
dateWeekDay{
Exact: 3,
},
},
},
// OR
date{
// Week number 40
Weeks: []dateWeek{
dateWeek{
Exact: 40,
},
},
},
// OR
date{
YearDays: []dateYearDay{
dateYearDay{
Max: 10,
},
// AND
dateYearDay{
Min: 360,
},
},
},
},
//Date: "(monthDay >= 14 && monthDay <= 20 && monthDay % 2 && month >= 5 && month <= 8 && year == 2015) || weekDay = 3 || week == 40 || (yearDay <= 10 || yearDay >= 360)",
},
background{
FileName: "default.jpg",


+ 0
- 186
date.go View File

@@ -1,186 +0,0 @@
package main

import (
"errors"
"fmt"
"time"
)

var errDateIsEmpty = errors.New("date is empty")

var now = time.Now()
var loc = time.FixedZone("Europe/London", 0)

// dateIsNow checks if today is in the given date.
func dateIsNow(d date) (bool, error) {
if dateIsEmpty(d) {
return false, errDateIsEmpty
}

// Week days
weekDays := make([]dateType, len(d.WeekDays))
for i, dt := range d.WeekDays {
weekDays[i] = dateType(dt)
}

match, err := dateTypeSliceIsNow(weekDays, "weekDay")
if !match || err != nil {
return false, err
}

// Month days
monthDays := make([]dateType, len(d.MonthDays))
for i, dt := range d.MonthDays {
monthDays[i] = dateType(dt)
}

match, err = dateTypeSliceIsNow(monthDays, "monthDay")
if !match || err != nil {
return false, err
}

// Year days
yearDays := make([]dateType, len(d.YearDays))
for i, dt := range d.YearDays {
yearDays[i] = dateType(dt)
}

match, err = dateTypeSliceIsNow(yearDays, "yearDay")
if !match || err != nil {
return false, err
}

// Weeks
weeks := make([]dateType, len(d.Weeks))
for i, dt := range d.Weeks {
weeks[i] = dateType(dt)
}

match, err = dateTypeSliceIsNow(weeks, "week")
if !match || err != nil {
return false, err
}

// Months
months := make([]dateType, len(d.Months))
for i, dt := range d.Months {
months[i] = dateType(dt)
}

match, err = dateTypeSliceIsNow(months, "month")
if !match || err != nil {
return false, err
}

// Years
years := make([]dateType, len(d.Years))
for i, dt := range d.Years {
years[i] = dateType(dt)
}

match, err = dateTypeSliceIsNow(years, "year")
if !match || err != nil {
return false, err
}

return true, nil
}

// dateTypeSliceIsNow checks if today is in the given []dateType.
// Only the specified type in checkFor is checked. See dateTypeIsNow.
func dateTypeSliceIsNow(dts []dateType, checkFor string) (bool, error) {
if len(dts) == 0 {
return true, nil
}

for _, dt := range dts {
matches, err := dateTypeIsNow(dt, checkFor)
if err != nil {
return false, err
}
if matches {
return true, nil
}
}

return false, nil
}

// dateIsEmpty returns true if the date doesn't contain any date specification values.
func dateIsEmpty(d date) (_ bool) {
if len(d.WeekDays) > 0 {
return
}

if len(d.MonthDays) > 0 {
return
}

if len(d.YearDays) > 0 {
return
}

if len(d.Weeks) > 0 {
return
}

if len(d.Months) > 0 {
return
}

if len(d.Years) > 0 {
return
}

return true
}

// dateTypeIsNow checks if today is in the given dateType.
// Only the specified type in checkFor is checked. checkFor can be:
// monthDay, weekDay, weeks, month, year
func dateTypeIsNow(dt dateType, checkFor string) (bool, error) {
var val int
switch checkFor {
case "weekDay":
val = int(now.Weekday())
case "monthDay":
val = now.Day()
case "yearDay":
val = now.YearDay()
case "week":
_, val = now.ISOWeek()
case "month":
val = int(now.Month())
case "year":
val = now.Year()
default:
return false, fmt.Errorf("Invalid checkFor string; %s", checkFor)
}

if val == dt.Exact {
return true, nil
}

// Range

// Both min and max specified
if dt.Min > 0 && dt.Max > 0 && val >= dt.Min && val <= dt.Max {
return true, nil
}

// Only min
if dt.Min > 0 && dt.Max <= 0 && val >= dt.Min {
return true, nil
}

// Only max
if dt.Max > 0 && dt.Min <= 0 && val <= dt.Max {
return true, nil
}

if dt.Interval > 0 && val%dt.Interval == 0 {
return true, nil
}

return false, nil
}

+ 0
- 132
date_test.go View File

@@ -1,132 +0,0 @@
package main

import (
"testing"
"time"
)

func TestDateIsNow(t *testing.T) {

// Test monthDay, month and year
d := initializeConfig().Backgrounds[0].Dates[0]
testDateIsNow(t, d, time.Date(2015, 6, 14, 0, 0, 0, 0, loc), true)
testDateIsNow(t, d, time.Date(2015, 6, 15, 0, 0, 0, 0, loc), true)
testDateIsNow(t, d, time.Date(2015, 6, 22, 0, 0, 0, 0, loc), true)
testDateIsNow(t, d, time.Date(2015, 8, 14, 0, 0, 0, 0, loc), true)
testDateIsNow(t, d, time.Date(2015, 6, 1, 0, 0, 0, 0, loc), false)
testDateIsNow(t, d, time.Date(2015, 4, 15, 0, 0, 0, 0, loc), false)
testDateIsNow(t, d, time.Date(2016, 6, 14, 0, 0, 0, 0, loc), false) // Year

// Test weekDays, different date.
d = initializeConfig().Backgrounds[0].Dates[1]
testDateIsNow(t, d, dateByDay(time.Now(), 3), true)
testDateIsNow(t, d, dateByDay(time.Now(), 2), false)

// Test weeks, different date thing.
d = initializeConfig().Backgrounds[0].Dates[2]
testDateIsNow(t, d, firstDayOfISOWeek(2019, 40, loc), true)
testDateIsNow(t, d, firstDayOfISOWeek(2019, 39, loc), false)

// Test yearDays. Optional range
d = initializeConfig().Backgrounds[0].Dates[3]
testDateIsNow(t, d, time.Date(2015, 1, 10, 0, 0, 0, 0, loc), true)
testDateIsNow(t, d, time.Date(2015, 1, 12, 0, 0, 0, 0, loc), false)
testDateIsNow(t, d, time.Date(2015, 12, 28, 0, 0, 0, 0, loc), true)
testDateIsNow(t, d, time.Date(2015, 12, 24, 0, 0, 0, 0, loc), false)

}

func testDateIsNow(t *testing.T, d date, tm time.Time, expectedReturn bool) {
now = tm
result, err := dateIsNow(d)
if err != nil {
t.Error(err.Error())
}
if result != expectedReturn {
t.Errorf("Expected %t got %t, time; %v", expectedReturn, result, tm)
}
}

func TestDateTypeIsNow(t *testing.T) {
dt := dateType{
// You don't need to use all of them, but just for testing.
Exact: 5,
Min: 8,
Max: 10,
Interval: 12,
}

// Test exact
testDateTypeIsNow(t, dt, time.Date(2015, 11, 5, 0, 0, 0, 0, loc), true)

// Test range
testDateTypeIsNow(t, dt, time.Date(2015, 11, 8, 0, 0, 0, 0, loc), true)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 9, 0, 0, 0, 0, loc), true)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 10, 0, 0, 0, 0, loc), true)

// Test interval
testDateTypeIsNow(t, dt, time.Date(2015, 11, 12, 0, 0, 0, 0, loc), true)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 24, 0, 0, 0, 0, loc), true)

// Things that should give false
testDateTypeIsNow(t, dt, time.Date(2015, 11, 4, 0, 0, 0, 0, loc), false)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 1, 0, 0, 0, 0, loc), false)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 25, 0, 0, 0, 0, loc), false)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 23, 0, 0, 0, 0, loc), false)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 13, 0, 0, 0, 0, loc), false)
testDateTypeIsNow(t, dt, time.Date(2015, 11, 30, 0, 0, 0, 0, loc), false)
}

func testDateTypeIsNow(t *testing.T, dt dateType, tm time.Time, expectedReturn bool) {
now = tm
result, err := dateTypeIsNow(dt, "monthDay")
if err != nil {
t.Error(err.Error())
}
if result != expectedReturn {
t.Errorf("Expected %t got %t, time; %v", expectedReturn, result, tm)
}
}

// firstDayOfISOWeek was 'stolen', but it's 'just a test' :$ .
// http://www.xferion.com/golang-reverse-isoweek-get-the-date-of-the-first-day-of-iso-week/
func firstDayOfISOWeek(year int, week int, timezone *time.Location) time.Time {
date := time.Date(year, 0, 0, 0, 0, 0, 0, timezone)
isoYear, isoWeek := date.ISOWeek()

// Iterate back to Monday
for date.Weekday() != time.Monday {
date = date.AddDate(0, 0, -1)
isoYear, isoWeek = date.ISOWeek()
}

// Iterate forward to the first day of the first week
for isoYear < year {
date = date.AddDate(0, 0, 7)
isoYear, isoWeek = date.ISOWeek()
}

// Iterate forward to the first day of the given week
for isoWeek < week {
date = date.AddDate(0, 0, 7)
isoYear, isoWeek = date.ISOWeek()
}

return date
}

func TestDateByDate(t *testing.T) {
dateByDay(time.Now(), 0)
}

// dateByday goes back in time from the given time, so that it reached the given
// weekDay. That time is returned.
func dateByDay(d time.Time, weekDay time.Weekday) time.Time {
if d.Weekday() > weekDay {
d = d.AddDate(0, 0, -1*int(d.Weekday()-weekDay))
} else if d.Weekday() < weekDay {
d = d.AddDate(0, 0, -1*(7-int(weekDay-d.Weekday())))
}

return d
}

+ 141
- 0
expression/expression.go View File

@@ -0,0 +1,141 @@
// Package expression can parse and evaluate boolean expressions from JSON.
package expression

import (
"encoding/json"
"fmt"
)

// UnmarshalJSON recursively unmarshals the given input JSON and set Expression.Expression with the unmarshalled values.
func (expr *Expression) UnmarshalJSON(input []byte) error {
var rawExpr rawExpression

err := json.Unmarshal(input, &rawExpr)
if err != nil {
return err
}

// unmarshalRawExpression may (/will probably) call itself recursively while it goes through the nested expression.

for eType, e := range rawExpr {
expr.expression, err = unmarshalRawExpression(eType, "", e)
}

return err
}

// unmarshalRawExpression creates a node for the given rawExpression.
// It may(/will probably) call itself recursively while it goes through the nested rawExpression.
func unmarshalRawExpression(exprType string, lastBoolOperator string, expr *json.RawMessage) (node, error) {
//exprType := *ptrExprType
switch exprType {
case "and", "or", "&&", "||":
switch exprType {
case "and", "&&":
exprType = "and"
case "or", "||":
exprType = "or"
}
// Type is a boolean operator

var rawOp rawOperator

err := json.Unmarshal(*expr, &rawOp)
if err != nil {
return nil, err
}

// Unmarshal the children of operator (left, right)

if len(rawOp) == 0 {
return nil, fmt.Errorf("no expressions in %q expression", exprType)
}

var childExprs operator
for key, value := range rawOp {
child, err := unmarshalRawExpression(key, exprType, value)
if err != nil {
return nil, err
}

childExprs = append(childExprs, child)
}

// Switch through the operator type and return the correct implementation of node for it.
switch exprType {
case "and", "&&":
return and(childExprs), nil
case "or", "||":
return or(childExprs), nil
default:
return nil, fmt.Errorf("What? %s is not a known operator", exprType)
}
case "equals", "smallerThan", "greaterThan", "smallerThanOrEqual", "greaterThanOrEqual",
"==", "<", ">", "<=", ">=":
// Type is a comparison

if expr == nil {
return nil, fmt.Errorf("expression with type %q does not contain an expression", exprType)
}

var comp comparison

err := json.Unmarshal(*expr, &comp.data)
if err != nil {
return nil, err
}

if len(comp.data) == 0 {
return nil, fmt.Errorf("no expressions in %q expression", exprType)
}

comp.parent = lastBoolOperator

// Switch through the comparison type and return the correct implementation of node for it.
switch exprType {
case "equals", "==":
return equals(comp), nil
case "smallerThan", "<":
return smallerThan(comp), nil
case "greaterThan", ">":
return greaterThan(comp), nil
case "smallerThanOrEqual", "<=":
return smallerThanOrEqual(comp), nil
case "greaterThanOrEqual", ">=":
return greaterThanOrEqual(comp), nil
default:
return nil, fmt.Errorf("What? %s is not a known comparison", exprType)
}
case "not", "!":
exprType = "not"
// Type is 'not'

var ra rawExpression

err := json.Unmarshal(*expr, &ra)
if err != nil {
return nil, err
}

// Unmarshal the child elem.
var n not
for key, value := range ra {
elem, err := unmarshalRawExpression(key, exprType, value)
if err != nil {
return nil, err
}

n = append(n, elem)
}

if err != nil {
return nil, err
}

return n, nil
case "":
return nil, fmt.Errorf("no expression type")
default:
return nil, fmt.Errorf("unknown expression type: %q", exprType)
}
}

+ 131
- 0
expression/expression_test.go View File

@@ -0,0 +1,131 @@
package expression

import (
"encoding/json"
"fmt"
"testing"
)

// (month == 12 OR month <= 10) AND month > 2 AND (not(dayOfMonth < 28) OR dayOfWeek >= 5)
const testingJSON = `{
"and": {
"or": {
"equals": {
"month": 12
},
"smallerThanOrEqual": {
"month": 10
}
},
"greaterThan": {
"month": 2
},
"||": {
"!": {
"<": {
"dayOfMonth": 28
}
},
">=": {
"dayOfWeek": 5
}
}
}
}`

func TestUnmarshalExpression(t *testing.T) {

var expr Expression

err := json.Unmarshal([]byte(testingJSON), &expr)

if err != nil {
t.Errorf("ERROR while parsing the date expressions; %s\n", err)
}

values := map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 6}

result, err := expr.Eval(values)
if err != nil {
t.Error("error from evaluating expression:", err)
}

if !result {
t.Error("expected expression to be evaluated to true, got false")
}
}

var expressionValuesEvaluationMap = map[*map[string]int]bool{
// month
&map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 6}: true,
&map[string]int{"month": 4, "dayOfMonth": 27, "dayOfWeek": 6}: true,
&map[string]int{"month": 5, "dayOfMonth": 27, "dayOfWeek": 6}: true,
&map[string]int{"month": 9, "dayOfMonth": 27, "dayOfWeek": 6}: true,
&map[string]int{"month": 10, "dayOfMonth": 27, "dayOfWeek": 6}: true,
&map[string]int{"month": 12, "dayOfMonth": 27, "dayOfWeek": 6}: true,
&map[string]int{"month": 11, "dayOfMonth": 27, "dayOfWeek": 6}: false,
// dayOfMonth and dayOfWeek (they are in an OR statement)
&map[string]int{"month": 3, "dayOfMonth": 28, "dayOfWeek": 6}: true,
&map[string]int{"month": 3, "dayOfMonth": 29, "dayOfWeek": 6}: true,
&map[string]int{"month": 3, "dayOfMonth": 28, "dayOfWeek": 5}: true,
&map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 6}: true, // only dayOfMonth false
&map[string]int{"month": 3, "dayOfMonth": 28, "dayOfWeek": 4}: true, // only dayOfWeek false
&map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 4}: false, // both false
}

func TestExpressionValuesEvaluation(t *testing.T) {
var expr Expression

err := json.Unmarshal([]byte(testingJSON), &expr)

if err != nil {
t.Errorf("ERROR while parsing the date expressions; %s\n", err)
}

for evals, expected := range expressionValuesEvaluationMap {

got, err := expr.Eval(*evals)
if err != nil {
t.Error("error from evaluating expression:", err)
}

if got != expected {
t.Errorf("expected expression to be evaluated to %t, got %t. Values: %#v.\n", expected, got, evals)
}
}
}

func BenchmarkExpression(b *testing.B) {

for i := 0; i < b.N; i++ {

var expr Expression

err := json.Unmarshal([]byte(testingJSON), &expr)

if err != nil {
fmt.Printf("ERROR while parsing the date expressions; %s\n", err)
return
}
}
}

func BenchmarkEvaluateExpression(b *testing.B) {

var expr Expression

err := json.Unmarshal([]byte(testingJSON), &expr)

if err != nil {
fmt.Printf("ERROR while parsing the date expressions; %s\n", err)
return
}

values := map[string]int{"month": 2, "dayOfMonth": 27, "dayOfWeek": 6}

b.ResetTimer()

for i := 0; i < b.N; i++ {
_, _ = expr.Eval(values)
}
}

+ 196
- 0
expression/structures.go View File

@@ -0,0 +1,196 @@
package expression

import (
"encoding/json"
"fmt"
)

// 'Raw' types used between translating the JSON string to a expression using interface type node which can be evaluated.

type rawExpression map[string]*json.RawMessage

type rawOperator rawExpression

type rawAnd struct {
rawOperator
}

type rawOr struct {
rawOperator
}

// Types used to store expressions that can be evaluated.

type Expression struct {
expression node
}

func (expr Expression) Expression() node {
return expr.expression
}

func (expr Expression) Eval(params map[string]int) (bool, error) {
if expr.expression == nil {
return false, fmt.Errorf("expession is nil")
}

return expr.expression.eval(params)
}

type node interface {
eval(params map[string]int) (bool, error)
}

type operator []node

type comparison struct {
parent string
data map[string]int
}

// evalInt is called by 'aliases' of comparison (equals, smallerThan, greaterThanOrEquals).
// params are the correct/current values of keys that can be checked for.
// The typeStr should contain the comparison alias type used for displaying error messages.
// callback is the function that should perform the comparison itself and return the resulting boolean.
func (c comparison) evalInt(params map[string]int, typeStr string, callback func(int, int) bool) (bool, error) {
for key, value := range c.data {
if dataVal, found := params[key]; found {
result := callback(dataVal, value)

switch c.parent {
case "and", "not", "":
// If only one in data is false, the AND statement itself is false.
if !result {
return false, nil
}
case "or":
// If only one in data is true, the OR statement itself is true.
if result {
return true, nil
}
}
} else {
return false, fmt.Errorf("invalid expression key %q used in %q expression", key, typeStr)
}
}

switch c.parent {
case "and", "not", "":
return true, nil
case "or":
return false, nil
default:
return false, fmt.Errorf("ERROR: invalid boolOperatorType %v, %s, %s", c, typeStr)
}
}

// Operators

type and operator

func (a and) eval(params map[string]int) (bool, error) {
if len(a) == 0 {
return false, fmt.Errorf("no expressions in \"and\" expression")
}

for _, expr := range a {
result, err := expr.eval(params)
if err != nil {
return false, err
}

if !result {
// No child in 'and' may be false.
return false, nil
}
}

return true, nil
}

type or operator

func (o or) eval(params map[string]int) (bool, error) {
if len(o) == 0 {
return false, fmt.Errorf("no expressions in \"or\" expression")
}

for _, expr := range o {
result, err := expr.eval(params)
if err != nil {
return false, err
}

if result {
// Only one child in 'or' expression has to be true.
return true, nil
}
}

return false, nil
}

// Not

type not []node

func (n not) eval(params map[string]int) (bool, error) {
if len(n) == 0 {
return false, fmt.Errorf("no expression in \"not\" expression")
}

for _, expr := range n {
result, err := expr.eval(params)
if err != nil {
return false, err
}

if result {
return false, nil
}
}

return true, nil
}

// Comparisons

type equals comparison

func (eq equals) eval(params map[string]int) (bool, error) {
return comparison(eq).evalInt(params, "equals", func(key, value int) bool {
return key == value
})
}

type greaterThan comparison

func (gt greaterThan) eval(params map[string]int) (bool, error) {
return comparison(gt).evalInt(params, "greaterThan", func(key, value int) bool {
return key > value
})
}

type greaterThanOrEqual comparison

func (gt greaterThanOrEqual) eval(params map[string]int) (bool, error) {
return comparison(gt).evalInt(params, "greaterThanOrEqual", func(key, value int) bool {
return key >= value
})
}

type smallerThan comparison

func (st smallerThan) eval(params map[string]int) (bool, error) {
return comparison(st).evalInt(params, "smallerThan", func(key, value int) bool {
return key < value
})
}

type smallerThanOrEqual comparison

func (st smallerThanOrEqual) eval(params map[string]int) (bool, error) {
return comparison(st).evalInt(params, "smallerThanOrEqual", func(key, value int) bool {
return key <= value
})
}

Loading…
Cancel
Save