package staros import ( "fmt" "math" "strconv" "strings" "unicode" ) // Calc evaluates a small frozen arithmetic expression language kept for // compatibility with older staros callers. func Calc(expr string) (float64, error) { parser := calcParser{input: strings.ToLower(strings.TrimSpace(expr))} if parser.input == "" { return 0, fmt.Errorf("empty expression") } value, err := parser.parseExpression() if err != nil { return 0, err } parser.skipSpace() if !parser.done() { return 0, fmt.Errorf("unexpected token %q at position %d", parser.peek(), parser.pos) } return normalizeCalcFloat(value), nil } type calcParser struct { input string pos int } func (p *calcParser) parseExpression() (float64, error) { return p.parseAddSub() } func (p *calcParser) parseAddSub() (float64, error) { left, err := p.parseMulDiv() if err != nil { return 0, err } for { p.skipSpace() switch p.peek() { case '+': p.pos++ right, err := p.parseMulDiv() if err != nil { return 0, err } left += right case '-': p.pos++ right, err := p.parseMulDiv() if err != nil { return 0, err } left -= right default: return left, nil } } } func (p *calcParser) parseMulDiv() (float64, error) { left, err := p.parsePower() if err != nil { return 0, err } for { p.skipSpace() switch p.peek() { case '*': p.pos++ right, err := p.parsePower() if err != nil { return 0, err } left *= right case '/': p.pos++ right, err := p.parsePower() if err != nil { return 0, err } if right == 0 { return 0, fmt.Errorf("divisor cannot be 0") } left /= right default: return left, nil } } } func (p *calcParser) parsePower() (float64, error) { left, err := p.parseUnary() if err != nil { return 0, err } p.skipSpace() if p.peek() != '^' { return left, nil } p.pos++ right, err := p.parsePower() if err != nil { return 0, err } return math.Pow(left, right), nil } func (p *calcParser) parseUnary() (float64, error) { p.skipSpace() switch p.peek() { case '+': p.pos++ return p.parseUnary() case '-': p.pos++ value, err := p.parseUnary() if err != nil { return 0, err } return -value, nil default: return p.parsePrimary() } } func (p *calcParser) parsePrimary() (float64, error) { p.skipSpace() if p.done() { return 0, fmt.Errorf("unexpected end of expression") } ch := p.peek() switch { case ch == '(': p.pos++ value, err := p.parseExpression() if err != nil { return 0, err } p.skipSpace() if p.peek() != ')' { return 0, fmt.Errorf("missing ')' at position %d", p.pos) } p.pos++ return value, nil case isCalcNumberStart(p.input, p.pos): return p.parseNumber() case isCalcIdentStart(ch): return p.parseIdentifier() default: return 0, fmt.Errorf("unexpected token %q at position %d", ch, p.pos) } } func (p *calcParser) parseNumber() (float64, error) { start := p.pos seenDot := false seenExp := false for !p.done() { ch := p.peek() switch { case ch >= '0' && ch <= '9': p.pos++ case ch == '.' && !seenDot && !seenExp: seenDot = true p.pos++ case (ch == 'e') && !seenExp: seenExp = true p.pos++ if !p.done() && (p.peek() == '+' || p.peek() == '-') { p.pos++ } default: value, err := strconv.ParseFloat(p.input[start:p.pos], 64) if err != nil { return 0, fmt.Errorf("invalid number %q at position %d", p.input[start:p.pos], start) } return value, nil } } value, err := strconv.ParseFloat(p.input[start:p.pos], 64) if err != nil { return 0, fmt.Errorf("invalid number %q at position %d", p.input[start:p.pos], start) } return value, nil } func (p *calcParser) parseIdentifier() (float64, error) { start := p.pos for !p.done() && isCalcIdent(p.peek()) { p.pos++ } name := p.input[start:p.pos] p.skipSpace() if p.peek() != '(' { value, ok := calcConstant(name) if !ok { return 0, fmt.Errorf("unknown identifier %q at position %d", name, start) } return value, nil } p.pos++ args, err := p.parseArguments(name) if err != nil { return 0, err } return calcFunction(name, args) } func (p *calcParser) parseArguments(name string) ([]float64, error) { p.skipSpace() if p.peek() == ')' { p.pos++ return nil, nil } var args []float64 for { arg, err := p.parseExpression() if err != nil { return nil, err } args = append(args, arg) p.skipSpace() if p.peek() != ',' { break } p.pos++ p.skipSpace() if p.peek() == ')' { return nil, fmt.Errorf("missing argument for function %q", name) } } if p.peek() != ')' { return nil, fmt.Errorf("missing ')' after function %q", name) } p.pos++ return args, nil } func (p *calcParser) skipSpace() { for !p.done() && unicode.IsSpace(rune(p.peek())) { p.pos++ } } func (p *calcParser) done() bool { return p.pos >= len(p.input) } func (p *calcParser) peek() byte { if p.done() { return 0 } return p.input[p.pos] } func isCalcNumberStart(input string, pos int) bool { ch := input[pos] if ch >= '0' && ch <= '9' { return true } return ch == '.' && pos+1 < len(input) && input[pos+1] >= '0' && input[pos+1] <= '9' } func isCalcIdentStart(ch byte) bool { return ch >= 'a' && ch <= 'z' } func isCalcIdent(ch byte) bool { return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' } func calcConstant(name string) (float64, bool) { switch name { case "pi": return math.Pi, true case "e": return math.E, true default: return 0, false } } func calcFunction(name string, args []float64) (float64, error) { argCount := len(args) if !calcFunctionArgCountValid(name, argCount) { return 0, fmt.Errorf("function %q accepts %s, got %d", name, calcFunctionArgSpec(name), argCount) } switch name { case "sin": return math.Sin(args[0]), nil case "cos": return math.Cos(args[0]), nil case "tan": return math.Tan(args[0]), nil case "sinh": return math.Sinh(args[0]), nil case "cosh": return math.Cosh(args[0]), nil case "tanh": return math.Tanh(args[0]), nil case "abs": return math.Abs(args[0]), nil case "arcsin", "asin": return math.Asin(args[0]), nil case "arccos", "acos": return math.Acos(args[0]), nil case "arctan", "atan": return math.Atan(args[0]), nil case "sqrt": return math.Sqrt(args[0]), nil case "cbrt": return math.Cbrt(args[0]), nil case "exp": return math.Exp(args[0]), nil case "loge", "ln": return math.Log(args[0]), nil case "log": return math.Log10(args[0]), nil case "log10": return math.Log10(args[0]), nil case "log2": return math.Log2(args[0]), nil case "floor": return math.Floor(args[0]), nil case "ceil": return math.Ceil(args[0]), nil case "round": return math.Round(args[0]), nil case "trunc": return math.Trunc(args[0]), nil case "rad": return args[0] * math.Pi / 180.0, nil case "deg": return args[0] * 180.0 / math.Pi, nil case "pow": return math.Pow(args[0], args[1]), nil case "hypot": return math.Hypot(args[0], args[1]), nil case "min": result := args[0] for _, arg := range args[1:] { if arg < result { result = arg } } return result, nil case "max": result := args[0] for _, arg := range args[1:] { if arg > result { result = arg } } return result, nil default: return 0, fmt.Errorf("unknown function %q", name) } } func calcFunctionArgCountValid(name string, count int) bool { switch name { case "pow", "hypot": return count == 2 case "min", "max": return count >= 1 case "sin", "cos", "tan", "sinh", "cosh", "tanh", "abs", "arcsin", "asin", "arccos", "acos", "arctan", "atan", "sqrt", "cbrt", "exp", "loge", "ln", "log", "log10", "log2", "floor", "ceil", "round", "trunc", "rad", "deg": return count == 1 default: return true } } func calcFunctionArgSpec(name string) string { switch name { case "pow", "hypot": return "exactly two arguments" case "min", "max": return "at least one argument" default: return "exactly one argument" } } func normalizeCalcFloat(value float64) float64 { text := strconv.FormatFloat(value, 'g', 15, 64) out, err := strconv.ParseFloat(text, 64) if err != nil { return value } if out == 0 { return 0 } return out }