Repository: brimsec/zq (go)

Issue:
Z function for joining array contents into strings
The Z equivalent of the Python:

```
$ python
>>> '.'.join(['www', 'brimsecurity', 'com'])
'www.brimsecurity.com'
```


Pull request description:
add join/split string functions
This commit adds functions for join() and split() with respect to strings.
Since Z is dynamically typed, this functions may do more down the road 
based on the types of their arguments, but for now they are just string
functions.

Closes #2089, #2090 


Patch:
diff --git a/README.md b/README.md
index bdbe7d7e4..660aca214 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# `zq` [![Tests][tests-img]][tests] [![GoDoc][doc-img]][doc]
+# `zq` [![Tests][tests-img]][tests]
 
 The `zq` repository contains tools and components used to search, analyze,
 and store structured log data, including:
@@ -42,8 +42,6 @@ See the [contributing guide](CONTRIBUTING.md) on how you can help improve `zq`!
 
 Join our [Public Slack](https://www.brimsecurity.com/join-slack/) workspace for announcements, Q&A, and to trade tips!
 
-[doc-img]: https://godoc.org/github.com/brimsec/zq?status.svg
-[doc]: https://pkg.go.dev/github.com/brimsec/zq
 [tests-img]: https://github.com/brimsec/zq/workflows/Tests/badge.svg
 [tests]: https://github.com/brimsec/zq/actions?query=workflow%3ATests
 
diff --git a/expr/function/function.go b/expr/function/function.go
index 350fee025..fdedb5853 100644
--- a/expr/function/function.go
+++ b/expr/function/function.go
@@ -22,50 +22,7 @@ type Interface interface {
 	Call([]zng.Value) (zng.Value, error)
 }
 
-var deprecated = map[string]string{
-	"Math.abs":              "abs",
-	"Math.ceil":             "ceil",
-	"Math.floor":            "floor",
-	"Math.log":              "log",
-	"Math.max":              "max",
-	"Math.min":              "min",
-	"Math.mod":              "mod",
-	"Math.round":            "round",
-	"Math.pow":              "pow",
-	"Math.sqrt":             "sqrt",
-	"String.byteLen":        "len",
-	"String.formatFloat":    "type cast, e.g., <float-value>:string",
-	"String.formatInt":      "type cast, e.g., <int-value>:string",
-	"String.formatIp":       "type cast, e.g., <ip-value>:string",
-	"String.parseFloat":     "type cast, e.g., <string-value>:float64",
-	"String.parseInt":       "type cast, e.g., <string-value>:int64",
-	"String.parseIp":        "type cast, e.g., <string-value>:ip",
-	"String.replace":        "replace",
-	"String.runeLen":        "rune_len",
-	"String.toLower":        "to_lower",
-	"String.toUpper":        "to_upper",
-	"String.trim":           "trim",
-	"Time.fromISO":          "iso",
-	"Time.fromMilliseconds": "msec and ype cast, e.g., msec(<msec-value>):time",
-	"Time.fromMicroseconds": "usec and type cast, e.g., usec(<usec-value>):time",
-	"Time.fromNanoseconds":  "type cast, e.g., <nsec-value>:time",
-	"Time.trunc":            "trunc",
-	"toBase64":              "to_base64",
-	"fromBase64":            "from_base64",
-}
-
-func isDeprecated(name string) error {
-	msg, ok := deprecated[name]
-	if ok {
-		return fmt.Errorf("function is deprecated: use %s", msg)
-	}
-	return nil
-}
-
 func New(zctx *resolver.Context, name string, narg int) (Interface, error) {
-	if err := isDeprecated(name); err != nil {
-		return nil, err
-	}
 	argmin := 1
 	argmax := 1
 	var f Interface
@@ -80,6 +37,9 @@ func New(zctx *resolver.Context, name string, narg int) (Interface, error) {
 		f = &ceil{}
 	case "floor":
 		f = &floor{}
+	case "join":
+		argmax = 2
+		f = &join{}
 	case "log":
 		f = &log{}
 	case "max":
@@ -116,6 +76,10 @@ func New(zctx *resolver.Context, name string, narg int) (Interface, error) {
 		f = &iso{}
 	case "sec":
 		f = &sec{}
+	case "split":
+		argmin = 2
+		argmax = 2
+		f = newSplit(zctx)
 	case "msec":
 		f = &msec{}
 	case "usec":
diff --git a/expr/function/string.go b/expr/function/string.go
index b93d71c5b..bb00efaec 100644
--- a/expr/function/string.go
+++ b/expr/function/string.go
@@ -7,7 +7,9 @@ import (
 	"unicode/utf8"
 
 	"github.com/brimsec/zq/expr/result"
+	"github.com/brimsec/zq/zcode"
 	"github.com/brimsec/zq/zng"
+	"github.com/brimsec/zq/zng/resolver"
 )
 
 // XXX these string format functions should be handlded by :string cast
@@ -245,3 +247,87 @@ func (*trim) Call(args []zng.Value) (zng.Value, error) {
 	s := strings.TrimSpace(string(zv.Bytes))
 	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
 }
+
+type split struct {
+	zctx  *resolver.Context
+	typ   zng.Type
+	bytes zcode.Bytes
+}
+
+func newSplit(zctx *resolver.Context) *split {
+	return &split{
+		typ: zctx.LookupTypeArray(zng.TypeString),
+	}
+}
+
+func (s *split) Call(args []zng.Value) (zng.Value, error) {
+	zs := args[0]
+	zsep := args[1]
+	if !zs.IsStringy() || !zsep.IsStringy() {
+		return badarg("split")
+	}
+	if zs.Bytes == nil || zsep.Bytes == nil {
+		return zng.Value{Type: s.typ}, nil
+	}
+	str, err := zng.DecodeString(zs.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	sep, err := zng.DecodeString(zsep.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	splits := strings.Split(str, sep)
+	b := s.bytes[0:]
+	for _, substr := range splits {
+		b = zcode.AppendPrimitive(b, zng.EncodeString(substr))
+	}
+	s.bytes = b
+	return zng.Value{s.typ, b}, nil
+}
+
+type join struct {
+	bytes   zcode.Bytes
+	builder strings.Builder
+}
+
+func (j *join) Call(args []zng.Value) (zng.Value, error) {
+	zsplits := args[0]
+	typ, ok := zng.AliasedType(zsplits.Type).(*zng.TypeArray)
+	if !ok {
+		return zng.NewErrorf("argument to join() is not an array"), nil
+	}
+	if !zng.IsStringy(typ.Type.ID()) {
+		return zng.NewErrorf("argument to join() is not a string array"), nil
+	}
+	var separator string
+	if len(args) == 2 {
+		zsep := args[1]
+		if !zsep.IsStringy() {
+			return zng.NewErrorf("separator argument to join() is not a string"), nil
+		}
+		var err error
+		separator, err = zng.DecodeString(zsep.Bytes)
+		if err != nil {
+			return zng.Value{}, err
+		}
+	}
+	b := j.builder
+	b.Reset()
+	it := zsplits.Bytes.Iter()
+	var sep string
+	for !it.Done() {
+		bytes, _, err := it.Next()
+		if err != nil {
+			return zng.Value{}, err
+		}
+		s, err := zng.DecodeString(bytes)
+		if err != nil {
+			return zng.Value{}, err
+		}
+		b.WriteString(sep)
+		b.WriteString(s)
+		sep = separator
+	}
+	return zng.Value{zng.TypeString, zng.EncodeString(b.String())}, nil
+}


Test patch:
diff --git a/expr/ztests/join.yaml b/expr/ztests/join.yaml
new file mode 100644
index 000000000..d80893a1a
--- /dev/null
+++ b/expr/ztests/join.yaml
@@ -0,0 +1,10 @@
+zql: |
+   cut s1=join(a1,"."),s2=join(a1,"."),s3=join(a3,"."),s4=join(a4,"."),s5=join(oo)
+
+input: |
+  #0:record[a1:array[string],a2:array[string],a3:array[string],a4:array[string],oo:array[string]]
+  0:[[foo;bar;com;][foo;]-;[;][f;.bar.com;]]
+
+output: |
+  #0:record[s1:string,s2:string,s3:string,s4:string,s5:string]
+  0:[foo.bar.com;foo.bar.com;;;f.bar.com;]
diff --git a/expr/ztests/split.yaml b/expr/ztests/split.yaml
new file mode 100644
index 000000000..eadb7055b
--- /dev/null
+++ b/expr/ztests/split.yaml
@@ -0,0 +1,10 @@
+zql: |
+   cut a1=split(s1,"."),a2=split(s2,"."),a3=split(s3,"."),a4=split(s4,"."),oo=split(s1,"oo")
+
+input: |
+  #0:record[s1:string,s2:string,s3:string,s4:string]
+  0:[foo.bar.com;foo;-;;]
+
+output: |
+  #0:record[a1:array[string],a2:array[string],a3:array[string],a4:array[string],oo:array[string]]
+  0:[[foo;bar;com;][foo;]-;[;][f;.bar.com;]]