1package functions23import (4 "regexp"5 "strings"6 "sync"78 "src.rest/cat/012/internal/value"9)1011// Splitting text and regular expressions. Sheets' regular expressions12// are RE2, as Go's regexp package is, so patterns mean the same.1314func init() {15 define(16 &FuncDef{Name: "SPLIT", Args: "text, delimiter, [split_by_each], [remove_empty_text]", Desc: "Text split at a delimiter into cells across (each character of it by default)", Min: 2, Max: 4,17 eval: split, arrays: takesArrays},18 &FuncDef{Name: "REGEXMATCH", Args: "text, regular_expression", Desc: "TRUE if text matches a regular expression", Min: 2, Max: 2,19 eval: regexMatch},20 &FuncDef{Name: "REGEXEXTRACT", Args: "text, regular_expression", Desc: "The first match of a regular expression, or its capture groups across", Min: 2, Max: 2,21 eval: regexExtract, arrays: liftPass},22 &FuncDef{Name: "REGEXREPLACE", Args: "text, regular_expression, replacement", Desc: "Text with every match replaced; $1 in the replacement is a capture group", Min: 3, Max: 3,23 eval: regexReplace},24 )25}2627// Evaluators for the table above, in its order.2829// split splits text at a delimiter: at each of its characters unless30// split_by_each is FALSE, leaving out empty pieces unless31// remove_empty_text is FALSE. Pieces that read as numbers are numbers,32// as in Sheets. In an array context each entry of text is split into a33// row of its own.34func split(args []Node, get lookup) Value {35 delim, err := textArg(args[1], get)36 if err != nil {37 return *err38 }39 each, err := boolArg(args, 2, true, get)40 if err != nil {41 return *err42 }43 drop, err := boolArg(args, 3, true, get)44 if err != nil {45 return *err46 }47 if delim == "" {48 return value.ErrValue49 }50 cut := func(s string) []Value { return splitText(s, delim, each, drop) }51 if get.lift == 0 {52 s, err := textArg(args[0], get)53 if err != nil {54 return *err55 }56 row := cut(s)57 out := NewArray(1, len(row))58 copy(out.V, row)59 return get.arrayValue(out)60 }61 texts, err := arrayArg(args[0], get)62 if err != nil {63 return *err64 }65 return get.arrayValue(splitRows(texts, cut))66}6768// splitRows splits each entry of texts (row by row) into a row of its69// own, the rows as wide as the widest, blank past their pieces.70func splitRows(texts *Array, cut func(string) []Value) *Array {71 var rows [][]Value72 width := 073 for r := range texts.DRows {74 for c := range texts.DCols {75 v := texts.At(r, c)76 var row []Value77 if v.Kind == value.Error {78 row = []Value{v}79 } else if v.Kind != value.Empty {80 row = cut(text(v))81 }82 rows = append(rows, row)83 width = max(width, len(row))84 }85 }86 width = max(width, 1)87 if tooBig(len(rows), width) {88 return &Array{Rows: 1, Cols: 1, DRows: 1, DCols: 1, V: []Value{value.ErrValue}}89 }90 out := NewArray(max(len(rows), 1), width)91 for i, row := range rows {92 copy(out.V[i*width:], row)93 }94 return out95}9697// splitText is the pieces of s between delimiters, as values.98func splitText(s, delim string, each, drop bool) []Value {99 var parts []string100 switch {101 case each && drop:102 parts = strings.FieldsFunc(s, func(r rune) bool { return strings.ContainsRune(delim, r) })103 case each:104 parts = splitEach(s, delim)105 default:106 parts = strings.Split(s, delim)107 }108 out := make([]Value, 0, len(parts))109 for _, p := range parts {110 if p == "" && drop {111 continue112 }113 if n, _, ok := value.ParseValue(p); ok && strings.TrimSpace(p) == p {114 out = append(out, num(n))115 continue116 }117 out = append(out, str(p))118 }119 if len(out) == 0 {120 out = append(out, Value{})121 }122 return out123}124125// splitEach splits s at every character of delim, keeping empty pieces.126func splitEach(s, delim string) []string {127 var parts []string128 start := 0129 for i, r := range s {130 if strings.ContainsRune(delim, r) {131 parts = append(parts, s[start:i])132 start = i + len(string(r))133 }134 }135 return append(parts, s[start:])136}137138func regexMatch(args []Node, get lookup) Value {139 s, re, err := regexArgs(args, get)140 if err != nil {141 return *err142 }143 return boolean(re.MatchString(s))144}145146func regexExtract(args []Node, get lookup) Value {147 s, re, err := regexArgs(args, get)148 if err != nil {149 return *err150 }151 m := re.FindStringSubmatch(s)152 switch {153 case m == nil:154 return value.ErrNA155 case len(m) == 1:156 return str(m[0])157 case len(m) == 2:158 return str(m[1])159 }160 out := NewArray(1, len(m)-1)161 for i, g := range m[1:] {162 out.V[i] = str(g)163 }164 return get.arrayValue(out)165}166167func regexReplace(args []Node, get lookup) Value {168 s, re, err := regexArgs(args, get)169 if err != nil {170 return *err171 }172 with, err := regexText(args[2], get)173 if err != nil {174 return *err175 }176 return capText(re.ReplaceAllString(s, with))177}178179// regexArgs reads the text and the regular expression of a REGEX180// function: text only, as Sheets takes (a number is #VALUE!), and a181// pattern that compiles.182func regexArgs(args []Node, get lookup) (string, *regexp.Regexp, *Value) {183 s, err := regexText(args[0], get)184 if err != nil {185 return "", nil, err186 }187 pattern, err := textArg(args[1], get)188 if err != nil {189 return "", nil, err190 }191 re, ok := compiled(pattern)192 if !ok {193 return "", nil, &value.ErrValue194 }195 return s, re, nil196}197198// regexText reads an argument that must be text.199func regexText(n Node, get lookup) (string, *Value) {200 v := eval1(n, get)201 switch v.Kind {202 case value.Error:203 return "", errOf(v)204 case value.Text, value.Empty:205 return v.Str, nil206 }207 return "", &value.ErrValue208}209210// regexCache keeps compiled patterns, as a column of REGEXMATCH or an211// ARRAYFORMULA of one uses the same pattern for every row. It is shared212// by every workbook (and every session of 012 serve), so it's locked,213// and emptied when full.214var regexCache = struct {215 sync.Mutex216 m map[string]*regexp.Regexp217}{m: map[string]*regexp.Regexp{}}218219const maxRegexCache = 256220221// compiled is pattern compiled, or false when it isn't a valid RE2222// pattern.223func compiled(pattern string) (*regexp.Regexp, bool) {224 regexCache.Lock()225 defer regexCache.Unlock()226 if re, ok := regexCache.m[pattern]; ok {227 return re, re != nil228 }229 re, err := regexp.Compile(pattern)230 if err != nil {231 re = nil232 }233 if len(regexCache.m) >= maxRegexCache {234 clear(regexCache.m)235 }236 regexCache.m[pattern] = re237 return re, re != nil238}