1package formula23import (4 "errors"5 "strings"67 "src.rest/cat/012/internal/locale"8)910// Formulas are stored and parsed in en-US's syntax: 1.5 for a number, ,11// between arguments and ; between an array's rows. A locale whose12// decimal separator is a comma writes 1,5, puts ; between arguments and13// \ between the values of an array's row, as Sheets does: =ROUND(1,5; 0)14// and {1\2;3\4}. Function names stay in English. Localize and15// Delocalize translate at the edges, where formulas are shown and typed;16// each separator is one byte in both, so a position in one is the same17// position in the other (a parse error's, the caret's).1819// Localize writes a formula (or any part of one) as stored in loc's20// syntax. Strings and sheet names are left alone.21func Localize(src string, loc *locale.Locale) string {22 return translate(src, loc, true)23}2425// Delocalize reads a formula typed in loc's syntax into the syntax it is26// stored and parsed in. It works on unfinished formulas too.27func Delocalize(src string, loc *locale.Locale) string {28 return translate(src, loc, false)29}3031// LocalizeError writes the message of a parse error in err (the error32// itself, or one it wraps) in loc's syntax, as the formula was typed:33// "Expected ; or ) in ROUND" where arguments are separated by ;, and34// the text typed where it names what it didn't expect. The position is35// the same in both syntaxes.36func LocalizeError(err error, loc *locale.Locale) error {37 var pe *ParseError38 if SameSyntax(loc) || !errors.As(err, &pe) {39 return err40 }41 msg := localMessage(pe, loc)42 if msg == pe.Msg {43 return err44 }45 if err == error(pe) {46 return &ParseError{Pos: pe.Pos, Msg: msg, src: pe.src}47 }48 return errors.New(strings.Replace(err.Error(), pe.Msg, msg, 1))49}5051// localMessage is a parse error's message in loc's syntax.52func localMessage(pe *ParseError, loc *locale.Locale) string {53 typed := Localize(pe.src, loc)54 arg, col := string(loc.ArgSep()), string(loc.ColSep())55 switch msg := pe.Msg; {56 case strings.HasPrefix(msg, "Expected , or ) in "):57 return "Expected " + arg + " or )" + strings.TrimPrefix(msg, "Expected , or )")58 case msg == "Expected , ; or } in an array":59 return "Expected " + col + " ; or } in an array"60 case strings.HasPrefix(msg, "Wrong number of arguments to "):61 return strings.ReplaceAll(msg, ", ", arg+" ")62 }63 for _, lead := range []string{"Unexpected ", "Invalid number "} {64 tok, ok := strings.CutPrefix(pe.Msg, lead)65 if end := pe.Pos + len(tok); ok && pe.Pos >= 0 && end <= len(typed) && len(typed) == len(pe.src) {66 return lead + typed[pe.Pos:end]67 }68 }69 return pe.Msg70}7172// SameSyntax reports whether loc writes formulas as they are stored.73func SameSyntax(loc *locale.Locale) bool {74 return loc == nil || loc.Decimal == '.' && loc.ArgSep() == ',' && loc.ColSep() == ','75}7677// separators are the characters a translation swaps, from and to.78type separators struct {79 dec, arg, col byte // in the syntax read80 toDec, toArg, toCol byte // in the syntax written81}8283func translate(src string, loc *locale.Locale, out bool) string {84 if SameSyntax(loc) {85 return src86 }87 sep := separators{dec: '.', arg: ',', col: ',', toDec: loc.Decimal, toArg: loc.ArgSep(), toCol: loc.ColSep()}88 if !out {89 sep = separators{dec: loc.Decimal, arg: loc.ArgSep(), col: loc.ColSep(), toDec: '.', toArg: ',', toCol: ','}90 }91 b := []byte(src)92 var open []byte // the ( and { around the position, innermost last93 for i := 0; i < len(b); {94 switch c := b[i]; {95 case c == '"' || c == '\'':96 i = skipQuoted(b, i)97 case c == '@' || isIdentStart(c):98 i = skipIdent(b, i+1)99 case isDigit(c) || c == sep.dec && i+1 < len(b) && isDigit(b[i+1]):100 i = sep.number(b, i)101 case c == '[':102 i = sep.brackets(b, i)103 case c == '(' || c == '{':104 open = append(open, c)105 i++106 case c == ')' || c == '}':107 if len(open) > 0 {108 open = open[:len(open)-1]109 }110 i++111 default:112 inArray := len(open) > 0 && open[len(open)-1] == '{'113 switch {114 case inArray && c == sep.col:115 b[i] = sep.toCol116 case !inArray && c == sep.arg:117 b[i] = sep.toArg118 }119 i++120 }121 }122 return string(b)123}124125// brackets translates the separators between the items of a126// structured reference, Sales[[#Headers],[Amount]], starting at the "["127// at i, and returns where it ends. Columns' names are left alone.128func (sep separators) brackets(b []byte, i int) int {129 end := bracketEnd(string(b), i)130 if end < 0 {131 end = len(b)132 }133 for j := i + 1; j < end; j++ {134 switch {135 case b[j] == '\'':136 j++137 case b[j] == sep.arg && prevByte(b, j) == ']' && nextByte(b, j, end) == '[':138 b[j] = sep.toArg139 }140 }141 return end142}143144// prevByte is the byte before i, past spaces, or 0.145func prevByte(b []byte, i int) byte {146 for i--; i >= 0 && b[i] == ' '; i-- {147 }148 if i < 0 {149 return 0150 }151 return b[i]152}153154// nextByte is the byte after i, past spaces, before end, or 0.155func nextByte(b []byte, i, end int) byte {156 for i++; i < end && b[i] == ' '; i++ {157 }158 if i >= end {159 return 0160 }161 return b[i]162}163164// skipQuoted skips a string or quoted sheet name starting at i, with its165// quote doubled inside; an unclosed one runs to the end.166func skipQuoted(b []byte, i int) int {167 q := b[i]168 for i++; i < len(b); i++ {169 if b[i] != q {170 continue171 }172 if i+1 < len(b) && b[i+1] == q {173 i++174 continue175 }176 return i + 1177 }178 return len(b)179}180181// skipIdent skips the rest of a reference, name or function name.182func skipIdent(b []byte, i int) int {183 for i < len(b) && (isIdentPart(b[i]) || b[i] >= 0x80) {184 i++185 }186 return i187}188189// number rewrites the decimal separator of the number at i, returning190// where it ends: digits, one decimal separator, and an exponent.191func (sep separators) number(b []byte, i int) int {192 point := false193 for ; i < len(b); i++ {194 switch c := b[i]; {195 case isDigit(c):196 case c == sep.dec && !point && (i+1 >= len(b) || b[i+1] != sep.dec):197 b[i], point = sep.toDec, true198 case (c == 'e' || c == 'E') && exponentAt(b, i+1):199 i++200 if b[i] == '+' || b[i] == '-' {201 i++202 }203 default:204 return i205 }206 }207 return i208}209210// exponentAt reports whether an exponent's sign or digits start at i.211func exponentAt(b []byte, i int) bool {212 if i < len(b) && (b[i] == '+' || b[i] == '-') {213 i++214 }215 return i < len(b) && isDigit(b[i])216}