wstl1/mapping_engine/builtins/builtins.go (634 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package builtins contains function definitions and implementation for built-in mapping functions. package builtins import ( "encoding/hex" "errors" "fmt" "hash/fnv" "math" "regexp" "sort" "strconv" "strings" "time" "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/util/jsonutil" /* copybara-comment: jsonutil */ "github.com/google/go-cmp/cmp" /* copybara-comment: cmp */ "bitbucket.org/creachadair/stringset" /* copybara-comment: stringset */ "github.com/google/uuid" /* copybara-comment: uuid */ ) // When adding a built-in, remember to add it to the map below with its name as the key. var BuiltinFunctions = map[string]interface{}{ // Arithmetic "$Div": Div, "$Mod": Mod, "$Mul": Mul, "$Sub": Sub, "$Sum": Sum, // Collections "$Flatten": Flatten, "$ListCat": ListCat, "$ListLen": ListLen, "$ListOf": ListOf, "$SortAndTakeTop": SortAndTakeTop, "$Range": Range, "$UnionBy": UnionBy, "$Unique": Unique, "$UnnestArrays": UnnestArrays, // Date/Time "$CurrentTime": CurrentTime, "$MultiFormatParseTime": MultiFormatParseTime, "$ParseTime": ParseTime, "$ParseUnixTime": ParseUnixTime, "$ReformatTime": ReformatTime, "$SplitTime": SplitTime, // Data operations "$Hash": Hash, "$IntHash": IntHash, "$IsNil": IsNil, "$IsNotNil": IsNotNil, "$MergeJSON": MergeJSON, "$UUID": UUID, "$Type": Type, // Debugging "$DebugString": DebugString, "$Void": Void, // Logic "$And": And, "$Eq": Eq, "$Gt": Gt, "$GtEq": GtEq, "$Lt": Lt, "$LtEq": LtEq, "$NEq": NEq, "$Not": Not, "$Or": Or, // Strings "$MatchesRegex": MatchesRegex, "$ParseFloat": ParseFloat, "$ParseInt": ParseInt, "$SubStr": SubStr, "$StrCat": StrCat, "$StrFmt": StrFmt, "$StrJoin": StrJoin, "$StrSplit": StrSplit, "$ToLower": ToLower, "$ToUpper": ToUpper, "$Trim": Trim, } // Now is exported to allow for mocking in tests. var Now = time.Now const ( defaultTimeFormat = "2006-01-02 03:04:05" pythonStyleDateTime = 0 goStyleDateTime = 1 ) // need to put more complicated formatting first and the substrings it contains after otherwise the longer string only gets partially translated var ( timeTokenMap = [...][2]string{ {"%c", "Mon Jan 2 15:04:05 2006"}, // Python: Locale’s appropriate date and time representation. {"%x", "01/02/06"}, // Python: Locale’s appropriate date representation. {"%X", "15:04:05"}, // Python: Locale’s appropriate time representation. {"%A", "Monday"}, // Python: Locale’s full weekday name. {"%a", "Mon"}, // Python: Locale’s abbreviated weekday name. {"%B", "January"}, // Python: Locale’s full month name. {"%b", "Jan"}, // Python: Locale’s abbreviated month name. {"%Y", "2006"}, // Python: Year with century as a decimal number. {"%d", "02"}, // Python: Day of the month as a decimal number [01,31]. {"%e", "2"}, // Google Cloud SQL: The day of month as a decimal number (1-31). {"%H", "15"}, // Python: Hour (24-hour clock) as a decimal number [00,23]. {"%I", "03"}, // Python: Hour (12-hour clock) as a decimal number [01,12]. {"%i", "3"}, // ADDED: 12H hour representation without padding {"%m", "01"}, // Python: Month as a decimal number [01,12]. {"%M", "04"}, // Python: Minute as a decimal number [00,59]. {"%p", "PM"}, // Python: Locale’s equivalent of either AM or PM. {"%S", "05"}, // Python: Second as a decimal number [00,60]. {"%s", "5"}, // ADDED: second as a decimal number without padding [0,60] {"%y", "06"}, // Python: Year without century as a decimal number [00,99]. {"%Z", "MST"}, // Python: Time zone name (no characters if no time zone exists). {"%z", "-07:00"}, // Python: Time zone offset indicating a positive or negative time difference from UTC/GMT {"%z", "-0700"}, {"%z", "-07"}, } pythonFormatRegex *regexp.Regexp ) func init() { precompilePythonTimeFormat() } // precompilePythonTimeFormat precompile the regex for python formatting string func precompilePythonTimeFormat() { var tokens [len(timeTokenMap)]string for i := 0; i < len(timeTokenMap); i++ { tokens[i] = timeTokenMap[i][0][1:] } pythonFormatRegex, _ = regexp.Compile(fmt.Sprintf("^(.{0}|(.*%%[%s].*))+$", strings.Join(tokens[:], ""))) } // Although arguments and types can vary, all projectors, including built-ins must return // (jsonutil.JSONToken, error). The first return value can be any type assignable to // jsonutil.JSONToken. For predicates that must return a boolean (jsonutil.JSONBool), the type // will be checked/enforced at runtime. // Div divides the first argument by the second. func Div(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONNum, error) { return left / right, nil } // Mod returns the remainder of dividing the first argument by the second. func Mod(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONNum, error) { res := math.Mod(float64(left), float64(right)) if math.IsNaN(res) { return -1, errors.New("modulo operation returned NaN") } return jsonutil.JSONNum(res), nil } // Mul multiplies together all given arguments. Returns 0 if nothing given. func Mul(operands ...jsonutil.JSONNum) (jsonutil.JSONNum, error) { if len(operands) == 0 { return 0, nil } var res jsonutil.JSONNum = 1 for _, n := range operands { res *= n } return res, nil } // Sub subtracts the second argument from the first. func Sub(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONNum, error) { return left - right, nil } // Sum adds up all given values. func Sum(operands ...jsonutil.JSONNum) (jsonutil.JSONNum, error) { var res jsonutil.JSONNum for _, n := range operands { res += n } return res, nil } // Unique returns the unique elements in the array by comparing their hashes. func Unique(array jsonutil.JSONArr) (jsonutil.JSONArr, error) { arr := make(jsonutil.JSONArr, 0) set := make(map[jsonutil.JSONStr]bool) for _, i := range array { hash, err := Hash(i) if err != nil { return nil, err } if !set[hash] { arr = append(arr, i) set[hash] = true } } return arr, nil } // Flatten turns a nested array of arrays (of any depth) into a single array. // Item ordering is preserved, depth first. func Flatten(array jsonutil.JSONArr) (jsonutil.JSONArr, error) { // This needs to always return an empty array, not a nil value. Nil values // may cause NPE down the line. res := make(jsonutil.JSONArr, 0) for _, item := range array { if subArr, ok := item.(jsonutil.JSONArr); ok { flat, err := Flatten(subArr) if err != nil { return nil, err } res = append(res, flat...) } else { res = append(res, item) } } return res, nil } // ListCat concatenates all given arrays into one array. func ListCat(args ...jsonutil.JSONArr) (jsonutil.JSONArr, error) { if len(args) == 0 { return jsonutil.JSONArr{}, nil } if len(args) == 1 { return args[0], nil } var cat jsonutil.JSONArr for _, a := range args { cat = append(cat, a...) } return cat, nil } // ListLen finds the length of the array. func ListLen(in jsonutil.JSONArr) (jsonutil.JSONNum, error) { return jsonutil.JSONNum(len(in)), nil } // ListOf creates a list of the given tokens. func ListOf(args ...jsonutil.JSONToken) (jsonutil.JSONArr, error) { return jsonutil.JSONArr(args), nil } // SortAndTakeTop sorts the elements in the array by the key in the specified direction and returns the top element. func SortAndTakeTop(arr jsonutil.JSONArr, key jsonutil.JSONStr, desc jsonutil.JSONBool) (jsonutil.JSONToken, error) { if len(arr) == 0 { return nil, nil } if len(arr) == 1 { return arr[0], nil } tm := map[string]jsonutil.JSONToken{} var keys []string for _, t := range arr { k, err := jsonutil.GetField(t, string(key)) if err != nil { return nil, err } kstr := fmt.Sprintf("%v", k) tm[kstr] = t keys = append(keys, kstr) } sort.Strings(keys) if desc { return tm[keys[len(keys)-1]], nil } return tm[keys[0]], nil } // Range generates an array of sequentially ordered number from start (inclusive) to end (exclusive) by a step of 1. // Example: // $Range(2, 5) returns: [2, 3, 4] // $Range(5, 2) returns: [5, 4, 3] // $Range(-2, 1) returns: [-2, -1, 0] func Range(start jsonutil.JSONNum, end jsonutil.JSONNum) (jsonutil.JSONArr, error) { result := make(jsonutil.JSONArr, 0) var increment = (start <= end) var i = start for (increment && i < end) || (!increment && i > end) { result = append(result, jsonutil.JSONNum(i)) if increment { i++ } else { i-- } } return result, nil } // UnionBy unions the items in the given array by the given keys, such that each item // in the resulting array has a unique combination of those keys. The first unique element // is picked when deduplicating. The items in the resulting array are ordered // deterministically (i.e unioning of array [x, y, z] and array [z, x, x, y], both return // [x, y, z]). // // E.g: // Arguments: items: `[{"id": 1}, {"id": 2}, {"id": 1, "foo": "hello"}]`, keys: "id" // Return: [{"id": 1}, {"id": 2}] func UnionBy(items jsonutil.JSONArr, keys ...jsonutil.JSONStr) (jsonutil.JSONArr, error) { set := make(map[jsonutil.JSONStr]jsonutil.JSONToken) var orderedKeys []jsonutil.JSONStr for _, i := range items { var key jsonutil.JSONStr for _, k := range keys { v, err := jsonutil.GetField(i, string(k)) if err != nil { return nil, err } h, err := Hash(v) if err != nil { return nil, err } key += h } if _, ok := set[key]; !ok { orderedKeys = append(orderedKeys, key) set[key] = i } } sort.Slice(orderedKeys, func(i int, j int) bool { return orderedKeys[i] < orderedKeys[j] }) var arr jsonutil.JSONArr for _, k := range orderedKeys { arr = append(arr, set[k]) } return arr, nil } // UnnestArrays takes a json object with nested arrays (e.g.: {"key1": [{}...], "key2": {}}) // and returns an unnested array that contains the top level key in the "k" field and each // array element, unnested, in the "v" field (e.g.: [{"k": "key1", "v": {}} ...]). // If the value of a key is an object, it simply returns that object. The // output is sorted by the keys, and the array ordering is preserved. // If the nested array is empty, the key is ignored. // // E.g: // c: `{"key1":[{"a": "z"}, {"b": "y"}], "key2": {"c": "x"}, "key3": []} // return: [{"k": "key1", "v":{"a": "z"}}`, {"k": "key1", "v":{"b": "y"}}, {"k": "key2", "v":{"c": "x"}}] func UnnestArrays(c jsonutil.JSONContainer) (jsonutil.JSONArr, error) { var out jsonutil.JSONArr var keys []string for k := range c { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { var kstr jsonutil.JSONToken = jsonutil.JSONStr(k) arr, ok := (*c[k]).(jsonutil.JSONArr) if !ok { kv := jsonutil.JSONContainer{ "k": &kstr, "v": c[k], } out = append(out, kv) continue } for _, i := range arr { vTkn := i kv := jsonutil.JSONContainer{ "k": &kstr, "v": &vTkn, } out = append(out, kv) } } return out, nil } // CurrentTime returns the current time based on the Go func time.Now // (https://golang.org/pkg/time/#Now). The function accepts a time format layout // (https://golang.org/pkg/time/#Time.Format) and an IANA formatted time zone // string (https://www.iana.org/time-zones). A string representing the current // time is returned. A default layout of '2006-01-02 03:04:05'and a default // time zone of 'UTC' will be used if not provided. func CurrentTime(format jsonutil.JSONStr, tz jsonutil.JSONStr) (jsonutil.JSONStr, error) { if len(format) == 0 { format = defaultTimeFormat } tm := Now().UTC() loc, err := time.LoadLocation(string(tz)) if err != nil { return jsonutil.JSONStr(""), err } outputTime := tm.In(loc).Format(string(format)) return jsonutil.JSONStr(outputTime), nil } // MultiFormatParseTime converts the time in the specified formats to RFC3339 (https://www.ietf.org/rfc/rfc3339.txt) format. It tries the formats in order and returns an error if none of the formats match. // The function accepts a go time format layout (https://golang.org/pkg/time/#Time.Format) or Python time format layout (defined in timeTokenMap) func MultiFormatParseTime(format jsonutil.JSONArr, date jsonutil.JSONStr) (jsonutil.JSONStr, error) { for _, f := range format { s, ok := f.(jsonutil.JSONStr) if !ok { return jsonutil.JSONStr(""), fmt.Errorf("expected array of strings instead of %v", format) } t, err := ParseTime(s, date) if err == nil { return t, nil } } return jsonutil.JSONStr(""), fmt.Errorf("no date formats(%v) matched %v", format, date) } // ParseTime converts the time in the specified format to RFC3339 (https://www.ietf.org/rfc/rfc3339.txt) format. // The function accepts a go time format layout (https://golang.org/pkg/time/#Time.Format) or Python time format layout (defined in timeTokenMap) func ParseTime(format jsonutil.JSONStr, date jsonutil.JSONStr) (jsonutil.JSONStr, error) { return ReformatTime(format, date, time.RFC3339Nano) } func parseTime(format, date jsonutil.JSONStr) (time.Time, error) { if len(date) == 0 { return time.Time{}, nil } format = convertTimeFormatToGo(format) isoDate, err := time.Parse(string(format), string(date)) if err != nil { return time.Time{}, err } return isoDate, nil } // isPythonTimeFormat returns true iff the format string contains python formatting string. func isPythonTimeFormat(format jsonutil.JSONStr) bool { return pythonFormatRegex.MatchString(string(format)) } // convertTimeFormatToGo converts input DateTime formatting string to GO DateTime formatting string if it's in Python format. func convertTimeFormatToGo(inFormat jsonutil.JSONStr) jsonutil.JSONStr { if isPythonTimeFormat(inFormat) { return convertTimeFormat(inFormat, pythonStyleDateTime, goStyleDateTime) } return inFormat } // convertTimeFormatGoToPython translates GO DateTime formatting string to Python DateTime formatting string. func convertTimeFormatGoToPython(goFormat jsonutil.JSONStr) (jsonutil.JSONStr, error) { if len(string(goFormat)) == 0 { return jsonutil.JSONStr(""), fmt.Errorf("the input goFormat cannot be empty") } pyFormat := convertTimeFormat(goFormat, goStyleDateTime, pythonStyleDateTime) isValid := isPythonTimeFormat(pyFormat) if !isValid { return jsonutil.JSONStr(""), fmt.Errorf("fail to convert the GO formatting string to valid python formatting string") } return pyFormat, nil } // ConvertTimeFormat converts a fomatting string in inStyle to that in outStyle, where inStyle and outStyle are defined in const func convertTimeFormat(inFormat jsonutil.JSONStr, inStyle, outStyle int) jsonutil.JSONStr { result := []byte(string(inFormat)) for _, tokenPair := range timeTokenMap { inToken := tokenPair[inStyle] outToken := tokenPair[outStyle] re := regexp.MustCompile(inToken) result = re.ReplaceAll(result, []byte(outToken)) } return jsonutil.JSONStr(string(result)) } // ParseUnixTime parses a unit and a unix timestamp into the speficied format. // The function accepts a go time format layout (https://golang.org/pkg/time/#Time.Format) and Python time format layout (defined in timeTokenMap) func ParseUnixTime(unit jsonutil.JSONStr, ts jsonutil.JSONNum, format jsonutil.JSONStr, tz jsonutil.JSONStr) (jsonutil.JSONStr, error) { sec := int64(ts) ns := int64(0) switch strings.ToLower(string(unit)) { case "s": // Do nothing. case "ms": ns = sec * int64(time.Millisecond) sec = 0 case "us": ns = sec * int64(time.Microsecond) sec = 0 case "ns": ns = sec sec = 0 default: return jsonutil.JSONStr(""), fmt.Errorf("unsupported unit %v, supported units are s, ms, us, ns", unit) } tm := time.Unix(sec, ns) loc, err := time.LoadLocation(string(tz)) if err != nil { return jsonutil.JSONStr(""), fmt.Errorf("unsupported timezone %v", tz) } tm = tm.In(loc) format = convertTimeFormatToGo(format) return jsonutil.JSONStr(tm.Format(string(format))), nil } // ReformatTime uses a Go or Python time-format to convert date into another Go or Python time-formatted date time. func ReformatTime(inFormat, date, outFormat jsonutil.JSONStr) (jsonutil.JSONStr, error) { if len(string(inFormat)) == 0 { return jsonutil.JSONStr(""), fmt.Errorf("inFormat string cannot be empty") } if len(string(outFormat)) == 0 { return jsonutil.JSONStr(""), fmt.Errorf("outFormat string cannot be empty") } inFormat = convertTimeFormatToGo(inFormat) outFormat = convertTimeFormatToGo(outFormat) isoDate, err := parseTime(inFormat, date) if err != nil { return jsonutil.JSONStr(""), err } if isoDate.IsZero() { return jsonutil.JSONStr(""), nil } return jsonutil.JSONStr(isoDate.Format(string(outFormat))), nil } // SplitTime splits a time string into components based on the Go // (https://golang.org/pkg/time/#Time.Format) and Python time-format provided. // An array with all components (year, month, day, hour, minute, second and // nanosecond) will be returned. func SplitTime(format jsonutil.JSONStr, date jsonutil.JSONStr) (jsonutil.JSONArr, error) { d, err := parseTime(format, date) if err != nil { return jsonutil.JSONArr([]jsonutil.JSONToken{}), err } c := []jsonutil.JSONToken{ jsonutil.JSONStr(strconv.Itoa(d.Year())), jsonutil.JSONStr(strconv.Itoa(int(d.Month()))), jsonutil.JSONStr(strconv.Itoa(d.Day())), jsonutil.JSONStr(strconv.Itoa(d.Hour())), jsonutil.JSONStr(strconv.Itoa(d.Minute())), jsonutil.JSONStr(strconv.Itoa(d.Second())), jsonutil.JSONStr(strconv.Itoa(d.Nanosecond())), } return jsonutil.JSONArr(c), nil } // Hash converts the given item into a hash. Key order is not considered (array item order is). // This is not cryptographically secure, and is not to be used for secure hashing. func Hash(obj jsonutil.JSONToken) (jsonutil.JSONStr, error) { h, err := jsonutil.Hash(obj, false) if err != nil { return "", err } return jsonutil.JSONStr(hex.EncodeToString(h)), nil } // IntHash converts the given item into a integer hash. Key order is not considered (array item order is). // This is not cryptographically secure, and is not to be used for secure hashing. func IntHash(obj jsonutil.JSONToken) (jsonutil.JSONNum, error) { h, err := jsonutil.Hash(obj, false) if err != nil { return -1, err } h32 := fnv.New32a() if _, err := h32.Write(h); err != nil { return -1, err } return jsonutil.JSONNum(h32.Sum32()), nil } // IsNil returns true iff the given object is nil or empty. func IsNil(object jsonutil.JSONToken) (jsonutil.JSONBool, error) { switch t := object.(type) { case jsonutil.JSONStr: return len(t) == 0, nil case jsonutil.JSONArr: return len(t) == 0, nil case jsonutil.JSONContainer: return len(t) == 0, nil case nil: return true, nil } return false, nil } // IsNotNil returns true iff the given object is not nil or empty. func IsNotNil(object jsonutil.JSONToken) (jsonutil.JSONBool, error) { isNil, err := IsNil(object) return !isNil, err } // MergeJSON merges the elements in the JSONArr into one JSON object by repeatedly calling the merge // function. The merge function overwrites single fields and concatenates array fields (unless // overwriteArrays is true, in which case arrays are overwritten). func MergeJSON(arr jsonutil.JSONArr, overwriteArrays jsonutil.JSONBool) (jsonutil.JSONToken, error) { var out jsonutil.JSONToken for _, t := range arr { if out == nil { out = jsonutil.Deepcopy(t) continue } err := jsonutil.Merge(t, &out, false, bool(overwriteArrays)) if err != nil { return nil, err } } return out, nil } // UUID generates a RFC4122 (https://tools.ietf.org/html/rfc4122) UUID. func UUID() (jsonutil.JSONStr, error) { return jsonutil.JSONStr(uuid.New().String()), nil } // Type returns the type of the given JSON Token as a string. func Type(object jsonutil.JSONToken) (jsonutil.JSONStr, error) { switch object.(type) { case jsonutil.JSONNum: return jsonutil.JSONStr("number"), nil case jsonutil.JSONBool: return jsonutil.JSONStr("bool"), nil case jsonutil.JSONStr: return jsonutil.JSONStr("string"), nil case jsonutil.JSONArr: return jsonutil.JSONStr("array"), nil case jsonutil.JSONContainer: return jsonutil.JSONStr("container"), nil case nil: return jsonutil.JSONStr("null"), nil } return jsonutil.JSONStr(""), fmt.Errorf("Unrecognized JSON token type: %T", object) } // DebugString converts the JSON element to a string representation by // recursively converting objects to strings. func DebugString(t jsonutil.JSONToken) (jsonutil.JSONStr, error) { return jsonutil.JSONStr(fmt.Sprintf("%v", t)), nil } // Void returns nil given any inputs. You non-nil into the Void, the Void nils back. func Void(unused ...jsonutil.JSONToken) (jsonutil.JSONToken, error) { return nil, nil } // And is a logical AND of all given arguments. func And(args ...jsonutil.JSONToken) (jsonutil.JSONBool, error) { if len(args) == 0 { return false, nil } for _, a := range args { notA, _ := Not(a) if notA { return false, nil } } return true, nil } // Eq returns true iff all given arguments are equal. func Eq(args ...jsonutil.JSONToken) (jsonutil.JSONBool, error) { if len(args) < 2 { return true, nil } for _, arg := range args[1:] { if !cmp.Equal(arg, args[0]) { return false, nil } } return true, nil } // Gt returns true iff the first argument is greater than the second. func Gt(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONBool, error) { return left > right, nil } // GtEq returns true iff the first argument is greater than or equal to the second. func GtEq(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONBool, error) { return left >= right, nil } // Lt returns true iff the first argument is less than the second. func Lt(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONBool, error) { return left < right, nil } // LtEq returns true iff the first argument is less than or equal to the second. func LtEq(left jsonutil.JSONNum, right jsonutil.JSONNum) (jsonutil.JSONBool, error) { return left <= right, nil } // NEq returns true iff all given arguments are different. func NEq(args ...jsonutil.JSONToken) (jsonutil.JSONBool, error) { if len(args) < 2 { return true, nil } hashSet := stringset.NewSize(len(args)) for _, a := range args { h, err := Hash(a) if err != nil { return false, err } if !hashSet.Add(string(h)) { return false, nil } } return true, nil } // Not returns true iff the given value is false. func Not(object jsonutil.JSONToken) (jsonutil.JSONBool, error) { result, ok := object.(jsonutil.JSONBool) if !ok { return IsNil(object) } return !result, nil } // Or is a logical OR of all given arguments. func Or(args ...jsonutil.JSONToken) (jsonutil.JSONBool, error) { for _, a := range args { boolVal, ok := a.(jsonutil.JSONBool) if !ok { boolVal, _ = IsNotNil(a) } if boolVal { return true, nil } } return false, nil } // MatchesRegex returns true iff the string matches the regex pattern. func MatchesRegex(str jsonutil.JSONStr, regex jsonutil.JSONStr) (jsonutil.JSONBool, error) { // TODO(): Consider compiling and caching these regexes. m, err := regexp.MatchString(string(regex), string(str)) return jsonutil.JSONBool(m), err } // ParseFloat parses a string into a float. func ParseFloat(str jsonutil.JSONStr) (jsonutil.JSONNum, error) { f, err := strconv.ParseFloat(string(str), 64) if err != nil { return 0, err } return jsonutil.JSONNum(f), nil } // ParseInt parses a string into an int. func ParseInt(str jsonutil.JSONStr) (jsonutil.JSONNum, error) { i, err := strconv.Atoi(string(str)) if err != nil { return -1, err } return jsonutil.JSONNum(i), nil } // SubStr returns a part of the string that is between the start index (inclusive) and the // end index (exclusive). If the end index is greater than the length of the string, the end // index is truncated to the length. func SubStr(str jsonutil.JSONStr, start, end jsonutil.JSONNum) (jsonutil.JSONStr, error) { e := int(end) l := len(str) if e > l { e = l } if int(start) > l { return jsonutil.JSONStr(""), fmt.Errorf("start index %v is greater string length %v", start, l) } return jsonutil.JSONStr(string(str)[int(start):e]), nil } // StrCat joins the input strings with the separator. func StrCat(args ...jsonutil.JSONToken) (jsonutil.JSONStr, error) { return StrJoin(jsonutil.JSONStr(""), args...) } // StrFmt formats the given item using the given Go format specifier (https://golang.org/pkg/fmt/). func StrFmt(format jsonutil.JSONStr, item jsonutil.JSONToken) (jsonutil.JSONStr, error) { // This cast avoids formatting issues with numbers (since JSONNum is not detected as a number by the formatter) if numItem, ok := item.(jsonutil.JSONNum); ok { fmtSpec := format[strings.Index(string(format), "%")+1] if strings.Contains("bcdoqxXU", string(fmtSpec)) { return jsonutil.JSONStr(fmt.Sprintf(string(format), int(numItem))), nil } return jsonutil.JSONStr(fmt.Sprintf(string(format), float64(numItem))), nil } return jsonutil.JSONStr(fmt.Sprintf(string(format), item)), nil } // StrJoin joins the inputs together and adds the separator between them. Non-string arguments // are converted to strings before joining. func StrJoin(sep jsonutil.JSONStr, args ...jsonutil.JSONToken) (jsonutil.JSONStr, error) { var o []string for _, token := range args { if token != nil { o = append(o, fmt.Sprintf("%v", token)) } } return jsonutil.JSONStr(strings.Join(o, string(sep))), nil } // StrSplit splits a string by the separator and ignores empty entries. func StrSplit(str jsonutil.JSONStr, sep jsonutil.JSONStr) (jsonutil.JSONArr, error) { outs := strings.Split(string(str), string(sep)) var res jsonutil.JSONArr for _, out := range outs { val := strings.TrimSpace(out) if len(val) == 0 { continue } res = append(res, jsonutil.JSONStr(val)) } return res, nil } // ToLower converts the given string with all unicode characters mapped to their lowercase. func ToLower(str jsonutil.JSONStr) (jsonutil.JSONStr, error) { return jsonutil.JSONStr(strings.ToLower(string(str))), nil } // ToUpper converts the given string with all unicode characters mapped to their uppercase. func ToUpper(str jsonutil.JSONStr) (jsonutil.JSONStr, error) { return jsonutil.JSONStr(strings.ToUpper(string(str))), nil } // Trim strips the leading and trailing whitespace of the input string. func Trim(str jsonutil.JSONStr) (jsonutil.JSONStr, error) { return jsonutil.JSONStr(strings.TrimSpace(string(str))), nil }