3
0

Added time, array, num and generic gval functions

This commit is contained in:
Peter Grlica
2021-03-17 17:30:39 +01:00
parent 0312dbe4bf
commit 49e5610af3
11 changed files with 1034 additions and 12 deletions
+121
View File
@@ -0,0 +1,121 @@
package expr
import (
"reflect"
"github.com/PaesslerAG/gval"
)
func ArrayFunctions() []gval.Language {
return []gval.Language{
gval.Function("push", push),
gval.Function("pop", pop),
gval.Function("shift", shift),
gval.Function("count", count),
gval.Function("has", has),
gval.Function("hasAll", hasAll),
}
}
// push adds a value to the end of slice, returns copy
func push(arr interface{}, p interface{}) (interface{}, error) {
if !isSlice(arr) {
return nil, &reflect.ValueError{Method: "Index", Kind: reflect.ValueOf(arr).Kind()}
}
c := reflect.ValueOf(arr)
nval := reflect.MakeSlice(c.Type(), c.Len()+1, c.Cap()+1)
reflect.Copy(nval, c)
nval.Index(c.Len()).Set(reflect.ValueOf(p))
return nval.Interface(), nil
}
// pop takes the last value in slice, does not modify original
func pop(arr interface{}) (interface{}, error) {
if !isSlice(arr) {
return nil, &reflect.ValueError{Method: "Index", Kind: reflect.ValueOf(arr).Kind()}
}
c := reflect.ValueOf(arr)
if c.Len() == 0 {
return nil, nil
}
return c.Index(c.Len() - 1).Interface(), nil
}
// shifts takes the first value in slice, does not modify original
func shift(arr interface{}) (interface{}, error) {
if !isSlice(arr) {
return nil, &reflect.ValueError{Method: "Index", Kind: reflect.ValueOf(arr).Kind()}
}
c := reflect.ValueOf(arr)
if c.Len() == 0 {
return nil, nil
}
return c.Index(0).Interface(), nil
}
// cound gets the count of occurences in the slice
func count(arr interface{}, v ...interface{}) int {
if !isSlice(arr) {
return 0
}
count := 0
for _, vv := range v {
if find(arr, vv) != -1 {
count++
}
}
return count
}
// has finds any occurence of the values in slice
func has(arr interface{}, v ...interface{}) bool {
return count(arr, v...) > 0
}
// hasAll finds all the occurences in the slice
func hasAll(arr interface{}, v ...interface{}) bool { return count(arr, v...) == len(v) }
// find takes a value and gets the position in slice
// if no results, returns -1
func find(arr interface{}, v interface{}) int {
if !isSlice(arr) {
return 0
}
for i := 0; i < reflect.ValueOf(arr).Len(); i++ {
c := reflect.ValueOf(arr)
if c.Index(i).Interface() == v {
return i
}
}
return -1
}
// slice slices slices
func slice(arr interface{}, start, end int) interface{} {
v := reflect.ValueOf(arr)
if start >= v.Len() {
return v.Interface()
}
if end == -1 || end > v.Len() {
end = v.Len()
}
return v.Slice(start, end).Interface()
}
+378
View File
@@ -0,0 +1,378 @@
package expr
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
var (
intArr = []int{}
stringArr = []string{"first"}
boolArr = []bool{true, true, false}
floatArr = []float64{69.420}
vals = map[string]interface{}{
"intArr": intArr,
"stringArr": stringArr,
"boolArr": boolArr,
"floatArr": floatArr,
"intVal": 42,
"stringVal": "foobar",
"boolVal": false,
}
)
type (
fac struct {
expect interface{}
arr interface{}
val interface{}
}
)
func Example_push_int() {
eval(`push(intArr, intVal)`, vals)
// output:
// [42]
}
func Example_push_string() {
eval(`push(stringArr, stringVal)`, vals)
// output:
// [first foobar]
}
func Example_push_bool() {
eval(`push(boolArr, boolVal)`, vals)
// output:
// [true true false false]
}
func Example_push_float() {
eval(`push(floatArr, 3.14)`, vals)
// output:
// [69.42 3.14]
}
func Example_pop_string() {
eval(`pop(stringArr)`, vals)
// output:
// first
}
func Example_pop_int() {
eval(`pop(intArr)`, vals)
// output:
// <nil>
}
func Example_pop_float() {
eval(`pop(floatArr)`, vals)
// output:
// 69.42
}
func Test_shift(t *testing.T) {
var (
req = require.New(t)
tcc = []tc{
{
value: []string{"1", "2", "3"},
expect: "1",
},
{
value: map[string]string{"test": "123"},
expect: nil,
},
{
value: []int{4, 5, 6, 7},
expect: 4,
},
{
value: []int{},
expect: nil,
},
{
value: int(1),
expect: nil,
},
{
value: []float64{11.1},
expect: 11.1,
},
}
)
for _, tst := range tcc {
val, _ := shift(tst.value)
req.Equal(tst.expect, val)
}
}
func Test_find(t *testing.T) {
var (
req = require.New(t)
tcc = []fac{
{
arr: []string{"1", "2", "3"},
val: "3",
expect: 2,
},
{
arr: []bool{true, false, true},
val: true,
expect: 0,
},
{
arr: []int{4, 5, 6, 7},
val: 7,
expect: 3,
},
{
arr: []int{},
val: 0,
expect: -1,
},
{
arr: []float64{11.1, 12.4},
val: 11.1,
expect: 0,
},
{
arr: []float64{11.1, 12.4},
val: 11.2,
expect: -1,
},
}
)
for _, tst := range tcc {
loc := find(tst.arr, tst.val)
req.Equal(tst.expect, loc)
}
}
func Test_count(t *testing.T) {
var (
req = require.New(t)
tcc = []fac{
{
arr: []string{"1", "2", "3"},
val: []string{"0", "3"},
expect: 1,
},
{
arr: []bool{true, true},
val: []bool{false, false},
expect: 0,
},
{
arr: []bool{true, true},
val: []bool{false, true},
expect: 1,
},
{
arr: []int{4, 5, 6, 7},
val: []int{7, 4},
expect: 2,
},
{
arr: []float64{11.1, 12.4},
val: []float64{0.1, 1.1},
expect: 0,
},
}
)
for _, tst := range tcc {
var loc int
switch reflect.TypeOf(tst.val).Elem().Kind() {
case reflect.String:
loc = count(tst.arr, tst.val.([]string)[0], tst.val.([]string)[1])
break
case reflect.Bool:
loc = count(tst.arr, tst.val.([]bool)[0], tst.val.([]bool)[1])
break
case reflect.Int:
loc = count(tst.arr, tst.val.([]int)[0], tst.val.([]int)[1])
break
case reflect.Float64:
loc = count(tst.arr, tst.val.([]float64)[0], tst.val.([]float64)[1])
break
}
req.Equal(tst.expect, loc)
}
}
func Test_has(t *testing.T) {
var (
req = require.New(t)
tcc = []fac{
{
arr: []string{"1", "2", "3"},
val: []string{"0", "3"},
expect: true,
},
{
arr: []bool{true, true},
val: []bool{false, false},
expect: false,
},
{
arr: []bool{true, true},
val: []bool{false, true},
expect: true,
},
{
arr: []int{4, 5, 6, 7},
val: []int{7, 4},
expect: true,
},
{
arr: []float64{11.1, 12.4},
val: []float64{0.1, 1.1},
expect: false,
},
}
)
for _, tst := range tcc {
var loc bool
switch reflect.TypeOf(tst.val).Elem().Kind() {
case reflect.String:
loc = has(tst.arr, tst.val.([]string)[0], tst.val.([]string)[1])
break
case reflect.Bool:
loc = has(tst.arr, tst.val.([]bool)[0], tst.val.([]bool)[1])
break
case reflect.Int:
loc = has(tst.arr, tst.val.([]int)[0], tst.val.([]int)[1])
break
case reflect.Float64:
loc = has(tst.arr, tst.val.([]float64)[0], tst.val.([]float64)[1])
break
}
req.Equal(tst.expect, loc)
}
}
func Test_hasAll(t *testing.T) {
var (
req = require.New(t)
tcc = []fac{
{
arr: []string{"1", "2", "3"},
val: []string{"0", "3"},
expect: false,
},
{
arr: []bool{true, true},
val: []bool{false, false},
expect: false,
},
{
arr: []bool{true, true},
val: []bool{false, true},
expect: false,
},
{
arr: []int{4, 5, 6, 7},
val: []int{7, 4},
expect: true,
},
{
arr: []float64{11.1, 12.4},
val: []float64{0.1, 1.1},
expect: false,
},
}
)
for _, tst := range tcc {
var loc bool
switch reflect.TypeOf(tst.arr).Elem().Kind() {
case reflect.String:
loc = hasAll(tst.arr, tst.val.([]string)[0], tst.val.([]string)[1])
break
case reflect.Bool:
loc = hasAll(tst.arr, tst.val.([]bool)[0], tst.val.([]bool)[1])
break
case reflect.Int:
loc = hasAll(tst.arr, tst.val.([]int)[0], tst.val.([]int)[1])
break
case reflect.Float64:
loc = hasAll(tst.arr, tst.val.([]float64)[0], tst.val.([]float64)[1])
break
}
req.Equal(tst.expect, loc)
}
}
func Test_slice(t *testing.T) {
type (
sct struct {
vals []int
arr interface{}
expect interface{}
}
)
var (
req = require.New(t)
tcc = []sct{
{
vals: []int{0, 3},
arr: []string{"1", "2", "3"},
expect: []string{"1", "2", "3"},
},
{
vals: []int{0, 1},
arr: []string{"1", "2", "3"},
expect: []string{"1"},
},
{
vals: []int{2, 3},
arr: []bool{true, true},
expect: []bool{true, true},
},
{
vals: []int{1, -1},
arr: []int{4, 5, 6, 7},
expect: []int{5, 6, 7},
},
{
vals: []int{3, -1},
arr: []float64{11.1, 12.4},
expect: []float64{11.1, 12.4},
},
}
)
for _, tst := range tcc {
new := slice(tst.arr, tst.vals[0], tst.vals[1])
req.Equal(tst.expect, new)
}
}
+62 -1
View File
@@ -1,13 +1,17 @@
package expr
import (
"github.com/PaesslerAG/gval"
"reflect"
"github.com/PaesslerAG/gval"
"github.com/davecgh/go-spew/spew"
)
func GenericFunctions() []gval.Language {
return []gval.Language{
gval.Function("coalesce", coalesce),
gval.Function("isEmpty", isEmpty),
gval.Function("isNil", isNil),
}
}
@@ -32,3 +36,60 @@ func isNil(i interface{}) bool {
return false
}
// empty checks values and slices
func isEmpty(i interface{}) bool {
if isNil(i) {
return true
}
spew.Dump("A", reflect.ValueOf(i).IsZero())
switch reflect.TypeOf(i).Kind() {
case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Map:
return reflect.ValueOf(i).Len() == 0
}
return reflect.ValueOf(i).IsZero()
}
func isInt(v interface{}) bool {
switch getKind(v) {
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint8:
return true
}
return false
}
func isString(v interface{}) bool {
return getKind(v) == reflect.String
}
func isFloat(v interface{}) bool {
switch getKind(v) {
case reflect.Float32, reflect.Float64:
return true
}
return false
}
func isBool(v interface{}) bool {
return getKind(v) == reflect.Bool
}
func getKind(v interface{}) reflect.Kind {
kind := reflect.TypeOf(v).Kind()
if isSlice(v) {
kind = reflect.TypeOf(v).Elem().Kind()
}
return kind
}
func isSlice(v interface{}) bool {
return reflect.TypeOf(v).Kind() == reflect.Slice
}
+93
View File
@@ -0,0 +1,93 @@
package expr
import (
"testing"
"github.com/stretchr/testify/require"
)
type (
tc struct {
value interface{}
expect interface{}
}
)
func Test_empty(t *testing.T) {
var (
req = require.New(t)
unsetSliceString []string
unsetSliceInt []int8
unsetSliceBool []bool
unsetSliceFloat []float32
unsetString string
unsetInt64 int64
tcc = []tc{
{
value: []string{},
expect: true,
},
{
value: map[string]string{},
expect: true,
},
{
value: unsetSliceString,
expect: true,
},
{
value: []int{},
expect: true,
},
{
value: []int{1},
expect: false,
},
{
value: unsetSliceInt,
expect: true,
},
{
value: unsetSliceBool,
expect: true,
},
{
value: int(1),
expect: false,
},
{
value: int(0),
expect: true,
},
{
value: "",
expect: true,
},
{
value: unsetString,
expect: true,
},
{
value: unsetSliceFloat,
expect: true,
},
{
value: unsetInt64,
expect: true,
},
{
value: []float32{11.1},
expect: false,
},
{
value: []float32{},
expect: true,
},
}
)
for _, tst := range tcc {
req.Equal(tst.expect, isEmpty(tst.value))
}
}
+42 -1
View File
@@ -1,8 +1,9 @@
package expr
import (
"github.com/PaesslerAG/gval"
"math"
"github.com/PaesslerAG/gval"
)
func NumericFunctions() []gval.Language {
@@ -12,6 +13,12 @@ func NumericFunctions() []gval.Language {
gval.Function("round", round),
gval.Function("floor", floor),
gval.Function("ceil", ceil),
gval.Function("abs", math.Abs),
gval.Function("log", math.Log10),
gval.Function("pow", math.Pow),
gval.Function("sqrt", math.Sqrt),
gval.Function("sum", sum),
gval.Function("average", average),
}
}
@@ -71,3 +78,37 @@ func floor(f float64) float64 {
func ceil(f float64) float64 {
return math.Ceil(f)
}
func sum(v ...interface{}) float64 {
var sum float64
for _, vv := range v {
c, err := CastToFloat(vv)
if err != nil {
continue
}
sum += c
}
return sum
}
func average(v ...interface{}) float64 {
var i int
var j, sum float64
for ; i < len(v); i++ {
c, err := CastToFloat(v[i])
if err != nil {
continue
}
j++
sum += c
}
return sum / j
}
+51 -9
View File
@@ -3,34 +3,76 @@ package expr
func Example_min() {
eval(`min(2, 1, 3)`, nil)
// output
// float64(1)
// output:
// 1
}
func Example_max() {
eval(`max(2, 1, 3)`, nil)
// output
// float64(3)
// output:
// 3
}
func Example_round() {
eval(`round(3.14,1)`, nil)
// output
// output:
// 3.1
}
func Example_floor() {
eval(`floor(3.14)`, nil)
// output
// float64(3)
// output:
// 3
}
func Example_ceil() {
eval(`ceil(3.14)`, nil)
// output
// float64(4)
// output:
// 4
}
func Example_abs() {
eval(`abs(-3.14)`, nil)
// output:
// 3.14
}
func Example_log() {
eval(`log(100)`, nil)
// output:
// 2
}
func Example_ln() {
eval(`pow(2, 3)`, nil)
// output:
// 8
}
func Example_sqrt() {
eval(`sqrt(16)`, nil)
// output:
// 4
}
func Example_sum() {
eval(`sum(1, 2, "foo", 3.4, 4, "3")`, 10.4)
// output:
// 13.4
}
func Example_average() {
eval(`average(1, 2, 3, "foo", 10)`, 10.4)
// output:
// 4
}
+101
View File
@@ -2,9 +2,11 @@ package expr
import (
"fmt"
"regexp"
"strings"
"github.com/PaesslerAG/gval"
valid "github.com/asaskevich/govalidator"
)
func StringFunctions() []gval.Language {
@@ -18,6 +20,21 @@ func StringFunctions() []gval.Language {
gval.Function("shortest", shortest),
gval.Function("longest", longest),
gval.Function("format", fmt.Sprintf),
gval.Function("title", title),
gval.Function("untitle", untitle),
gval.Function("repeat", strings.Repeat),
gval.Function("replace", strings.Replace),
gval.Function("isUrl", valid.IsURL),
gval.Function("isEmail", valid.IsEmail),
gval.Function("split", strings.Split),
gval.Function("join", strings.Join),
gval.Function("hasSubstring", hasSubstring),
gval.Function("substring", substring),
gval.Function("hasPrefix", strings.HasPrefix),
gval.Function("hasSuffix", strings.HasSuffix),
gval.Function("shorten", shorten),
gval.Function("camelize", camelize),
gval.Function("snakify", snakify),
}
}
@@ -44,3 +61,87 @@ func longest(f string, aa ...string) string {
func length(s string) int {
return len(s)
}
// title works similarly as strings.ToTitle, with the expception
// of uppercasing only the first word in line
func title(s string) string {
split := strings.Split(s, " ")
return fmt.Sprintf("%s %s", strings.Title(split[0]), strings.Join(split[1:], " "))
}
// untitle works only on the first word in line
func untitle(s string) string {
split := strings.Split(s, " ")
first := strings.ToLower(split[0][:1]) + split[0][1:]
return fmt.Sprintf("%s %s", first, strings.Join(split[1:], " "))
}
// hasSubstring checks if a substring exists in original string
// use watchCase if need case sensitivity
func hasSubstring(s, substring string, watchCase bool) bool {
if watchCase {
return strings.Contains(s, substring)
}
return strings.Contains(strings.ToLower(s), strings.ToLower(substring))
}
// substring extracts a substring from original string
// specifying end value will not match till end of string
func substring(s string, start, end int) string {
if end == -1 {
end = len(s)
}
if start >= len(s) {
return ""
}
end++
if end > len(s) {
end = len(s)
}
return s[start:end]
}
// shorten trims by whole words or only chars by
// the specified amount
func shorten(s, t string, count int) string {
var joined string
if t == "char" {
if count > len(s) {
return ""
}
joined = s[:count]
} else {
fields := strings.Fields(s)
if len(fields) == 0 {
return ""
}
joined = strings.Join(fields[:count], " ")
}
reg, err := regexp.Compile("[^a-zA-Z0-9]$")
if err != nil {
return ""
}
joined = reg.ReplaceAllString(joined, "")
return joined + " …"
}
func camelize(s string) string {
return untitle(strings.Replace(strings.Title(s), " ", "", -1))
}
func snakify(s string) string {
return strings.ToLower(strings.Replace(strings.Title(s), " ", "_", -1))
}
+147
View File
@@ -42,3 +42,150 @@ func Example_longest() {
// output:
// foobar
}
func Example_title() {
eval(`title("foo bAR")`, nil)
// output:
// Foo bAR
}
func Example_untitle() {
eval(`untitle("Foo Bar")`, nil)
// output:
// foo Bar
}
func Example_repeat() {
eval(`repeat("duran ", c)`, map[string]interface{}{"c": 2})
// output:
// duran duran
}
func Example_replace_all() {
eval(`replace("foobar baz", "ba", "BA", c)`, map[string]interface{}{"c": -1})
// output:
// fooBAr BAz
}
func Example_replace_first() {
eval(`replace("foobar baz", "ba", "BA", c)`, map[string]interface{}{"c": 1})
// output:
// fooBAr baz
}
func Example_isUrl() {
eval(`isUrl("http:/example.tld")`, nil)
// output:
// false
}
func Example_isEmail() {
eval(`isUrl("example.user+valid@example.tld")`, nil)
// output:
// true
}
func Example_split() {
eval(`split("This will be split in:2-parts", ":")`, nil)
// output:
// [This will be split in 2-parts]
}
func Example_join() {
eval(`join(exploded, ",")`, map[string][]string{"exploded": {"One", "two", "three"}})
// output:
// One,two,three
}
func Example_hasSubstring_caseS() {
eval(`hasSubstring("foo BAR", "o b", true)`, nil)
// output:
// false
}
func Example_hasSubstring_caseI() {
eval(`hasSubstring("foo BAR", "o b", false)`, nil)
// output:
// true
}
func Example_hasSuffix() {
eval(`hasSuffix("foo bar", "ar")`, nil)
// output:
// true
}
func Example_hasPrefix() {
eval(`hasPrefix("foo bar", "foo ")`, nil)
// output:
// true
}
func Example_shorten_word() {
eval(`shorten("foo bar 1337, this is. test one - or three", "word", c)`, map[string]int{"c": 3})
// output:
// foo bar 1337 …
}
func Example_shorten_char() {
eval(`shorten("foo bar 1337, this is. test one - or three", "char", c)`, map[string]int{"c": 22})
// output:
// foo bar 1337, this is …
}
func Example_camelize() {
eval(`camelize("foo bar baz_test")`, nil)
// output:
// fooBarBaz_test
}
func Example_snakify() {
eval(`snakify("foo bar baz_test")`, nil)
// output:
// foo_bar_baz_test
}
func Example_substring() {
eval(`substring("foo bar baz_test", start, end)`, map[string]interface{}{"start": 2, "end": -1})
// output:
// o bar baz_test
}
func Example_substring_highStart() {
eval(`substring("foo", start, end)`, map[string]interface{}{"start": 3, "end": -1})
// output:
//
}
func Example_substring_withEnd() {
eval(`substring("foo bar baz_test", start, end)`, map[string]interface{}{"start": 2, "end": 4})
// output:
// o b
}
func Example_substring_endOverflow() {
eval(`substring("foo bar baz_test", start, end)`, map[string]interface{}{"start": 2, "end": 100})
// output:
// o bar baz_test
}
+19
View File
@@ -15,9 +15,28 @@ func TimeFunctions() []gval.Language {
gval.Function("modTime", modTime),
gval.Function("parseDuration", time.ParseDuration),
gval.Function("strftime", strfTime),
gval.Function("isLeapYear", isLeapYear),
gval.Function("now", now),
gval.Function("isWeekDay", isWeekDay),
}
}
func now() time.Time {
return time.Now()
}
func isLeapYear(f time.Time) bool {
return time.Date(f.Year(), time.December, 31, 0, 0, 0, 0, time.Local).YearDay() == 366
}
func isLeapDay(f time.Time) bool {
return f.Day() == 29 && f.Month() == 2
}
func isWeekDay(f time.Time) bool {
return time.Sunday < f.Weekday() && f.Weekday() < time.Saturday
}
func earliest(f time.Time, aa ...time.Time) time.Time {
for _, s := range aa {
if s.Before(f) {
+18 -1
View File
@@ -1,6 +1,8 @@
package expr
import "time"
import (
"time"
)
var (
@@ -17,6 +19,7 @@ var (
"hgp": hgp,
"wfa": wfa,
"ghd": ghd,
"now": now,
}
)
@@ -68,3 +71,17 @@ func Example_latest() {
// output:
// 1993-02-02 06:00:00 -0500 -0500
}
func Example_isLeapYear() {
eval(`isLeapYear(ghd)`, exampleTimeParams)
// output:
// false
}
func Example_isWeekDay() {
eval(`isWeekDay(now)`, exampleTimeParams)
// output:
// true
}
+2
View File
@@ -3,6 +3,7 @@ package expr
import (
"context"
"fmt"
"github.com/PaesslerAG/gval"
)
@@ -84,6 +85,7 @@ func AllFunctions() []gval.Language {
ff = append(ff, StringFunctions()...)
ff = append(ff, NumericFunctions()...)
ff = append(ff, TimeFunctions()...)
ff = append(ff, ArrayFunctions()...)
return ff
}