- 重构 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 与平台适配回归测试
404 lines
8.0 KiB
Go
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
|
|
}
|