Browse Source

Added new way to format expressions

Remi Reuvekamp 3 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 @@
1 1
 package main
2 2
 
3
-type backgroundError struct {
4
-	fileName string
3
+import (
4
+	"fmt"
5
+	"time"
6
+)
7
+
8
+var now = time.Now()
9
+
10
+// bgError is used by determineBackground
11
+type bgError struct {
12
+	msg       string
13
+	bg        background
14
+	errString string
5 15
 }
6 16
 
17
+// Error messages
18
+const (
19
+	bgErrorMsgInvalidExpr = "Invalid expression for file %s. Expressions should return a boolean.\nDate expression: %s. This may be of help: %s."
20
+	bgErrorOccured        = "An error occured while processing the expression for file %s. Date expression: %s. This may be of help: %s."
21
+)
22
+
7 23
 // determineBackground gives a list of backgrounds for today.
24
+// All errors in the []error returned are of type bgError.
8 25
 func determineBackground(cfg config) ([]background, []error) {
9 26
 
27
+	year, week := now.ISOWeek()
28
+
29
+	params := map[string]int{
30
+		"dayOfWeek":  int(now.Weekday()),
31
+		"dayOfMonth": now.Day(),
32
+		"dayOfYear":  now.YearDay(),
33
+		"week":       week,
34
+		"month":      int(now.Month()),
35
+		"year":       year,
36
+	}
37
+
10 38
 	var errs []error
11 39
 	var backgrounds []background
12 40
 
41
+	var err error
42
+
13 43
 	for _, bg := range cfg.Backgrounds {
14
-		var match bool
15
-		for _, d := range bg.Dates {
16
-			isNow, err := dateIsNow(d)
44
+		result := true
45
+
46
+		if !bg.Always {
47
+			result, err = bg.expr.Eval(params)
48
+
49
+			fmt.Printf("%#v\n", bg.expr.Expression())
50
+
17 51
 			if err != nil {
18
-				switch err {
19
-				case errDateIsEmpty:
20
-					errs = append(errs, backgroundError{bg.FileName})
21
-				default:
22
-					errs = append(errs, err)
23
-				}
52
+				errs = append(errs, err)
24 53
 				continue
25 54
 			}
26
-
27
-			if isNow {
28
-				match = true
29
-				break
30
-			}
31 55
 		}
32
-		if match || len(bg.Dates) == 0 {
56
+
57
+		if result {
33 58
 			backgrounds = append(backgrounds, bg)
34 59
 		}
35 60
 	}
@@ -37,7 +62,6 @@ func determineBackground(cfg config) ([]background, []error) {
37 62
 	return backgrounds, errs
38 63
 }
39 64
 
40
-func (err backgroundError) Error() string {
41
-	return "One or more date values for " + err.fileName + " is empty. Please check " +
42
-		"the configuration file for spelling mistakes and empty date values."
65
+func (err bgError) Error() string {
66
+	return fmt.Sprintf(err.msg, err.bg.FileName, err.bg.DateExpr, err.errString)
43 67
 }

+ 23
- 100
config.go View File

@@ -4,10 +4,11 @@ import (
4 4
 	"encoding/json"
5 5
 	"errors"
6 6
 	"flag"
7
-	"fmt"
8 7
 	"io/ioutil"
9 8
 	"strings"
10 9
 
10
+	"remi.im/misc/background/expression"
11
+
11 12
 	"gopkg.in/yaml.v2"
12 13
 )
13 14
 
@@ -25,47 +26,20 @@ var errCannotReadFile = errors.New("Cannot read the file, does it exist?")
25 26
 
26 27
 // config is the format of the configuration file.
27 28
 type config struct {
28
-	FilePathCurBg string       `yaml:"filePathCurrentBackground",json:"filePathCurrentBackground"`
29
-	DirPathBgs    string       `yaml:"directoryPathAllBackgrounds",json:"directoryPathAllBackgrounds"`
30
-	Backgrounds   []background `yaml:"backgrounds",json:"backgrounds"`
29
+	FilePathCurBg string       `json:"filePathCurrentBackground"`
30
+	DirPathBgs    string       `json:"directoryPathAllBackgrounds"`
31
+	Backgrounds   []background `json:"backgrounds"`
31 32
 }
32 33
 
33 34
 // background pairs a background filename with a []date the background should be active.
34 35
 type background struct {
35 36
 	FileName string `yaml:"filename",json:"filename"`
36 37
 	Note     string `yaml:"note,omitempty",json:"note,omitempty"`
37
-	Dates    []date `yaml:"dates,omitempty",json:"dates,omitempty"`
38
-}
39
-
40
-// date contains options for matching dates.
41
-type date struct {
42
-	WeekDays  []dateWeekDay  `yaml:"weekDays,omitempty",json:"weekDays,omitempty"`
43
-	MonthDays []dateMonthDay `yaml:"days,omitempty",json:"days,omitempty"`
44
-	YearDays  []dateYearDay  `yaml:"yearDays,omitempty",json:"yearDays,omitempty"`
45
-	Weeks     []dateWeek     `yaml:"weeks,omitempty",json:"weeks,omitempty"`
46
-	Months    []dateMonth    `yaml:"months,omitempty",json:"months,omitempty"`
47
-	Years     []dateYear     `yaml:"years,omitempty",json:"years,omitempty"`
48
-}
49
-
50
-// dateType can specify an integer by an exact number, a range and/or an interval.
51
-type dateType struct {
52
-	// A maximum of one of each row/type (e.g. dayMin+dayMax or dayInterval) is used.
53
-	Exact int `yaml:"exact,omitempty",json:"exact,omitempty"` // 5 (only the fifth {type})
54
-
55
-	// Range. 5 - 10 (5, 6, 7, 8, 9, 10 {type}t)
56
-	Min int `yaml:"min,omitempty",json:"min,omitempty"`
57
-	Max int `yaml:"max,omitempty",json:"max,omitempty"`
58
-
59
-	Interval int `yaml:"interval,omitempty",json:"interval,omitempty"` // Every x of {type}. 3 (3, 6, 9, etc)
38
+	expr     expression.Expression
39
+	DateExpr *json.RawMessage `json:"date,omitempty"`
40
+	Always   bool             `json:"always,omitempty"`
60 41
 }
61 42
 
62
-type dateWeekDay dateType
63
-type dateMonthDay dateType
64
-type dateYearDay dateType
65
-type dateWeek dateType
66
-type dateMonth dateType
67
-type dateYear dateType
68
-
69 43
 // loadConfig loads the configuration of type config with the filename in
70 44
 // cfgFilePath. cfgFormat is used for determining the format which the configuration
71 45
 // written, which could be either yaml or json.
@@ -78,17 +52,25 @@ func loadConfig() (config, error) {
78 52
 	}
79 53
 
80 54
 	switch cfgFormat {
81
-	case cfgTypeYAML:
82
-		// Too bad a fairly large library has to be imported.
83
-		err = yaml.Unmarshal(data, &cfg)
84 55
 	case cfgTypeJSON:
85 56
 		err = json.Unmarshal(data, &cfg)
57
+	}
58
+
59
+	for i, bg := range cfg.Backgrounds {
60
+		if bg.Always {
61
+			continue
62
+		}
63
+
64
+		if bg.DateExpr == nil {
65
+			return config{}, errors.New("date expression is empty but 'always' is not set")
66
+		}
67
+
68
+		err := json.Unmarshal(*bg.DateExpr, &cfg.Backgrounds[i].expr)
86 69
 		if err != nil {
87
-			err = fmt.Errorf("json: %v", err)
70
+			return config{}, err
88 71
 		}
89
-		// (Yaml library also prefixes the error with 'yaml: ', it might make the
90
-		//  user realise he/she set an incorrect config format.)
91 72
 	}
73
+
92 74
 	return cfg, err
93 75
 }
94 76
 
@@ -123,66 +105,7 @@ func initializeConfig() config {
123 105
 			background{
124 106
 				FileName: "special1.jpg",
125 107
 				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.",
126
-				Dates: []date{
127
-					date{
128
-						// Day of month: 14, 16, 18, 20
129
-						// In months 5, 6, 7, 8
130
-						// In year 2015
131
-						MonthDays: []dateMonthDay{
132
-							dateMonthDay{
133
-								Min: 14,
134
-								Max: 20,
135
-							},
136
-							// AND
137
-							dateMonthDay{
138
-								Interval: 2,
139
-							},
140
-						},
141
-						// AND
142
-						Months: []dateMonth{
143
-							dateMonth{
144
-								Min: 5,
145
-								Max: 8,
146
-							},
147
-						},
148
-						// AND
149
-						Years: []dateYear{
150
-							dateYear{
151
-								Exact: 2015,
152
-							},
153
-						},
154
-					},
155
-					// OR
156
-					date{
157
-						// Every wednesday
158
-						WeekDays: []dateWeekDay{
159
-							dateWeekDay{
160
-								Exact: 3,
161
-							},
162
-						},
163
-					},
164
-					// OR
165
-					date{
166
-						// Week number 40
167
-						Weeks: []dateWeek{
168
-							dateWeek{
169
-								Exact: 40,
170
-							},
171
-						},
172
-					},
173
-					// OR
174
-					date{
175
-						YearDays: []dateYearDay{
176
-							dateYearDay{
177
-								Max: 10,
178
-							},
179
-							// AND
180
-							dateYearDay{
181
-								Min: 360,
182
-							},
183
-						},
184
-					},
185
-				},
108
+				//Date:     "(monthDay >= 14 && monthDay <= 20 && monthDay % 2 && month >= 5 && month <= 8 && year == 2015) || weekDay = 3 || week == 40 || (yearDay <= 10 || yearDay >= 360)",
186 109
 			},
187 110
 			background{
188 111
 				FileName: "default.jpg",

+ 0
- 186
date.go View File

@@ -1,186 +0,0 @@
1
-package main
2
-
3
-import (
4
-	"errors"
5
-	"fmt"
6
-	"time"
7
-)
8
-
9
-var errDateIsEmpty = errors.New("date is empty")
10
-
11
-var now = time.Now()
12
-var loc = time.FixedZone("Europe/London", 0)
13
-
14
-// dateIsNow checks if today is in the given date.
15
-func dateIsNow(d date) (bool, error) {
16
-	if dateIsEmpty(d) {
17
-		return false, errDateIsEmpty
18
-	}
19
-
20
-	// Week days
21
-	weekDays := make([]dateType, len(d.WeekDays))
22
-	for i, dt := range d.WeekDays {
23
-		weekDays[i] = dateType(dt)
24
-	}
25
-
26
-	match, err := dateTypeSliceIsNow(weekDays, "weekDay")
27
-	if !match || err != nil {
28
-		return false, err
29
-	}
30
-
31
-	// Month days
32
-	monthDays := make([]dateType, len(d.MonthDays))
33
-	for i, dt := range d.MonthDays {
34
-		monthDays[i] = dateType(dt)
35
-	}
36
-
37
-	match, err = dateTypeSliceIsNow(monthDays, "monthDay")
38
-	if !match || err != nil {
39
-		return false, err
40
-	}
41
-
42
-	// Year days
43
-	yearDays := make([]dateType, len(d.YearDays))
44
-	for i, dt := range d.YearDays {
45
-		yearDays[i] = dateType(dt)
46
-	}
47
-
48
-	match, err = dateTypeSliceIsNow(yearDays, "yearDay")
49
-	if !match || err != nil {
50
-		return false, err
51
-	}
52
-
53
-	// Weeks
54
-	weeks := make([]dateType, len(d.Weeks))
55
-	for i, dt := range d.Weeks {
56
-		weeks[i] = dateType(dt)
57
-	}
58
-
59
-	match, err = dateTypeSliceIsNow(weeks, "week")
60
-	if !match || err != nil {
61
-		return false, err
62
-	}
63
-
64
-	// Months
65
-	months := make([]dateType, len(d.Months))
66
-	for i, dt := range d.Months {
67
-		months[i] = dateType(dt)
68
-	}
69
-
70
-	match, err = dateTypeSliceIsNow(months, "month")
71
-	if !match || err != nil {
72
-		return false, err
73
-	}
74
-
75
-	// Years
76
-	years := make([]dateType, len(d.Years))
77
-	for i, dt := range d.Years {
78
-		years[i] = dateType(dt)
79
-	}
80
-
81
-	match, err = dateTypeSliceIsNow(years, "year")
82
-	if !match || err != nil {
83
-		return false, err
84
-	}
85
-
86
-	return true, nil
87
-}
88
-
89
-// dateTypeSliceIsNow checks if today is in the given []dateType.
90
-// Only the specified type in checkFor is checked. See dateTypeIsNow.
91
-func dateTypeSliceIsNow(dts []dateType, checkFor string) (bool, error) {
92
-	if len(dts) == 0 {
93
-		return true, nil
94
-	}
95
-
96
-	for _, dt := range dts {
97
-		matches, err := dateTypeIsNow(dt, checkFor)
98
-		if err != nil {
99
-			return false, err
100
-		}
101
-		if matches {
102
-			return true, nil
103
-		}
104
-	}
105
-
106
-	return false, nil
107
-}
108
-
109
-// dateIsEmpty returns true if the date doesn't contain any date specification values.
110
-func dateIsEmpty(d date) (_ bool) {
111
-	if len(d.WeekDays) > 0 {
112
-		return
113
-	}
114
-
115
-	if len(d.MonthDays) > 0 {
116
-		return
117
-	}
118
-
119
-	if len(d.YearDays) > 0 {
120
-		return
121
-	}
122
-
123
-	if len(d.Weeks) > 0 {
124
-		return
125
-	}
126
-
127
-	if len(d.Months) > 0 {
128
-		return
129
-	}
130
-
131
-	if len(d.Years) > 0 {
132
-		return
133
-	}
134
-
135
-	return true
136
-}
137
-
138
-// dateTypeIsNow checks if today is in the given dateType.
139
-// Only the specified type in checkFor is checked. checkFor can be:
140
-// monthDay, weekDay, weeks, month, year
141
-func dateTypeIsNow(dt dateType, checkFor string) (bool, error) {
142
-	var val int
143
-	switch checkFor {
144
-	case "weekDay":
145
-		val = int(now.Weekday())
146
-	case "monthDay":
147
-		val = now.Day()
148
-	case "yearDay":
149
-		val = now.YearDay()
150
-	case "week":
151
-		_, val = now.ISOWeek()
152
-	case "month":
153
-		val = int(now.Month())
154
-	case "year":
155
-		val = now.Year()
156
-	default:
157
-		return false, fmt.Errorf("Invalid checkFor string; %s", checkFor)
158
-	}
159
-
160
-	if val == dt.Exact {
161
-		return true, nil
162
-	}
163
-
164
-	// Range
165
-
166
-	// Both min and max specified
167
-	if dt.Min > 0 && dt.Max > 0 && val >= dt.Min && val <= dt.Max {
168
-		return true, nil
169
-	}
170
-
171
-	// Only min
172
-	if dt.Min > 0 && dt.Max <= 0 && val >= dt.Min {
173
-		return true, nil
174
-	}
175
-
176
-	// Only max
177
-	if dt.Max > 0 && dt.Min <= 0 && val <= dt.Max {
178
-		return true, nil
179
-	}
180
-
181
-	if dt.Interval > 0 && val%dt.Interval == 0 {
182
-		return true, nil
183
-	}
184
-
185
-	return false, nil
186
-}

+ 0
- 132
date_test.go View File

@@ -1,132 +0,0 @@
1
-package main
2
-
3
-import (
4
-	"testing"
5
-	"time"
6
-)
7
-
8
-func TestDateIsNow(t *testing.T) {
9
-
10
-	// Test monthDay, month and year
11
-	d := initializeConfig().Backgrounds[0].Dates[0]
12
-	testDateIsNow(t, d, time.Date(2015, 6, 14, 0, 0, 0, 0, loc), true)
13
-	testDateIsNow(t, d, time.Date(2015, 6, 15, 0, 0, 0, 0, loc), true)
14
-	testDateIsNow(t, d, time.Date(2015, 6, 22, 0, 0, 0, 0, loc), true)
15
-	testDateIsNow(t, d, time.Date(2015, 8, 14, 0, 0, 0, 0, loc), true)
16
-	testDateIsNow(t, d, time.Date(2015, 6, 1, 0, 0, 0, 0, loc), false)
17
-	testDateIsNow(t, d, time.Date(2015, 4, 15, 0, 0, 0, 0, loc), false)
18
-	testDateIsNow(t, d, time.Date(2016, 6, 14, 0, 0, 0, 0, loc), false) // Year
19
-
20
-	// Test weekDays, different date.
21
-	d = initializeConfig().Backgrounds[0].Dates[1]
22
-	testDateIsNow(t, d, dateByDay(time.Now(), 3), true)
23
-	testDateIsNow(t, d, dateByDay(time.Now(), 2), false)
24
-
25
-	// Test weeks, different date thing.
26
-	d = initializeConfig().Backgrounds[0].Dates[2]
27
-	testDateIsNow(t, d, firstDayOfISOWeek(2019, 40, loc), true)
28
-	testDateIsNow(t, d, firstDayOfISOWeek(2019, 39, loc), false)
29
-
30
-	// Test yearDays. Optional range
31
-	d = initializeConfig().Backgrounds[0].Dates[3]
32
-	testDateIsNow(t, d, time.Date(2015, 1, 10, 0, 0, 0, 0, loc), true)
33
-	testDateIsNow(t, d, time.Date(2015, 1, 12, 0, 0, 0, 0, loc), false)
34
-	testDateIsNow(t, d, time.Date(2015, 12, 28, 0, 0, 0, 0, loc), true)
35
-	testDateIsNow(t, d, time.Date(2015, 12, 24, 0, 0, 0, 0, loc), false)
36
-
37
-}
38
-
39
-func testDateIsNow(t *testing.T, d date, tm time.Time, expectedReturn bool) {
40
-	now = tm
41
-	result, err := dateIsNow(d)
42
-	if err != nil {
43
-		t.Error(err.Error())
44
-	}
45
-	if result != expectedReturn {
46
-		t.Errorf("Expected %t got %t, time; %v", expectedReturn, result, tm)
47
-	}
48
-}
49
-
50
-func TestDateTypeIsNow(t *testing.T) {
51
-	dt := dateType{
52
-		// You don't need to use all of them, but just for testing.
53
-		Exact:    5,
54
-		Min:      8,
55
-		Max:      10,
56
-		Interval: 12,
57
-	}
58
-
59
-	// Test exact
60
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 5, 0, 0, 0, 0, loc), true)
61
-
62
-	// Test range
63
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 8, 0, 0, 0, 0, loc), true)
64
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 9, 0, 0, 0, 0, loc), true)
65
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 10, 0, 0, 0, 0, loc), true)
66
-
67
-	// Test interval
68
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 12, 0, 0, 0, 0, loc), true)
69
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 24, 0, 0, 0, 0, loc), true)
70
-
71
-	// Things that should give false
72
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 4, 0, 0, 0, 0, loc), false)
73
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 1, 0, 0, 0, 0, loc), false)
74
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 25, 0, 0, 0, 0, loc), false)
75
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 23, 0, 0, 0, 0, loc), false)
76
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 13, 0, 0, 0, 0, loc), false)
77
-	testDateTypeIsNow(t, dt, time.Date(2015, 11, 30, 0, 0, 0, 0, loc), false)
78
-}
79
-
80
-func testDateTypeIsNow(t *testing.T, dt dateType, tm time.Time, expectedReturn bool) {
81
-	now = tm
82
-	result, err := dateTypeIsNow(dt, "monthDay")
83
-	if err != nil {
84
-		t.Error(err.Error())
85
-	}
86
-	if result != expectedReturn {
87
-		t.Errorf("Expected %t got %t, time; %v", expectedReturn, result, tm)
88
-	}
89
-}
90
-
91
-// firstDayOfISOWeek was 'stolen', but it's 'just a test' :$ .
92
-// http://www.xferion.com/golang-reverse-isoweek-get-the-date-of-the-first-day-of-iso-week/
93
-func firstDayOfISOWeek(year int, week int, timezone *time.Location) time.Time {
94
-	date := time.Date(year, 0, 0, 0, 0, 0, 0, timezone)
95
-	isoYear, isoWeek := date.ISOWeek()
96
-
97
-	// Iterate back to Monday
98
-	for date.Weekday() != time.Monday {
99
-		date = date.AddDate(0, 0, -1)
100
-		isoYear, isoWeek = date.ISOWeek()
101
-	}
102
-
103
-	// Iterate forward to the first day of the first week
104
-	for isoYear < year {
105
-		date = date.AddDate(0, 0, 7)
106
-		isoYear, isoWeek = date.ISOWeek()
107
-	}
108
-
109
-	// Iterate forward to the first day of the given week
110
-	for isoWeek < week {
111
-		date = date.AddDate(0, 0, 7)
112
-		isoYear, isoWeek = date.ISOWeek()
113
-	}
114
-
115
-	return date
116
-}
117
-
118
-func TestDateByDate(t *testing.T) {
119
-	dateByDay(time.Now(), 0)
120
-}
121
-
122
-// dateByday goes back in time from the given time, so that it reached the given
123
-// weekDay. That time is returned.
124
-func dateByDay(d time.Time, weekDay time.Weekday) time.Time {
125
-	if d.Weekday() > weekDay {
126
-		d = d.AddDate(0, 0, -1*int(d.Weekday()-weekDay))
127
-	} else if d.Weekday() < weekDay {
128
-		d = d.AddDate(0, 0, -1*(7-int(weekDay-d.Weekday())))
129
-	}
130
-
131
-	return d
132
-}

+ 141
- 0
expression/expression.go View File

@@ -0,0 +1,141 @@
1
+// Package expression can parse and evaluate boolean expressions from JSON.
2
+package expression
3
+
4
+import (
5
+	"encoding/json"
6
+	"fmt"
7
+)
8
+
9
+// UnmarshalJSON recursively unmarshals the given input JSON and set Expression.Expression with the unmarshalled values.
10
+func (expr *Expression) UnmarshalJSON(input []byte) error {
11
+	var rawExpr rawExpression
12
+
13
+	err := json.Unmarshal(input, &rawExpr)
14
+	if err != nil {
15
+		return err
16
+	}
17
+
18
+	// unmarshalRawExpression may (/will probably) call itself recursively while it goes through the nested expression.
19
+
20
+	for eType, e := range rawExpr {
21
+		expr.expression, err = unmarshalRawExpression(eType, "", e)
22
+	}
23
+
24
+	return err
25
+}
26
+
27
+// unmarshalRawExpression creates a node for the given rawExpression.
28
+// It may(/will probably) call itself recursively while it goes through the nested rawExpression.
29
+func unmarshalRawExpression(exprType string, lastBoolOperator string, expr *json.RawMessage) (node, error) {
30
+	//exprType := *ptrExprType
31
+	switch exprType {
32
+	case "and", "or", "&&", "||":
33
+		switch exprType {
34
+		case "and", "&&":
35
+			exprType = "and"
36
+		case "or", "||":
37
+			exprType = "or"
38
+		}
39
+		// Type is a boolean operator
40
+
41
+		var rawOp rawOperator
42
+
43
+		err := json.Unmarshal(*expr, &rawOp)
44
+		if err != nil {
45
+			return nil, err
46
+		}
47
+
48
+		// Unmarshal the children of operator (left, right)
49
+
50
+		if len(rawOp) == 0 {
51
+			return nil, fmt.Errorf("no expressions in %q expression", exprType)
52
+		}
53
+
54
+		var childExprs operator
55
+		for key, value := range rawOp {
56
+			child, err := unmarshalRawExpression(key, exprType, value)
57
+			if err != nil {
58
+				return nil, err
59
+			}
60
+
61
+			childExprs = append(childExprs, child)
62
+		}
63
+
64
+		// Switch through the operator type and return the correct implementation of node for it.
65
+		switch exprType {
66
+		case "and", "&&":
67
+			return and(childExprs), nil
68
+		case "or", "||":
69
+			return or(childExprs), nil
70
+		default:
71
+			return nil, fmt.Errorf("What? %s is not a known operator", exprType)
72
+		}
73
+	case "equals", "smallerThan", "greaterThan", "smallerThanOrEqual", "greaterThanOrEqual",
74
+		"==", "<", ">", "<=", ">=":
75
+		// Type is a comparison
76
+
77
+		if expr == nil {
78
+			return nil, fmt.Errorf("expression with type %q does not contain an expression", exprType)
79
+		}
80
+
81
+		var comp comparison
82
+
83
+		err := json.Unmarshal(*expr, &comp.data)
84
+		if err != nil {
85
+			return nil, err
86
+		}
87
+
88
+		if len(comp.data) == 0 {
89
+			return nil, fmt.Errorf("no expressions in %q expression", exprType)
90
+		}
91
+
92
+		comp.parent = lastBoolOperator
93
+
94
+		// Switch through the comparison type and return the correct implementation of node for it.
95
+		switch exprType {
96
+		case "equals", "==":
97
+			return equals(comp), nil
98
+		case "smallerThan", "<":
99
+			return smallerThan(comp), nil
100
+		case "greaterThan", ">":
101
+			return greaterThan(comp), nil
102
+		case "smallerThanOrEqual", "<=":
103
+			return smallerThanOrEqual(comp), nil
104
+		case "greaterThanOrEqual", ">=":
105
+			return greaterThanOrEqual(comp), nil
106
+		default:
107
+			return nil, fmt.Errorf("What? %s is not a known comparison", exprType)
108
+		}
109
+	case "not", "!":
110
+		exprType = "not"
111
+		// Type is 'not'
112
+
113
+		var ra rawExpression
114
+
115
+		err := json.Unmarshal(*expr, &ra)
116
+		if err != nil {
117
+			return nil, err
118
+		}
119
+
120
+		// Unmarshal the child elem.
121
+		var n not
122
+		for key, value := range ra {
123
+			elem, err := unmarshalRawExpression(key, exprType, value)
124
+			if err != nil {
125
+				return nil, err
126
+			}
127
+
128
+			n = append(n, elem)
129
+		}
130
+
131
+		if err != nil {
132
+			return nil, err
133
+		}
134
+
135
+		return n, nil
136
+	case "":
137
+		return nil, fmt.Errorf("no expression type")
138
+	default:
139
+		return nil, fmt.Errorf("unknown expression type: %q", exprType)
140
+	}
141
+}

+ 131
- 0
expression/expression_test.go View File

@@ -0,0 +1,131 @@
1
+package expression
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"testing"
7
+)
8
+
9
+// (month == 12 OR month <= 10) AND month > 2 AND (not(dayOfMonth < 28) OR dayOfWeek >= 5)
10
+const testingJSON = `{
11
+	"and": {
12
+		"or": {
13
+			"equals": {
14
+				"month": 12
15
+			},
16
+			"smallerThanOrEqual": {
17
+				"month": 10
18
+			}
19
+		},
20
+		"greaterThan": {
21
+			"month": 2
22
+		},
23
+		"||": {
24
+			"!": {
25
+				"<": {
26
+					"dayOfMonth": 28
27
+				}
28
+			},
29
+			">=": {
30
+				"dayOfWeek": 5
31
+			}
32
+		}
33
+	}
34
+}`
35
+
36
+func TestUnmarshalExpression(t *testing.T) {
37
+
38
+	var expr Expression
39
+
40
+	err := json.Unmarshal([]byte(testingJSON), &expr)
41
+
42
+	if err != nil {
43
+		t.Errorf("ERROR while parsing the date expressions; %s\n", err)
44
+	}
45
+
46
+	values := map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 6}
47
+
48
+	result, err := expr.Eval(values)
49
+	if err != nil {
50
+		t.Error("error from evaluating expression:", err)
51
+	}
52
+
53
+	if !result {
54
+		t.Error("expected expression to be evaluated to true, got false")
55
+	}
56
+}
57
+
58
+var expressionValuesEvaluationMap = map[*map[string]int]bool{
59
+	// month
60
+	&map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 6}:  true,
61
+	&map[string]int{"month": 4, "dayOfMonth": 27, "dayOfWeek": 6}:  true,
62
+	&map[string]int{"month": 5, "dayOfMonth": 27, "dayOfWeek": 6}:  true,
63
+	&map[string]int{"month": 9, "dayOfMonth": 27, "dayOfWeek": 6}:  true,
64
+	&map[string]int{"month": 10, "dayOfMonth": 27, "dayOfWeek": 6}: true,
65
+	&map[string]int{"month": 12, "dayOfMonth": 27, "dayOfWeek": 6}: true,
66
+	&map[string]int{"month": 11, "dayOfMonth": 27, "dayOfWeek": 6}: false,
67
+	// dayOfMonth and dayOfWeek (they are in an OR statement)
68
+	&map[string]int{"month": 3, "dayOfMonth": 28, "dayOfWeek": 6}: true,
69
+	&map[string]int{"month": 3, "dayOfMonth": 29, "dayOfWeek": 6}: true,
70
+	&map[string]int{"month": 3, "dayOfMonth": 28, "dayOfWeek": 5}: true,
71
+	&map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 6}: true,  // only dayOfMonth false
72
+	&map[string]int{"month": 3, "dayOfMonth": 28, "dayOfWeek": 4}: true,  // only dayOfWeek false
73
+	&map[string]int{"month": 3, "dayOfMonth": 27, "dayOfWeek": 4}: false, // both false
74
+}
75
+
76
+func TestExpressionValuesEvaluation(t *testing.T) {
77
+	var expr Expression
78
+
79
+	err := json.Unmarshal([]byte(testingJSON), &expr)
80
+
81
+	if err != nil {
82
+		t.Errorf("ERROR while parsing the date expressions; %s\n", err)
83
+	}
84
+
85
+	for evals, expected := range expressionValuesEvaluationMap {
86
+
87
+		got, err := expr.Eval(*evals)
88
+		if err != nil {
89
+			t.Error("error from evaluating expression:", err)
90
+		}
91
+
92
+		if got != expected {
93
+			t.Errorf("expected expression to be evaluated to %t, got %t. Values: %#v.\n", expected, got, evals)
94
+		}
95
+	}
96
+}
97
+
98
+func BenchmarkExpression(b *testing.B) {
99
+
100
+	for i := 0; i < b.N; i++ {
101
+
102
+		var expr Expression
103
+
104
+		err := json.Unmarshal([]byte(testingJSON), &expr)
105
+
106
+		if err != nil {
107
+			fmt.Printf("ERROR while parsing the date expressions; %s\n", err)
108
+			return
109
+		}
110
+	}
111
+}
112
+
113
+func BenchmarkEvaluateExpression(b *testing.B) {
114
+
115
+	var expr Expression
116
+
117
+	err := json.Unmarshal([]byte(testingJSON), &expr)
118
+
119
+	if err != nil {
120
+		fmt.Printf("ERROR while parsing the date expressions; %s\n", err)
121
+		return
122
+	}
123
+
124
+	values := map[string]int{"month": 2, "dayOfMonth": 27, "dayOfWeek": 6}
125
+
126
+	b.ResetTimer()
127
+
128
+	for i := 0; i < b.N; i++ {
129
+		_, _ = expr.Eval(values)
130
+	}
131
+}

+ 196
- 0
expression/structures.go View File

@@ -0,0 +1,196 @@
1
+package expression
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+)
7
+
8
+// 'Raw' types used between translating the JSON string to a expression using interface type node which can be evaluated.
9
+
10
+type rawExpression map[string]*json.RawMessage
11
+
12
+type rawOperator rawExpression
13
+
14
+type rawAnd struct {
15
+	rawOperator
16
+}
17
+
18
+type rawOr struct {
19
+	rawOperator
20
+}
21
+
22
+// Types used to store expressions that can be evaluated.
23
+
24
+type Expression struct {
25
+	expression node
26
+}
27
+
28
+func (expr Expression) Expression() node {
29
+	return expr.expression
30
+}
31
+
32
+func (expr Expression) Eval(params map[string]int) (bool, error) {
33
+	if expr.expression == nil {
34
+		return false, fmt.Errorf("expession is nil")
35
+	}
36
+
37
+	return expr.expression.eval(params)
38
+}
39
+
40
+type node interface {
41
+	eval(params map[string]int) (bool, error)
42
+}
43
+
44
+type operator []node
45
+
46
+type comparison struct {
47
+	parent string
48
+	data   map[string]int
49
+}
50
+
51
+// evalInt is called by 'aliases' of comparison (equals, smallerThan, greaterThanOrEquals).
52
+// params are the correct/current values of keys that can be checked for.
53
+// The typeStr should contain the comparison alias type used for displaying error messages.
54
+// callback is the function that should perform the comparison itself and return the resulting boolean.
55
+func (c comparison) evalInt(params map[string]int, typeStr string, callback func(int, int) bool) (bool, error) {
56
+	for key, value := range c.data {
57
+		if dataVal, found := params[key]; found {
58
+			result := callback(dataVal, value)
59
+
60
+			switch c.parent {
61
+			case "and", "not", "":
62
+				// If only one in data is false, the AND statement itself is false.
63
+				if !result {
64
+					return false, nil
65
+				}
66
+			case "or":
67
+				// If only one in data is true, the OR statement itself is true.
68
+				if result {
69
+					return true, nil
70
+				}
71
+			}
72
+		} else {
73
+			return false, fmt.Errorf("invalid expression key %q used in %q expression", key, typeStr)
74
+		}
75
+	}
76
+
77
+	switch c.parent {
78
+	case "and", "not", "":
79
+		return true, nil
80
+	case "or":
81
+		return false, nil
82
+	default:
83
+		return false, fmt.Errorf("ERROR: invalid boolOperatorType %v, %s, %s", c, typeStr)
84
+	}
85
+}
86
+
87
+// Operators
88
+
89
+type and operator
90
+
91
+func (a and) eval(params map[string]int) (bool, error) {
92
+	if len(a) == 0 {
93
+		return false, fmt.Errorf("no expressions in \"and\" expression")
94
+	}
95
+
96
+	for _, expr := range a {
97
+		result, err := expr.eval(params)
98
+		if err != nil {
99
+			return false, err
100
+		}
101
+
102
+		if !result {
103
+			// No child in 'and' may be false.
104
+			return false, nil
105
+		}
106
+	}
107
+
108
+	return true, nil
109
+}
110
+
111
+type or operator
112
+
113
+func (o or) eval(params map[string]int) (bool, error) {
114
+	if len(o) == 0 {
115
+		return false, fmt.Errorf("no expressions in \"or\" expression")
116
+	}
117
+
118
+	for _, expr := range o {
119
+		result, err := expr.eval(params)
120
+		if err != nil {
121
+			return false, err
122
+		}
123
+
124
+		if result {
125
+			// Only one child in 'or' expression has to be true.
126
+			return true, nil
127
+		}
128
+	}
129
+
130
+	return false, nil
131
+}
132
+
133
+// Not
134
+
135
+type not []node
136
+
137
+func (n not) eval(params map[string]int) (bool, error) {
138
+	if len(n) == 0 {
139
+		return false, fmt.Errorf("no expression in \"not\" expression")
140
+	}
141
+
142
+	for _, expr := range n {
143
+		result, err := expr.eval(params)
144
+		if err != nil {
145
+			return false, err
146
+		}
147
+
148
+		if result {
149
+			return false, nil
150
+		}
151
+	}
152
+
153
+	return true, nil
154
+}
155
+
156
+// Comparisons
157
+
158
+type equals comparison
159
+
160
+func (eq equals) eval(params map[string]int) (bool, error) {
161
+	return comparison(eq).evalInt(params, "equals", func(key, value int) bool {
162
+		return key == value
163
+	})
164
+}
165
+
166
+type greaterThan comparison
167
+
168
+func (gt greaterThan) eval(params map[string]int) (bool, error) {
169
+	return comparison(gt).evalInt(params, "greaterThan", func(key, value int) bool {
170
+		return key > value
171
+	})
172
+}
173
+
174
+type greaterThanOrEqual comparison
175
+
176
+func (gt greaterThanOrEqual) eval(params map[string]int) (bool, error) {
177
+	return comparison(gt).evalInt(params, "greaterThanOrEqual", func(key, value int) bool {
178
+		return key >= value
179
+	})
180
+}
181
+
182
+type smallerThan comparison
183
+
184
+func (st smallerThan) eval(params map[string]int) (bool, error) {
185
+	return comparison(st).evalInt(params, "smallerThan", func(key, value int) bool {
186
+		return key < value
187
+	})
188
+}
189
+
190
+type smallerThanOrEqual comparison
191
+
192
+func (st smallerThanOrEqual) eval(params map[string]int) (bool, error) {
193
+	return comparison(st).evalInt(params, "smallerThanOrEqual", func(key, value int) bool {
194
+		return key <= value
195
+	})
196
+}

Loading…
Cancel
Save