staros/math.go
starainrt d93a851d1b
feat: 完善 staros 系统能力并更新 wincmd 发布版依赖
- 重构 sysconf 为文档模型 INI Parser 与 Config Framework
- 强化 hosts 解析、插入校验、写回与异常输入处理
- 完善 StarCmd 生命周期、等待 API、流式输出与 IO 重定向
- 扩展跨平台文件时间、文件锁、内存、进程与网络能力
- 将 Windows 进程适配更新到 b612.me/wincmd v0.1.0
- 移除本地 wincmd/win32api replace,改用发布版依赖
- 将最低 Go 版本提升到 1.18
- 补充 hosts、sysconf、FileLock、StarCmd 与平台适配回归测试
2026-06-09 18:10:19 +08:00

404 lines
8.0 KiB
Go

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
}