initial commit [141.14.140.180,mike]

This commit is contained in:
2026-07-27 16:14:41 +02:00
commit 917a3d41ac
18 changed files with 3587 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
# Binaries
goca
dist/
bin/
+197
View File
@@ -0,0 +1,197 @@
# 🧮 goca
> A feature-rich, high-precision, scientific command-line calculator written in Go, powered by an Abstract Syntax Tree (AST) evaluator and arbitrary-precision decimal arithmetic.
[![Go Version](https://img.shields.io/badge/go-1.18%2B-blue.svg)](https://go.dev/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
---
## ✨ Features
* **High-Precision Arithmetic**: Uses arbitrary-precision decimal math (no floating-point rounding errors).
* **Unit Conversions**: Supports Length, Mass, Time, Area, Temperature, Digital Storage, and Angle conversions natively.
* **Live Currency Exchange**: Real-time currency conversion rates fetched automatically and cached.
* **IP & CIDR Utilities**: Built-in network functions to parse IPs, calculate networks, broadcasts, netmasks, and address ranges.
* **User-Defined Variables & Functions**: Persists variables and custom functions across sessions automatically.
* **Rich Scientific Math**: Trigonometric (supporting deg/rad/grad), hyperbolic, and standard statistical/mathematical functions.
* **Interactive CLI**: Autocompletion (TAB), history scroll, and syntax-colored output. Can also be used non-interactively via shell pipes.
---
## 🚀 Installation & Setup
Ensure you have [Go](https://go.dev/doc/install) installed.
1. **Clone and Navigate**:
```bash
git clone https://git.fhi.mpg.de/mike/goca.git
cd goca
```
2. **Build the Binary**:
```bash
go build -o goca
```
3. **Install to Path (Optional)**:
```bash
mv goca /usr/local/bin/
```
---
## 💡 Quick Start
Simply run `goca` to start the interactive shell:
```bash
$ ./goca
goca v1.0.15, type 'help' for examples.
goca> 5 + 3 * 2
= 11 (0xB, 0b1011)
goca> 100 USD to EUR
= 91.42 EUR
```
Or pipe expressions directly from your shell:
```bash
echo "10 mi to km" | ./goca
```
### 🌐 Web GUI & Server Mode
goca now features an interactive cyberpunk-styled web dashboard! Start the server by passing the `-p` or `--port` flag:
```bash
$ ./goca -p 8800
🌐 Go-Ca web server started on http://localhost:8800
```
Open your browser and navigate to `http://localhost:8800` to access the console. The Web GUI supports:
* **Interactive Console**: Runs calculations with ANSI terminal color rendering.
* **Command History**: Navigate your previous queries using the `ArrowUp` and `ArrowDown` keys in the input box.
* **User Registry Panel**: Displays your currently defined variables and custom functions in real-time. Click any item to insert it directly into the input line.
* **Quick Reference Guide**: Direct access to mathematical, conversion, IP, and currency syntax templates.
* **State Management**: Clear the server state (variables/functions) via the `Reset State` header action.
#### 📡 Developer REST API
Integrate the calculator engine with other services via HTTP REST endpoints:
* **Evaluate**: `GET /api/calculate?q=<expression>` - Returns `{ "output": "<result string>", "exit": <bool> }`
* **List Variables**: `GET /api/variables` - Returns a sorted JSON array of user-defined variables
* **List Functions**: `GET /api/functions` - Returns a sorted JSON array of user-defined functions
* **Reset**: `POST /api/reset` - Deletes all user-defined variables/functions
---
## 📘 Comprehensive Guide
### 🔢 Core Math & Number Systems
goca supports standard mathematical operations, bitwise logic, and various number systems:
* **Arithmetic**: `+`, `-`, `*`, `/`, `%` (modulo), `**` or `pow(a, b)`
* **Bitwise Operations**: `&` (AND), `|` (OR), `^` (XOR), `~` (NOT), `<<` (Left Shift), `>>` (Right Shift)
* **Number Systems**:
* **Hexadecimal**: `0xFF`
* **Binary**: `0b1010`
* **Octal**: `0o77`
* **Implicit Multiplication**: `2(3 + 4)` or `2km`
### 📐 Scientific & Mathematical Functions
A rich suite of functions is built-in:
* **Trigonometry**: `sin(x)`, `cos(x)`, `tan(x)` (supports suffixes, e.g., `sin(90 deg)` or `sin(pi rad)`)
* **Inverse Trig**: `asin(x)`, `acos(x)`, `atan(x)` (returns units, e.g., `asin(1) to deg`)
* **Hyperbolic**: `sinh(x)`, `cosh(x)`, `tanh(x)`
* **General Math**: `sqrt(x)`, `abs(x)`, `exp(x)`, `ln(x)` (natural), `log(x)` (base 10), `log2(x)`
* **Rounding**: `ceil(x)`, `floor(x)`, `round(x)`
* **Combinatorics & Stats**: `fact(x)` (factorial), `min(a, b, ...)`, `max(a, b, ...)`, `mod(a, b)`
* **Conditionals**: `if(cond, true_val, false_val)` (e.g., `if(5 > 3, 10, 20)`)
* **Constants**: `PI` and `E`
### 💾 Variables & Custom Functions
Define and persist your own variables and functions:
* **Variables**:
* Assign: `x = 5.5`
* Reference last output: `ans` or `_` (e.g., `ans * 2`)
* List variables: `var`
* Delete variable: `unset x` (or `unset *` to clear all)
* **Custom Functions**:
* Define: `f(x, y) = x * y + 2`
* Evaluate: `f(3, 4)` (yields `14`)
* List functions: `funcs`
* Delete function: `unset f`
All custom variables and functions are saved to `~/.goca_variables.json` and `~/.goca_functions.json` respectively, making them available in future sessions.
### 🌐 IP & Subnet Calculations
goca includes a robust set of network utility functions:
* **Parse IP / CIDR**: `ip("192.168.1.1")`, `cidr("10.0.0.0/24")`
* **Network Address**: `network(cidr("10.0.0.50/24"))` (yields `10.0.0.0/24`)
* **Broadcast Address**: `broadcast(cidr("10.0.0.50/24"))` (yields `10.0.0.255`)
* **Netmask**: `mask(cidr("10.0.0.50/24"))` (yields `255.255.255.0`)
* **Hosts Count**: `hosts(cidr("10.0.0.0/24"))` (yields `254`)
* **IP Range**: `range(cidr("10.0.0.0/24"))` (returns string representation of start and end IPs)
### 📏 Unit Conversions
Convert measurements using the syntax: `<value> <unit> to <unit>` or `<value> <unit> in <unit>`.
| Dimension | Supported Units |
| :--- | :--- |
| **Length** | `km`, `m`, `dm`, `cm`, `mm`, `um`, `nm`, `mi` (mile), `nmi` (nautical mile), `ft` (foot), `in` (inch), `yd` (yard) |
| **Mass** | `t` (tonne), `kg`, `g`, `mg`, `ct` (carat), `lb` (pound), `oz` (ounce) |
| **Time** | `yr` (year), `wk` (week), `d` (day), `h` (hour), `min` (minute), `s`, `ms`, `us`, `ns` |
| **Digital Storage** | `EB`, `PB`, `TB`, `GB`, `MB`, `KB`, `B` (binary multiplier: `1024` base) |
| **Area** | `km2` (sq. kilometer), `ha` (hectare), `acre`, `m2` (sq. meter), `cm2`, `mm2` |
| **Temperature** | `K` (Kelvin), `C` (Celsius), `F` (Fahrenheit) |
| **Angle** | `rad`, `deg` (degree), `grad` |
**Examples**:
```text
goca> 100 mi to km
= 160.9344 km
goca> 1 GB in MB
= 1024 MB
goca> 0 C to F
= 32.0000 °F
```
### 💱 Live Currency Exchange
goca fetches exchange rates daily from the Open Exchange Rates API and caches them locally at `~/.goca_rates.json`.
* **Conversion**: `100 USD to EUR` or `10$ to €`
* **Change Base Currency**: `base USD`
* **List Supported Currency Codes**: `cur`
* **View Exchange Rates**: `rates` (shows core currencies relative to base)
---
## 🛠️ Commands Reference
The interactive shell supports the following commands:
* `help` - Show help information.
* `units` - List all supported measurement units.
* `rates` - Show currency exchange rates relative to the base currency.
* `cur` - List all supported currency codes.
* `var` - List all user-defined variables.
* `funcs` - List all user-defined functions.
* `unset <name>` - Delete a variable or custom function (or `unset *` to clear all).
* `base <currency>` - Change base currency.
* `clear` - Clear the terminal screen.
* `exit` or `quit` - Close the session.
---
## 📦 Project Architecture
* [cli.go](file:///Users/mike/src/goca/cli.go) - Command loop, line-completion, history, and user input handler.
* [units.go](file:///Users/mike/src/goca/units.go) - Units registry and conversion metadata.
* [currency.go](file:///Users/mike/src/goca/currency.go) - Exchange rates fetcher, caching, and currency unit initialization.
* [evaluator.go](file:///Users/mike/src/goca/evaluator.go) - AST evaluator implementing all math, logic, IP, and unit conversion rules.
* [parser.go](file:///Users/mike/src/goca/parser.go) - Lexer and recursive descent parser.
* [ast.go](file:///Users/mike/src/goca/ast.go) - Abstract Syntax Tree structures for parsing expressions.
* [types.go](file:///Users/mike/src/goca/types.go) - Core types used across parsing and evaluation.
* [main_test.go](file:///Users/mike/src/goca/main_test.go) - Test suite covering math, variables, functions, and unit conversions.
+63
View File
@@ -0,0 +1,63 @@
package main
// Node represents an AST node.
type Node interface {
isNode()
}
type Expr interface {
Node
isExpr()
}
// BinaryExpr represents a binary operation.
type BinaryExpr struct {
Op string
Left Expr
Right Expr
}
func (e *BinaryExpr) isNode() {}
func (e *BinaryExpr) isExpr() {}
// UnaryExpr represents a unary operation (+, -, ~).
type UnaryExpr struct {
Op string
Expr Expr
}
func (e *UnaryExpr) isNode() {}
func (e *UnaryExpr) isExpr() {}
// NumberExpr represents a numeric literal.
type NumberExpr struct {
Value string // parsed as decimal later
}
func (e *NumberExpr) isNode() {}
func (e *NumberExpr) isExpr() {}
// IdentExpr represents an identifier (variable, unit, constant, or currency symbol).
type IdentExpr struct {
Name string
}
func (e *IdentExpr) isNode() {}
func (e *IdentExpr) isExpr() {}
// CallExpr represents a function call.
type CallExpr struct {
Func string
Args []Expr
}
func (e *CallExpr) isNode() {}
func (e *CallExpr) isExpr() {}
// StringExpr represents a string literal.
type StringExpr struct {
Value string
}
func (e *StringExpr) isNode() {}
func (e *StringExpr) isExpr() {}
Executable
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -e
# Make sure we are in the script's directory
cd "$(dirname "$0")"
# Read version from version.go
VERSION_LINE=$(grep "var Version =" version.go)
CURRENT_VERSION=$(echo "$VERSION_LINE" | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$CURRENT_VERSION" ]; then
echo "❌ Error: Could not parse version from version.go"
exit 1
fi
# Split version by dot
IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION"
MAJOR="${VERSION_PARTS[0]}"
MINOR="${VERSION_PARTS[1]}"
PATCH="${VERSION_PARTS[2]}"
# Increment patch version
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
echo "🚀 Incrementing version: $CURRENT_VERSION ➡️ $NEW_VERSION"
# Write back to version.go using perl for portable in-place edit
perl -pi -e "s/var Version = \"$CURRENT_VERSION\"/var Version = \"$NEW_VERSION\"/" version.go
# Update README.md version output
if [ -f README.md ]; then
perl -pi -e "s/goca v$CURRENT_VERSION/goca v$NEW_VERSION/g" README.md
fi
# Build the project
echo "🛠️ Building goca binary..."
go build -o goca
echo "🎉 Build successful! version: v$NEW_VERSION"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -e
# Make sure we are in the script's directory
cd "$(dirname "$0")"
# Read version from version.go
VERSION_LINE=$(grep "var Version =" version.go)
VERSION=$(echo "$VERSION_LINE" | sed -E 's/.*"([^"]+)".*/\1/')
echo "🛠️ Compiling release binaries for version v$VERSION..."
mkdir -p dist
# macOS Intel
echo "🍏 Building macOS Intel (amd64)..."
GOOS=darwin GOARCH=amd64 go build -o dist/goca-darwin-amd64
# macOS Apple Silicon
echo "🍏 Building macOS Apple Silicon (arm64)..."
GOOS=darwin GOARCH=arm64 go build -o dist/goca-darwin-arm64
# Linux Intel
echo "🐧 Building Linux Intel (amd64)..."
GOOS=linux GOARCH=amd64 go build -o dist/goca-linux-amd64
# Windows Intel
echo "🏁 Building Windows Intel (amd64)..."
GOOS=windows GOARCH=amd64 go build -o dist/goca-windows-amd64.exe
echo "🎉 All release binaries built in dist/!"
+95
View File
@@ -0,0 +1,95 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"github.com/shopspring/decimal"
)
var baseCurrency = "EUR"
var ratesAPIURL = "https://open.er-api.com/v6/latest/EUR"
type RatesCache struct {
Timestamp time.Time `json:"timestamp"`
Rates map[string]float64 `json:"rates"`
}
// fetchRates downloads exchange rates from the API or loads them from a local cache
// if they are less than 24 hours old.
func fetchRates() error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("could not find user home dir for cache: %w", err)
}
cacheFile := filepath.Join(home, ".goca_rates.json")
var data RatesCache
useCache := false
// Try loading from cache
file, err := os.Open(cacheFile)
if err == nil {
if err := json.NewDecoder(file).Decode(&data); err == nil {
if time.Since(data.Timestamp) < 24*time.Hour && len(data.Rates) > 0 {
useCache = true
}
}
file.Close()
}
if !useCache {
// Fetch from API
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(ratesAPIURL)
if err != nil {
return fmt.Errorf("failed to fetch rates from API: %w", err)
}
defer resp.Body.Close()
var apiResp struct {
Rates map[string]float64 `json:"rates"`
}
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return fmt.Errorf("failed to decode rates API response: %w", err)
}
data = RatesCache{
Timestamp: time.Now(),
Rates: apiResp.Rates,
}
// Save to cache asynchronously
go func() {
file, err := os.Create(cacheFile)
if err == nil {
json.NewEncoder(file).Encode(data)
file.Close()
}
}()
}
// Populate unit registry with rates
for code, rate := range data.Rates {
// Avoid division by zero
if rate > 0 {
factor := decimal.NewFromFloat(1.0).Div(decimal.NewFromFloat(rate))
unitRegistry[code] = UnitInfo{factor, DimCurrency, code}
}
}
unitRegistry["EUR"] = UnitInfo{decimal.NewFromFloat(1.0), DimCurrency, "EUR"}
return nil
}
// eurRatesLookup returns the conversion factor of the currency to EUR.
func eurRatesLookup(code string) decimal.Decimal {
if u, ok := unitRegistry[code]; ok && u.Dimension == DimCurrency {
return u.Factor
}
return decimal.NewFromFloat(1.0)
}
+568
View File
@@ -0,0 +1,568 @@
package main
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"github.com/shopspring/decimal"
)
var variables = make(map[string]Result)
var userFuncs = make(map[string]UserFunc)
var stringTable []string
// evaluate evaluates a full command string, potentially handling "to/in" conversions.
func evaluate(exprStr string) (Result, string, error) {
re := regexp.MustCompile(`(?i)\s+(to|in)\s+`)
loc := re.FindStringIndex(exprStr)
var targetUnit string
var exprToEval string
if loc != nil {
exprToEval = exprStr[:loc[0]]
targetUnit = strings.ToUpper(strings.TrimSpace(exprStr[loc[1]:]))
} else {
exprToEval = exprStr
}
p := NewParser(exprToEval)
ast, err := p.Parse()
if err != nil {
return Result{}, "", fmt.Errorf("parse error: %w", err)
}
res, err := evalExpr(ast, nil)
if err != nil {
return Result{}, "", fmt.Errorf("eval error: %w", err)
}
if targetUnit != "" {
resolvedUnit := targetUnit
switch targetUnit {
case "$":
resolvedUnit = "USD"
case "€":
resolvedUnit = "EUR"
case "£":
resolvedUnit = "GBP"
case "¥":
resolvedUnit = "JPY"
}
u, ok := unitRegistry[resolvedUnit]
if !ok {
return Result{}, "", fmt.Errorf("unknown unit: %s", targetUnit)
}
if res.Dim != u.Dimension && res.Dim != DimNone {
return Result{}, "", fmt.Errorf("cannot convert %s to %s", getDimName(res.Dim), targetUnit)
}
if res.Dim == DimTemp {
var convertedVal decimal.Decimal
switch targetUnit {
case "C", "CELSIUS":
convertedVal = res.Value.Sub(d(273.15))
case "F", "FAHRENHEIT":
convertedVal = res.Value.Sub(d(273.15)).Mul(d(1.8)).Add(d(32))
case "K", "KELVIN":
convertedVal = res.Value
default:
return Result{}, "", fmt.Errorf("unknown temperature unit: %s", targetUnit)
}
return Result{convertedVal, u.Dimension}, targetUnit, nil
}
return Result{res.Value.Div(u.Factor), u.Dimension}, targetUnit, nil
}
return res, "", nil
}
func parseIP(s string) (int64, error) {
parts := strings.Split(s, ".")
if len(parts) != 4 {
return 0, fmt.Errorf("invalid IP format")
}
var ip int64
for i := 0; i < 4; i++ {
val, err := strconv.ParseInt(parts[i], 10, 64)
if err != nil || val < 0 || val > 255 {
return 0, fmt.Errorf("invalid IP format")
}
ip = (ip << 8) | val
}
return ip, nil
}
func parseCIDR(s string) (int64, error) {
parts := strings.Split(s, "/")
if len(parts) != 2 {
return 0, fmt.Errorf("invalid CIDR format")
}
ip, err := parseIP(parts[0])
if err != nil {
return 0, err
}
mask, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || mask < 0 || mask > 32 {
return 0, fmt.Errorf("invalid CIDR mask")
}
return (ip << 8) | mask, nil
}
func evalExpr(node Expr, locals map[string]Result) (Result, error) {
switch n := node.(type) {
case *StringExpr:
stringTable = append(stringTable, n.Value)
idx := len(stringTable) - 1
return Result{decimal.NewFromInt(int64(idx)), DimString}, nil
case *NumberExpr:
text := n.Value
if strings.HasPrefix(strings.ToLower(text), "0x") || strings.HasPrefix(strings.ToLower(text), "0b") || strings.HasPrefix(strings.ToLower(text), "0o") {
val, err := strconv.ParseInt(text, 0, 64)
if err != nil {
return Result{}, fmt.Errorf("invalid integer format: %w", err)
}
return Result{decimal.NewFromInt(val), DimNone}, nil
}
val, err := decimal.NewFromString(text)
if err != nil {
return Result{}, fmt.Errorf("invalid number %s: %w", text, err)
}
return Result{val, DimNone}, nil
case *IdentExpr:
upper := strings.ToUpper(n.Name)
switch upper {
case "PI":
return Result{d(math.Pi), DimNone}, nil
case "E":
return Result{d(math.E), DimNone}, nil
}
if locals != nil {
if v, ok := locals[n.Name]; ok {
return v, nil
}
}
if v, ok := variables[n.Name]; ok { // variables are case-sensitive
return v, nil
}
// Currencies shorthand
switch n.Name {
case "$":
upper = "USD"
case "€":
upper = "EUR"
case "£":
upper = "GBP"
case "¥":
upper = "JPY"
}
u, ok := unitRegistry[upper]
if !ok {
return Result{}, fmt.Errorf("unknown unit or identifier: %s", n.Name)
}
return Result{u.Factor, u.Dimension}, nil
case *UnaryExpr:
res, err := evalExpr(n.Expr, locals)
if err != nil {
return Result{}, err
}
if n.Op == "-" {
return Result{res.Value.Neg(), res.Dim}, nil
} else if n.Op == "~" {
val := ^res.Value.IntPart()
return Result{decimal.NewFromInt(val), DimNone}, nil
} else if n.Op == "!" {
if res.Value.IsZero() {
return Result{decimal.NewFromInt(1), DimNone}, nil
}
return Result{decimal.Zero, DimNone}, nil
}
return Result{}, fmt.Errorf("unknown unary op: %s", n.Op)
case *BinaryExpr:
left, err := evalExpr(n.Left, locals)
if err != nil {
return Result{}, err
}
// Handle short-circuiting or specific ops
// Right is evaluated eagerly here for most ops
right, err := evalExpr(n.Right, locals)
if err != nil {
return Result{}, err
}
if n.Op == "implicit_mult" {
// Check if right is a temperature unit
isRightTemp := false
if right.Dim == DimTemp {
if id, ok := n.Right.(*IdentExpr); ok {
upper := strings.ToUpper(id.Name)
switch upper {
case "C", "CELSIUS":
val := left.Value.Add(d(273.15))
return Result{val, DimTemp}, nil
case "F", "FAHRENHEIT":
val := left.Value.Sub(d(32)).Div(d(1.8)).Add(d(273.15))
return Result{val, DimTemp}, nil
}
isRightTemp = true
}
}
if isRightTemp {
return Result{left.Value.Mul(right.Value), DimTemp}, nil // K
}
if left.Dim != DimNone && right.Dim != DimNone {
return Result{}, fmt.Errorf("cannot multiply two units")
}
dim := left.Dim
if dim == DimNone {
dim = right.Dim
}
return Result{left.Value.Mul(right.Value), dim}, nil
}
switch n.Op {
case "==", "!=", "<", ">", "<=", ">=":
if left.Dim != right.Dim && left.Dim != DimNone && right.Dim != DimNone {
return Result{}, fmt.Errorf("dimension mismatch in comparison")
}
cmp := left.Value.Cmp(right.Value)
var isTrue bool
switch n.Op {
case "==": isTrue = cmp == 0
case "!=": isTrue = cmp != 0
case "<": isTrue = cmp < 0
case ">": isTrue = cmp > 0
case "<=": isTrue = cmp <= 0
case ">=": isTrue = cmp >= 0
}
if isTrue {
return Result{decimal.NewFromInt(1), DimNone}, nil
}
return Result{decimal.Zero, DimNone}, nil
case "+", "-":
if left.Dim != right.Dim && left.Dim != DimNone && right.Dim != DimNone {
return Result{}, fmt.Errorf("dimension mismatch: cannot add %s and %s", getDimName(left.Dim), getDimName(right.Dim))
}
dim := left.Dim
if dim == DimNone {
dim = right.Dim
}
if n.Op == "+" {
return Result{left.Value.Add(right.Value), dim}, nil
}
return Result{left.Value.Sub(right.Value), dim}, nil
case "*":
if left.Dim != DimNone && right.Dim != DimNone {
return Result{}, fmt.Errorf("cannot multiply two units")
}
dim := left.Dim
if dim == DimNone {
dim = right.Dim
}
return Result{left.Value.Mul(right.Value), dim}, nil
case "/":
if right.Value.IsZero() {
return Result{}, fmt.Errorf("division by zero")
}
if right.Dim != DimNone {
return Result{}, fmt.Errorf("cannot divide by a unit")
}
return Result{left.Value.Div(right.Value), left.Dim}, nil
case "%":
if right.Value.IsZero() {
return Result{}, fmt.Errorf("division by zero")
}
if left.Dim != DimNone || right.Dim != DimNone {
return Result{}, fmt.Errorf("modulo operator not supported on units")
}
return Result{left.Value.Mod(right.Value), DimNone}, nil
case "**":
f1, _ := left.Value.Float64()
f2, _ := right.Value.Float64()
return Result{d(math.Pow(f1, f2)), DimNone}, nil
case "|":
return Result{decimal.NewFromInt(left.Value.IntPart() | right.Value.IntPart()), DimNone}, nil
case "^":
return Result{decimal.NewFromInt(left.Value.IntPart() ^ right.Value.IntPart()), DimNone}, nil
case "&":
return Result{decimal.NewFromInt(left.Value.IntPart() & right.Value.IntPart()), DimNone}, nil
case "<<":
shift := right.Value.IntPart()
if shift < 0 {
return Result{}, fmt.Errorf("negative shift count")
}
if shift >= 64 {
return Result{decimal.Zero, DimNone}, nil
}
return Result{decimal.NewFromInt(left.Value.IntPart() << uint64(shift)), DimNone}, nil
case ">>":
shift := right.Value.IntPart()
if shift < 0 {
return Result{}, fmt.Errorf("negative shift count")
}
if shift >= 64 {
return Result{decimal.Zero, DimNone}, nil
}
return Result{decimal.NewFromInt(left.Value.IntPart() >> uint64(shift)), DimNone}, nil
}
case *CallExpr:
fName := strings.ToLower(n.Func)
if fName == "if" {
if len(n.Args) != 3 {
return Result{}, fmt.Errorf("if expects 3 arguments: condition, true_val, false_val")
}
cond, err := evalExpr(n.Args[0], locals)
if err != nil {
return Result{}, err
}
if !cond.Value.IsZero() {
return evalExpr(n.Args[1], locals)
}
return evalExpr(n.Args[2], locals)
}
args := make([]Result, len(n.Args))
for i, a := range n.Args {
res, err := evalExpr(a, locals)
if err != nil {
return Result{}, err
}
args[i] = res
}
// Helper to require exactly N arguments
requireArgs := func(count int) error {
if len(args) != count {
return fmt.Errorf("%s expects %d argument(s)", fName, count)
}
return nil
}
switch fName {
case "ip":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimString { return Result{}, fmt.Errorf("ip expects a string") }
str := stringTable[args[0].Value.IntPart()]
val, err := parseIP(str)
if err != nil { return Result{}, err }
return Result{decimal.NewFromInt(val), DimIP}, nil
case "cidr":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimString { return Result{}, fmt.Errorf("cidr expects a string") }
str := stringTable[args[0].Value.IntPart()]
val, err := parseCIDR(str)
if err != nil { return Result{}, err }
return Result{decimal.NewFromInt(val), DimCIDR}, nil
case "network":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimCIDR { return Result{}, fmt.Errorf("network expects a CIDR") }
val := args[0].Value.IntPart()
ip := val >> 8
mask := val & 0xFF
shift := 32 - mask
network := (ip >> shift) << shift
return Result{decimal.NewFromInt(network), DimIP}, nil
case "broadcast":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimCIDR { return Result{}, fmt.Errorf("broadcast expects a CIDR") }
val := args[0].Value.IntPart()
ip := val >> 8
mask := val & 0xFF
shift := 32 - mask
broadcast := ip | ((1 << shift) - 1)
return Result{decimal.NewFromInt(broadcast), DimIP}, nil
case "mask":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimCIDR { return Result{}, fmt.Errorf("mask expects a CIDR") }
val := args[0].Value.IntPart()
mask := val & 0xFF
shift := 32 - mask
maskIp := ((1 << mask) - 1) << shift
return Result{decimal.NewFromInt(int64(maskIp)), DimIP}, nil
case "hosts":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimCIDR { return Result{}, fmt.Errorf("hosts expects a CIDR") }
val := args[0].Value.IntPart()
mask := val & 0xFF
if mask >= 31 {
return Result{decimal.Zero, DimNone}, nil
}
hosts := (1 << (32 - mask)) - 2
return Result{decimal.NewFromInt(int64(hosts)), DimNone}, nil
case "range":
if err := requireArgs(1); err != nil { return Result{}, err }
if args[0].Dim != DimCIDR { return Result{}, fmt.Errorf("range expects a CIDR") }
val := args[0].Value.IntPart()
ip := val >> 8
mask := val & 0xFF
shift := 32 - mask
network := (ip >> shift) << shift
broadcast := ip | ((1 << shift) - 1)
var first, last int64
if mask >= 31 {
first = network
last = broadcast
} else {
first = network + 1
last = broadcast - 1
}
formatIP := func(i int64) string {
return fmt.Sprintf("%d.%d.%d.%d", (i>>24)&0xFF, (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF)
}
str := fmt.Sprintf("%s - %s", formatIP(first), formatIP(last))
stringTable = append(stringTable, str)
return Result{decimal.NewFromInt(int64(len(stringTable) - 1)), DimString}, nil
case "sin", "cos", "tan":
if err := requireArgs(1); err != nil {
return Result{}, err
}
if args[0].Dim != DimNone && args[0].Dim != DimAngle {
return Result{}, fmt.Errorf("%s expects a dimensionless number or angle", fName)
}
val, _ := args[0].Value.Float64()
var res float64
if fName == "sin" { res = math.Sin(val) }
if fName == "cos" { res = math.Cos(val) }
if fName == "tan" { res = math.Tan(val) }
return Result{d(res), DimNone}, nil
case "asin", "acos", "atan":
if err := requireArgs(1); err != nil {
return Result{}, err
}
if args[0].Dim != DimNone {
return Result{}, fmt.Errorf("%s expects a dimensionless number", fName)
}
val, _ := args[0].Value.Float64()
var res float64
if fName == "asin" { res = math.Asin(val) }
if fName == "acos" { res = math.Acos(val) }
if fName == "atan" { res = math.Atan(val) }
return Result{d(res), DimAngle}, nil
case "sinh", "cosh", "tanh":
if err := requireArgs(1); err != nil {
return Result{}, err
}
if args[0].Dim != DimNone {
return Result{}, fmt.Errorf("%s expects a dimensionless number", fName)
}
val, _ := args[0].Value.Float64()
var res float64
if fName == "sinh" { res = math.Sinh(val) }
if fName == "cosh" { res = math.Cosh(val) }
if fName == "tanh" { res = math.Tanh(val) }
return Result{d(res), DimNone}, nil
case "sqrt":
if err := requireArgs(1); err != nil { return Result{}, err }
f, _ := args[0].Value.Float64()
return Result{d(math.Sqrt(f)), DimNone}, nil
case "log":
if err := requireArgs(1); err != nil { return Result{}, err }
f, _ := args[0].Value.Float64()
return Result{d(math.Log10(f)), DimNone}, nil
case "log2":
if err := requireArgs(1); err != nil { return Result{}, err }
f, _ := args[0].Value.Float64()
return Result{d(math.Log2(f)), DimNone}, nil
case "ln":
if err := requireArgs(1); err != nil { return Result{}, err }
f, _ := args[0].Value.Float64()
return Result{d(math.Log(f)), DimNone}, nil
case "exp":
if err := requireArgs(1); err != nil { return Result{}, err }
f, _ := args[0].Value.Float64()
return Result{d(math.Exp(f)), DimNone}, nil
case "abs":
if err := requireArgs(1); err != nil { return Result{}, err }
return Result{args[0].Value.Abs(), DimNone}, nil
case "ceil":
if err := requireArgs(1); err != nil { return Result{}, err }
return Result{args[0].Value.Ceil(), DimNone}, nil
case "floor":
if err := requireArgs(1); err != nil { return Result{}, err }
return Result{args[0].Value.Floor(), DimNone}, nil
case "round":
if err := requireArgs(1); err != nil { return Result{}, err }
return Result{args[0].Value.Round(0), DimNone}, nil
case "pow":
if err := requireArgs(2); err != nil { return Result{}, err }
f1, _ := args[0].Value.Float64()
f2, _ := args[1].Value.Float64()
return Result{d(math.Pow(f1, f2)), DimNone}, nil
case "min":
if len(args) < 1 { return Result{}, fmt.Errorf("min expects at least 1 argument") }
minVal := args[0].Value
for _, arg := range args[1:] {
if arg.Value.LessThan(minVal) { minVal = arg.Value }
}
return Result{minVal, DimNone}, nil
case "max":
if len(args) < 1 { return Result{}, fmt.Errorf("max expects at least 1 argument") }
maxVal := args[0].Value
for _, arg := range args[1:] {
if arg.Value.GreaterThan(maxVal) { maxVal = arg.Value }
}
return Result{maxVal, DimNone}, nil
case "mod":
if err := requireArgs(2); err != nil { return Result{}, err }
return Result{args[0].Value.Mod(args[1].Value), DimNone}, nil
case "fact":
if err := requireArgs(1); err != nil { return Result{}, err }
f, _ := args[0].Value.Float64()
if f < 0 || f != math.Trunc(f) {
return Result{}, fmt.Errorf("fact expects a non-negative integer")
}
fact := decimal.NewFromInt(1)
valInt := args[0].Value.IntPart()
for i := int64(1); i <= valInt; i++ {
fact = fact.Mul(decimal.NewFromInt(i))
}
return Result{fact, DimNone}, nil
default:
if uf, ok := userFuncs[n.Func]; ok {
if len(args) != len(uf.Args) {
return Result{}, fmt.Errorf("%s expects %d argument(s)", n.Func, len(uf.Args))
}
newLocals := make(map[string]Result)
for i, argName := range uf.Args {
newLocals[argName] = args[i]
}
p := NewParser(uf.Expr)
ast, err := p.Parse()
if err != nil {
return Result{}, fmt.Errorf("error in user function %s: %w", n.Func, err)
}
return evalExpr(ast, newLocals)
}
return Result{}, fmt.Errorf("unknown function: %s", fName)
}
}
return Result{}, fmt.Errorf("unknown AST node")
}
// evaluateExpr is a helper wrapper for tests that directly evaluate simple expressions
func evaluateExpr(expr string) (Result, error) {
res, _, err := evaluate(expr)
return res, err
}
+21
View File
@@ -0,0 +1,21 @@
module goca
go 1.26.1
require (
aead.dev/minisign v0.2.0 // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/minio/selfupdate v0.6.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.4.0 // indirect
)
+78
View File
@@ -0,0 +1,78 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+642
View File
@@ -0,0 +1,642 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/chzyer/readline"
)
var assignmentRegex = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$`)
var funcAssignmentRegex = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)\s*=\s*(.*)$`)
func isReservedName(name string) bool {
upper := strings.ToUpper(name)
if upper == "PI" || upper == "E" { return true }
lower := strings.ToLower(name)
switch lower {
case "help", "units", "rates", "base", "clear", "exit", "quit", "var", "cur", "unset", "funcs", "ans", "_":
return true
}
switch lower {
case "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
"sqrt", "log", "log2", "ln", "exp", "abs", "ceil", "floor", "round", "pow", "min", "max", "mod", "fact", "if",
"ip", "cidr", "network", "broadcast", "mask", "hosts", "range":
return true
}
if _, ok := unitRegistry[upper]; ok { return true }
return false
}
func loadVariables(path string) error {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("could not open variables file: %w", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(&variables); err != nil {
return fmt.Errorf("could not decode variables json: %w", err)
}
return nil
}
func saveVariables(path string) error {
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create variables file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(variables); err != nil {
return fmt.Errorf("could not encode variables json: %w", err)
}
return nil
}
func loadUserFuncs(path string) error {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("could not open functions file: %w", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(&userFuncs); err != nil {
return fmt.Errorf("could not decode functions json: %w", err)
}
return nil
}
func saveUserFuncs(path string) error {
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create functions file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(userFuncs); err != nil {
return fmt.Errorf("could not encode functions json: %w", err)
}
return nil
}
func printHelp() {
fmt.Println("\033[1;34mgoca Help\033[0m")
fmt.Println("\033[1mArithmetic & Operators:\033[0m")
fmt.Println(" Basic: +, -, *, /, % (modulo)")
fmt.Println(" Power: a ** b or pow(a, b)")
fmt.Println(" Bitwise: & (AND), | (OR), ^ (XOR), ~ (NOT), << (Left Shift), >> (Right Shift)")
fmt.Println(" Logic: ==, !=, <, >, <=, >=, ! (NOT)")
fmt.Println(" Implicit: 2(3 + 4) or 2m (implicit multiplication)")
fmt.Println(" Numbers: Decimal (10), Hex (0xFF), Binary (0b1010), Octal (0o77)")
fmt.Println(" Variables: Assign (e.g. x = 5), Use 'ans' or '_' for last result")
fmt.Println(" Custom Fn: f(x, y) = x * y + 2")
fmt.Println("\033[1mScientific Functions:\033[0m")
fmt.Println(" Trig: sin(x), cos(x), tan(x) (accepts angles like 90 deg or pi rad)")
fmt.Println(" Inv Trig: asin(x), acos(x), atan(x) (returns angles, e.g., asin(1) to deg)")
fmt.Println(" Hyper: sinh(x), cosh(x), tanh(x)")
fmt.Println(" General: sqrt(x), abs(x), exp(x), ln(x) (natural), log(x) (base 10), log2(x)")
fmt.Println(" Rounding: ceil(x), floor(x), round(x)")
fmt.Println(" Stats: min(a, b, ...), max(a, b, ...), mod(a, b), fact(x) (factorial)")
fmt.Println(" Logic: if(cond, true_val, false_val)")
fmt.Println(" Constants: PI, E")
fmt.Println("\033[1mIP & Subnet:\033[0m")
fmt.Println(" Parse: ip(\"192.168.1.1\"), cidr(\"10.0.0.0/24\")")
fmt.Println(" Subnet: network(c), broadcast(c), mask(c), hosts(c), range(c)")
fmt.Println("\033[1mUnits & Conversions:\033[0m")
fmt.Println(" Syntax: <value> <unit> to/in <unit> (e.g., 10 mi to km, 1 GB in MB)")
fmt.Println(" Types: Length, Mass, Time, Digital, Area, Temperature, Angle")
fmt.Println(" Example: 0 C to K, 90 deg to rad, 32 F to C")
fmt.Println("\033[1mCurrencies (Live Rates):\033[0m")
fmt.Println(" Usage: Exchange currency codes (e.g., 100 USD to EUR)")
fmt.Println(" Symbols: $, €, £, ¥ (e.g., 10$ to €)")
fmt.Println("\033[1mCommands:\033[0m")
fmt.Println(" help Show this help information")
fmt.Println(" units List all supported measurement units")
fmt.Println(" rates Show common currency exchange rates relative to base")
fmt.Println(" cur List all supported live currency codes")
fmt.Println(" var List all user-defined variables")
fmt.Println(" funcs List all user-defined functions")
fmt.Println(" unset <v> Delete variable <v> (or 'unset *' to delete all)")
fmt.Println(" base <C> Change base currency (e.g., base USD)")
fmt.Println(" clear Clear the terminal screen")
fmt.Println(" exit/quit Exit goca")
}
type CalcCompleter struct{}
func (c *CalcCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
var wordStart = pos
for wordStart > 0 {
ch := line[wordStart-1]
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' {
wordStart--
} else {
break
}
}
word := string(line[wordStart:pos])
if word == "" {
return nil, 0
}
allCandidates := []string{
"help", "units", "rates", "cur", "var", "funcs", "clear", "exit", "quit", "base", "unset",
"sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
"sqrt", "log", "log2", "ln", "exp", "abs", "ceil", "floor", "round", "pow", "min", "max", "mod", "fact", "if",
"ip", "cidr", "network", "broadcast", "mask", "hosts", "range",
"PI", "E", "ans", "_",
}
for u := range unitRegistry {
allCandidates = append(allCandidates, strings.ToLower(u))
allCandidates = append(allCandidates, u)
}
var matches []string
seen := make(map[string]bool)
for _, cand := range allCandidates {
if strings.HasPrefix(strings.ToLower(cand), strings.ToLower(word)) {
if !seen[cand] {
seen[cand] = true
matches = append(matches, cand)
}
}
}
for v := range variables {
if strings.HasPrefix(strings.ToLower(v), strings.ToLower(word)) {
if !seen[v] {
seen[v] = true
matches = append(matches, v)
}
for uf := range userFuncs {
if strings.HasPrefix(strings.ToLower(uf), strings.ToLower(word)) {
if !seen[uf] {
seen[uf] = true
matches = append(matches, uf)
}
}
}
}
}
sort.Strings(matches)
var ret [][]rune
for _, m := range matches {
suffix := m[len(word):]
isFunc := false
switch m {
case "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
"sqrt", "log", "log2", "ln", "exp", "abs", "ceil", "floor", "round", "pow", "min", "max", "mod", "fact", "if",
"ip", "cidr", "network", "broadcast", "mask", "hosts", "range":
isFunc = true
}
if isFunc || func() bool { _, ok := userFuncs[m]; return ok }() {
suffix += "("
}
ret = append(ret, []rune(suffix))
}
return ret, len(word)
}
func printResult(res Result, target string) {
if res.Dim == DimString {
fmt.Printf("\033[1;33m= \"%s\"\033[0m\n", stringTable[res.Value.IntPart()])
return
}
val := res.Value
if target != "" {
fmt.Printf("\033[1;33m= %v %s\033[0m\n", val, target)
} else if res.Dim == DimCurrency {
rateToBase := d(1.0).Div(eurRatesLookup(baseCurrency))
result := val.Mul(rateToBase)
if result.IsInteger() {
i := result.IntPart()
fmt.Printf("\033[1;33m%d %s\033[0m", i, baseCurrency)
if baseCurrency == "EUR" || baseCurrency == "USD" {
fmt.Printf(" (Hex: 0x%X, Bin: 0b%b)", i, i)
}
fmt.Println()
} else {
f, _ := result.Float64()
fmt.Printf("\033[1;33m= %.4f %s\033[0m\n", f, baseCurrency)
}
} else if res.Dim == DimIP {
i := val.IntPart()
fmt.Printf("\033[1;33m= %d.%d.%d.%d\033[0m\n", (i>>24)&0xFF, (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF)
} else if res.Dim == DimCIDR {
i := val.IntPart()
ipPart := i >> 8
mask := i & 0xFF
fmt.Printf("\033[1;33m= %d.%d.%d.%d/%d\033[0m\n", (ipPart>>24)&0xFF, (ipPart>>16)&0xFF, (ipPart>>8)&0xFF, ipPart&0xFF, mask)
} else if res.Dim != DimNone {
label := ""
for _, v := range unitRegistry {
if v.Dimension == res.Dim && v.Factor.Equal(d(1.0)) {
label = v.Label
break
}
}
fmt.Printf("\033[1;33m= %v %s\033[0m\n", val, label)
} else {
if val.IsInteger() {
i := val.IntPart()
fmt.Printf("\033[1;33m%d\033[0m (0x%X, 0b%b)\n", i, i, i)
} else {
fmt.Printf("\033[1;33m= %v\033[0m\n", val)
}
}
}
func handleLine(line string, useReadline bool, variablesFile string, functionsFile string) bool {
line = strings.TrimSpace(line)
if line == "" {
return false
}
lower := strings.ToLower(line)
if lower == "exit" || lower == "quit" {
return true
}
if lower == "clear" {
if useReadline {
fmt.Print("\033[2J\033[H")
}
return false
}
if lower == "help" {
printHelp()
return false
}
if lower == "units" {
fmt.Println("Supported Units:")
fmt.Println(" Length: km, m, dm, cm, mm, um, nm, mi, nmi, ft, in, yd")
fmt.Println(" Mass: t, kg, g, mg, ct, lb, oz")
fmt.Println(" Time: yr, wk, d, h, min, s, ms, us, ns")
fmt.Println(" Digital: EB, PB, TB, GB, MB, KB, B")
fmt.Println(" Area: km2, ha, acre, m2, cm2, mm2")
fmt.Println(" Temperature: K, C, F")
fmt.Println(" Angle: rad, deg, grad")
return false
}
if lower == "rates" {
fmt.Printf("Exchange rates (relative to %s):\n", baseCurrency)
rateToBase := d(1.0).Div(eurRatesLookup(baseCurrency))
for _, c := range []string{"EUR", "USD", "GBP", "JPY", "CHF", "CNY"} {
if r, ok := unitRegistry[c]; ok {
f, _ := r.Factor.Mul(rateToBase).Float64()
fmt.Printf(" 1 %s = %.4f %s\n", c, f, baseCurrency)
}
}
return false
}
if lower == "cur" {
var currencies []string
for code, u := range unitRegistry {
if u.Dimension == DimCurrency {
currencies = append(currencies, code)
}
}
sort.Strings(currencies)
fmt.Println("Supported Currencies:")
fmt.Println(" " + strings.Join(currencies, ", "))
return false
}
if lower == "funcs" {
if len(userFuncs) == 0 {
fmt.Println("No user-defined functions.")
return false
}
fmt.Println("Defined Functions:")
var keys []string
for k := range userFuncs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, name := range keys {
uf := userFuncs[name]
fmt.Printf(" %s(%s) = %s\n", name, strings.Join(uf.Args, ", "), uf.Expr)
}
return false
}
if lower == "var" {
if len(variables) == 0 {
fmt.Println("No variables defined.")
return false
}
fmt.Println("Defined Variables:")
var keys []string
for k := range variables {
keys = append(keys, k)
}
sort.Strings(keys)
for _, name := range keys {
res := variables[name]
if res.Dim == DimCurrency {
fmt.Printf(" %s = %v (Currency)\n", name, res.Value)
} else if res.Dim != DimNone {
label := ""
for _, v := range unitRegistry {
if v.Dimension == res.Dim && v.Factor.Equal(d(1.0)) {
label = v.Label
break
}
}
fmt.Printf(" %s = %v %s\n", name, res.Value, label)
} else {
fmt.Printf(" %s = %v\n", name, res.Value)
}
}
return false
}
if strings.HasPrefix(lower, "base ") {
newBase := strings.TrimSpace(line[5:])
switch newBase {
case "$":
newBase = "USD"
case "€":
newBase = "EUR"
case "£":
newBase = "GBP"
case "¥":
newBase = "JPY"
default:
newBase = strings.ToUpper(newBase)
}
if _, ok := unitRegistry[newBase]; ok && unitRegistry[newBase].Dimension == DimCurrency {
baseCurrency = newBase
fmt.Printf("Base set to %s\n", baseCurrency)
} else {
fmt.Printf("Error: Unknown currency %s\n", newBase)
}
return false
}
if strings.HasPrefix(lower, "unset ") {
target := strings.TrimSpace(line[6:])
if target == "*" || strings.ToLower(target) == "all" {
if len(variables) == 0 && len(userFuncs) == 0 {
fmt.Println("No variables or functions defined.")
} else {
variables = make(map[string]Result)
if err := saveVariables(variablesFile); err != nil {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to clear persisted variables: %v\n", err)
}
userFuncs = make(map[string]UserFunc)
if err := saveUserFuncs(functionsFile); err != nil {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to clear persisted functions: %v\n", err)
}
fmt.Println("All variables and functions deleted.")
}
} else {
found := false
if _, ok := variables[target]; ok {
delete(variables, target)
if err := saveVariables(variablesFile); err != nil {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist variables: %v\n", err)
}
fmt.Printf("Variable '%s' deleted.\n", target)
found = true
}
if _, ok := userFuncs[target]; ok {
delete(userFuncs, target)
if err := saveUserFuncs(functionsFile); err != nil {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist functions: %v\n", err)
}
fmt.Printf("Function '%s' deleted.\n", target)
found = true
}
if !found {
fmt.Printf("\033[1;31mError:\033[0m Variable or function '%s' not found.\n", target)
}
}
return false
}
if matches := funcAssignmentRegex.FindStringSubmatch(line); matches != nil {
name := matches[1]
argsStr := matches[2]
exprStr := matches[3]
if isReservedName(name) {
fmt.Printf("\033[1;31mError:\033[0m %s is a reserved name (constant, unit, function, or command)\n", name)
return false
}
var args []string
for _, a := range strings.Split(argsStr, ",") {
a = strings.TrimSpace(a)
if a != "" {
args = append(args, a)
}
}
p := NewParser(exprStr)
_, err := p.Parse()
if err != nil {
fmt.Printf("\033[1;31mError:\033[0m Failed to parse function body: %v\n", err)
return false
}
userFuncs[name] = UserFunc{Args: args, Expr: exprStr}
if err := saveUserFuncs(functionsFile); err != nil {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist functions: %v\n", err)
}
fmt.Printf("\033[1;33mDefined function %s(%s)\033[0m\n", name, strings.Join(args, ", "))
return false
}
if matches := assignmentRegex.FindStringSubmatch(line); matches != nil {
name := matches[1]
expr := matches[2]
if isReservedName(name) {
fmt.Printf("\033[1;31mError:\033[0m %s is a reserved name (constant, unit, function, or command)\n", name)
return false
}
res, target, err := evaluate(expr)
if err != nil {
fmt.Printf("\033[1;31mError:\033[0m %v\n", err)
return false
}
variables[name] = res
variables["ans"] = res
variables["_"] = res
if err := saveVariables(variablesFile); err != nil {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist variables: %v\n", err)
}
if target != "" {
fmt.Printf("\033[1;33m%s = %v %s\033[0m\n", name, res.Value, target)
} else if res.Dim == DimCurrency {
rateToBase := d(1.0).Div(eurRatesLookup(baseCurrency))
result := res.Value.Mul(rateToBase)
if result.IsInteger() {
fmt.Printf("\033[1;33m%s = %d %s\033[0m\n", name, result.IntPart(), baseCurrency)
} else {
f, _ := result.Float64()
fmt.Printf("\033[1;33m%s = %.4f %s\033[0m\n", name, f, baseCurrency)
}
} else if res.Dim != DimNone {
label := ""
for _, v := range unitRegistry {
if v.Dimension == res.Dim && v.Factor.Equal(d(1.0)) {
label = v.Label
break
}
}
fmt.Printf("\033[1;33m%s = %v %s\033[0m\n", name, res.Value, label)
} else {
if res.Value.IsInteger() {
fmt.Printf("\033[1;33m%s = %d\033[0m\n", name, res.Value.IntPart())
} else {
fmt.Printf("\033[1;33m%s = %v\033[0m\n", name, res.Value)
}
}
return false
}
res, target, err := evaluate(line)
if err != nil {
fmt.Printf("\033[1;31mError:\033[0m %v\n", err)
return false
}
variables["ans"] = res
variables["_"] = res
printResult(res, target)
return false
}
func main() {
initUnits()
optY := false
newArgs := []string{os.Args[0]}
for i := 1; i < len(os.Args); i++ {
if os.Args[i] == "-y" {
optY = true
} else {
newArgs = append(newArgs, os.Args[i])
}
}
os.Args = newArgs
if !optY {
checkforupdate(UPDATEURL)
}
hasArgs := len(os.Args) > 1
if err := fetchRates(); err != nil && !hasArgs {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to fetch rates: %v\n", err)
}
home, _ := os.UserHomeDir()
historyFile := filepath.Join(home, ".goca_history")
variablesFile := filepath.Join(home, ".goca_variables.json")
functionsFile := filepath.Join(home, ".goca_functions.json")
if err := loadUserFuncs(functionsFile); err != nil && !hasArgs {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to load persisted functions: %v\n", err)
}
if err := loadVariables(variablesFile); err != nil && !hasArgs {
fmt.Printf("\033[1;31mWarning:\033[0m Failed to load persisted variables: %v\n", err)
}
if hasArgs {
// Check for -p or --port flag
for i := 1; i < len(os.Args); i++ {
arg := os.Args[i]
if arg == "-p" || arg == "--port" {
if i+1 < len(os.Args) {
port := os.Args[i+1]
startWebServer(port)
return
} else {
fmt.Println("\033[1;31mError:\033[0m Missing port number after -p/--port")
return
}
}
}
input := strings.Join(os.Args[1:], " ")
handleLine(input, false, variablesFile, functionsFile)
return
}
var useReadline = true
if fi, err := os.Stdin.Stat(); err == nil && (fi.Mode()&os.ModeCharDevice) == 0 {
useReadline = false
}
var rl *readline.Instance
if useReadline {
var err error
rl, err = readline.NewEx(&readline.Config{
Prompt: "\033[1;32mgoca>\033[0m ",
HistoryFile: historyFile,
InterruptPrompt: "^C",
EOFPrompt: "exit",
AutoComplete: &CalcCompleter{},
})
if err != nil {
useReadline = false
} else {
defer rl.Close()
}
}
if useReadline {
fmt.Printf("\033[1;34mgoca v%s\033[0m, type 'help' for examples.\n", Version)
}
var scanner *bufio.Scanner
if !useReadline {
scanner = bufio.NewScanner(os.Stdin)
}
for {
var line string
if useReadline {
var err error
line, err = rl.Readline()
if err != nil {
break
}
} else {
if !scanner.Scan() {
break
}
line = scanner.Text()
}
if handleLine(line, useReadline, variablesFile, functionsFile) {
break
}
}
}
+295
View File
@@ -0,0 +1,295 @@
package main
import (
"encoding/json"
"math"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/shopspring/decimal"
)
func TestEvaluateExpr(t *testing.T) {
initUnits()
tests := []struct {
expr string
wantVal float64
wantErr bool
}{
// Basic arithmetic
{"2 + 3", 5, false},
{"2 - 3", -1, false},
{"2 * 3", 6, false},
{"6 / 3", 2, false},
{"2 * 3 + 4", 10, false},
{"2 + 3 * 4", 14, false},
{"(2 + 3) * 4", 20, false},
{"-5 + 10", 5, false},
{"-(5 + 10)", -15, false},
// Division by zero
{"6 / 0", 0, true},
// Hex, binary, and octal literals
{"0xFF", 255, false},
{"0b1010", 10, false},
{"0o77", 63, false},
{"0XF", 15, false},
{"0B11", 3, false},
{"0O10", 8, false},
// Exponentiation (**)
{"2 ** 3", 8, false},
{"2 ** 3 ** 2", 512, false},
{"2 * 3 ** 2", 18, false},
{"3 ** 2 * 2", 18, false},
// Scientific functions
{"abs(-5)", 5, false},
{"sqrt(9)", 3, false},
{"ceil(4.2)", 5, false},
{"floor(4.8)", 4, false},
{"round(4.5)", 5, false},
{"sin(0)", 0, false},
{"cos(0)", 1, false},
{"tan(0)", 0, false},
{"asin(0)", 0, false},
{"acos(1)", 0, false},
{"atan(0)", 0, false},
{"sinh(0)", 0, false},
{"cosh(0)", 1, false},
{"tanh(0)", 0, false},
{"ln(E)", 1, false},
{"log(10)", 1, false},
{"log2(8)", 3, false},
{"exp(1)", math.E, false},
{"pow(2, 3)", 8, false},
{"pow(2 + 1, 3)", 27, false},
{"min(5, 2, 7)", 2, false},
{"max(1, 8, 3)", 8, false},
{"mod(10, 3)", 1, false},
{"fact(5)", 120, false},
{"fact(0)", 1, false},
// Function error cases
{"fact(-1)", 0, true},
{"fact(3.5)", 0, true},
// Constants
{"PI", math.Pi, false},
{"E", math.E, false},
// Bitwise
{"5 & 3", 1, false}, // 101 & 011 = 001
{"5 | 3", 7, false}, // 101 | 011 = 111
{"5 ^ 3", 6, false}, // 101 ^ 011 = 110
{"~5", -6, false},
{"1 << 8", 256, false},
{"1024 >> 2", 256, false},
{"1 << 2 + 3", 32, false}, // precedence test: 1 << (2 + 3)
{"1 << -1", 0, true}, // error: negative shift count
// Modulo operator
{"10 % 3", 1, false},
{"10 % 0", 0, true},
// Implicit multiplication with units and parentheses
{"2m", 2, false},
{"2 m", 2, false},
{"2km", 2000, false},
{"2(3 + 4)", 14, false},
{"(2 + 3)(4 + 5)", 45, false},
}
for _, tt := range tests {
res, err := evaluateExpr(tt.expr)
if (err != nil) != tt.wantErr {
t.Errorf("evaluateExpr(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr)
continue
}
if !tt.wantErr {
f, _ := res.Value.Float64()
if math.Abs(f-tt.wantVal) > 1e-9 {
t.Errorf("evaluateExpr(%q) = %v, want %v", tt.expr, f, tt.wantVal)
}
}
}
}
func TestEvaluateConversions(t *testing.T) {
initUnits()
unitRegistry["USD"] = UnitInfo{Factor: decimal.NewFromFloat(0.85), Dimension: DimCurrency, Label: "USD"}
unitRegistry["EUR"] = UnitInfo{Factor: decimal.NewFromFloat(1.0), Dimension: DimCurrency, Label: "EUR"}
tests := []struct {
expr string
wantVal float64
wantUnit string
wantErr bool
}{
// Unit conversions (Length)
{"10 mi to km", 16.09344, "KM", false},
{"10 dm to m", 1, "M", false},
{"1e9 nm to m", 1, "M", false},
{"1000 um to mm", 1, "MM", false},
{"1 nmi to m", 1852, "M", false},
// Unit conversions (Mass)
{"1000g in kg", 1, "KG", false},
{"1 t to kg", 1000, "KG", false},
{"5 ct to g", 1, "G", false},
// Unit conversions (Time)
{"1h to min", 60, "MIN", false},
{"1e9 ns to s", 1, "S", false},
{"1 wk to d", 7, "D", false},
{"1 yr to d", 365, "D", false},
// Unit conversions (Digital)
{"1 GB in MB", 1024, "MB", false},
{"1 PB to TB", 1024, "TB", false},
{"1 EB to PB", 1024, "PB", false},
// Unit conversions (Area)
{"1 ha to m2", 10000, "M2", false},
{"10000 m2 in ha", 1, "HA", false},
// Currency conversions
{"10 USD to EUR", 8.5, "EUR", false},
{"10$ to EUR", 8.5, "EUR", false},
{"10 USD to €", 8.5, "€", false},
{"10$ to €", 8.5, "€", false},
// Temperature conversions
{"0 C to K", 273.15, "K", false},
{"100 C in F", 212, "F", false},
{"32 F to C", 0, "C", false},
{"293.15 K to C", 20, "C", false},
{"20 C + 5 K", 298.15, "", false}, // evaluates internally to 298.15 K
{"20 CELSIUS to KELVIN", 293.15, "KELVIN", false},
// Angle conversions & trigonometry
{"90 deg to rad", math.Pi / 2, "RAD", false},
{"180 deg in rad", math.Pi, "RAD", false},
{"pi rad to deg", 180, "DEG", false},
{"sin(90 deg)", 1, "", false},
{"cos(pi rad)", -1, "", false},
{"asin(1) to deg", 90, "DEG", false},
{"asin(0.5) to deg", 30, "DEG", false},
// Error cases
{"10m to kg", 0, "", true},
{"10 USD to m", 0, "", true},
{"10m + 5kg", 0, "", true},
{"20 C to m", 0, "", true},
{"10m / 2s", 0, "", true},
{"10 / (2m)", 0, "", true},
{"10m % 3", 0, "", true},
{"sin(10m)", 0, "", true},
{"asin(10m)", 0, "", true},
}
for _, tt := range tests {
res, target, err := evaluate(tt.expr)
if (err != nil) != tt.wantErr {
t.Errorf("evaluate(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr)
continue
}
if !tt.wantErr {
if target != tt.wantUnit {
t.Errorf("evaluate(%q) target unit = %q, want %q", tt.expr, target, tt.wantUnit)
}
f, _ := res.Value.Float64()
if math.Abs(f-tt.wantVal) > 1e-9 {
t.Errorf("evaluate(%q) = %v, want %v", tt.expr, f, tt.wantVal)
}
}
}
}
func TestVariableAssignments(t *testing.T) {
initUnits()
variables = make(map[string]Result)
variables["x"] = Result{d(5), DimNone}
res, err := evaluateExpr("x * 2")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !res.Value.Equal(d(10)) {
t.Errorf("x * 2 = %v, want 10", res.Value)
}
variables["y"] = Result{d(10), DimLength}
val, target, err := evaluate("y to cm")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if target != "CM" || !val.Value.Equal(d(1000)) {
t.Errorf("evaluate('y to cm') = %v %s, want 1000 CM", val.Value, target)
}
}
func TestFetchRatesMock(t *testing.T) {
initUnits()
mockResponse := struct {
Rates map[string]float64 `json:"rates"`
}{
Rates: map[string]float64{
"USD": 1.1,
"GBP": 0.85,
},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(mockResponse)
}))
defer server.Close()
oldRatesAPIURL := ratesAPIURL
ratesAPIURL = server.URL
defer func() { ratesAPIURL = oldRatesAPIURL }()
delete(unitRegistry, "USD")
delete(unitRegistry, "GBP")
// remove old cache file to force fetch
home, _ := os.UserHomeDir()
os.Remove(filepath.Join(home, ".goca_rates.json"))
err := fetchRates()
if err != nil {
t.Fatalf("fetchRates failed: %v", err)
}
usd, ok := unitRegistry["USD"]
if !ok {
t.Fatalf("USD was not added to unitRegistry")
}
expectedUSD := d(1.0).Div(d(1.1))
if !usd.Factor.Equal(expectedUSD) {
t.Errorf("USD factor = %v, want %v", usd.Factor, expectedUSD)
}
}
func FuzzEvaluateExpr(f *testing.F) {
initUnits()
// Add seed corpus
f.Add("2 + 3")
f.Add("sin(pi rad)")
f.Add("10 km to mi")
f.Add("0xFF + 0b10")
f.Add("x = 5")
f.Add("1 << 4")
f.Add("((2+3)*4)/5")
f.Fuzz(func(t *testing.T, expr string) {
// Fuzzing ensures no panics occur on malformed input
evaluate(expr)
})
}
Executable
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Script to sign and notarize goca Darwin binaries
set -e
# Make sure we are in the script's directory
cd "$(dirname "$0")"
# Validate required variables
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_PASSWORD" ] || [ -z "$TEAM_ID" ]; then
echo "❌ Error: Please set the required environment variables: APPLE_ID, APPLE_PASSWORD, and TEAM_ID."
echo ""
echo "Example usage:"
echo " export APPLE_ID='mw@pstbx.org'"
echo " export APPLE_PASSWORD='your-app-specific-password'"
echo " export TEAM_ID='26TUG6V94S'"
echo " ./notarize.sh"
exit 1
fi
SIGNING_IDENTITY="Developer ID Application: Mike Wesemann ($TEAM_ID)"
# Verify binaries exist
if [ ! -f "dist/goca-darwin-amd64" ] || [ ! -f "dist/goca-darwin-arm64" ]; then
echo "❌ Error: Darwin binaries not found in 'dist/'. Run ./build_releases.sh first."
exit 1
fi
echo "🔐 Step 1: Codesigning Darwin binaries..."
codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" dist/goca-darwin-amd64
codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" dist/goca-darwin-arm64
echo "📦 Step 2: Packaging binaries into ZIP archives..."
mkdir -p dist/notarize
rm -f dist/notarize/*.zip
ditto -c -k --keepParent dist/goca-darwin-amd64 dist/notarize/goca-darwin-amd64.zip
ditto -c -k --keepParent dist/goca-darwin-arm64 dist/notarize/goca-darwin-arm64.zip
echo "🚀 Step 3: Submitting macOS Intel binary (amd64) to Apple Notarization..."
xcrun notarytool submit dist/notarize/goca-darwin-amd64.zip \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$TEAM_ID" \
--wait
echo "🚀 Step 4: Submitting macOS Apple Silicon binary (arm64) to Apple Notarization..."
xcrun notarytool submit dist/notarize/goca-darwin-arm64.zip \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$TEAM_ID" \
--wait
echo "🎉 Notarization successfully completed for both macOS binaries!"
+341
View File
@@ -0,0 +1,341 @@
package main
import (
"fmt"
"strings"
"text/scanner"
)
// Parser parses a string into an AST.
type Parser struct {
s scanner.Scanner
}
func NewParser(expr string) *Parser {
// Preprocess multi-char operators to single chars so the scanner reads them easily
expr = strings.ReplaceAll(expr, "**", "\x01")
expr = strings.ReplaceAll(expr, "<<", "\x02")
expr = strings.ReplaceAll(expr, ">>", "\x03")
expr = strings.ReplaceAll(expr, "==", "\x04")
expr = strings.ReplaceAll(expr, "!=", "\x05")
expr = strings.ReplaceAll(expr, "<=", "\x06")
expr = strings.ReplaceAll(expr, ">=", "\x07")
p := &Parser{}
p.s.Init(strings.NewReader(expr))
p.s.Mode = scanner.ScanInts | scanner.ScanFloats | scanner.ScanIdents | scanner.ScanStrings
p.s.Error = func(s *scanner.Scanner, msg string) {}
return p
}
func (p *Parser) skipWhitespace() {
for {
ch := p.s.Peek()
if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' {
p.s.Next()
continue
}
break
}
}
// Parse parses the entire expression.
func (p *Parser) Parse() (Expr, error) {
expr, err := p.parseExpression()
if err != nil {
return nil, err
}
p.skipWhitespace()
if p.s.Peek() != scanner.EOF {
return nil, fmt.Errorf("unexpected trailing characters")
}
return expr, nil
}
func (p *Parser) parseExpression() (Expr, error) {
return p.parseEquality()
}
func (p *Parser) parseEquality() (Expr, error) {
res, err := p.parseRelational()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
ch := p.s.Peek()
if ch == '\x04' {
p.s.Next()
rhs, err := p.parseRelational()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "==", Left: res, Right: rhs}
} else if ch == '\x05' {
p.s.Next()
rhs, err := p.parseRelational()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "!=", Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parseRelational() (Expr, error) {
res, err := p.parseXor()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
ch := p.s.Peek()
if ch == '\x06' {
p.s.Next()
rhs, err := p.parseXor()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "<=", Left: res, Right: rhs}
} else if ch == '\x07' {
p.s.Next()
rhs, err := p.parseXor()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: ">=", Left: res, Right: rhs}
} else if ch == '<' {
p.s.Next()
rhs, err := p.parseXor()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "<", Left: res, Right: rhs}
} else if ch == '>' {
p.s.Next()
rhs, err := p.parseXor()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: ">", Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parseXor() (Expr, error) {
res, err := p.parseAnd()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
if p.s.Peek() == '|' || p.s.Peek() == '^' {
op := string(p.s.Next())
rhs, err := p.parseAnd()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: op, Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parseAnd() (Expr, error) {
res, err := p.parseShift()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
if p.s.Peek() == '&' {
p.s.Next()
rhs, err := p.parseShift()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "&", Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parseShift() (Expr, error) {
res, err := p.parseSum()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
ch := p.s.Peek()
if ch == '\x02' {
p.s.Next()
rhs, err := p.parseSum()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "<<", Left: res, Right: rhs}
} else if ch == '\x03' {
p.s.Next()
rhs, err := p.parseSum()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: ">>", Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parseSum() (Expr, error) {
res, err := p.parseTerm()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
ch := p.s.Peek()
if ch == '+' || ch == '-' {
op := string(p.s.Next())
rhs, err := p.parseTerm()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: op, Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parseTerm() (Expr, error) {
res, err := p.parsePower()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
ch := p.s.Peek()
if ch == '*' || ch == '/' || ch == '%' {
op := string(p.s.Next())
rhs, err := p.parsePower()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: op, Left: res, Right: rhs}
} else if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '$' || ch == '€' || ch == '£' || ch == '¥' || ch == '(' {
// Implicit multiplication
rhs, err := p.parseFactor()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "implicit_mult", Left: res, Right: rhs}
} else {
return res, nil
}
}
}
func (p *Parser) parsePower() (Expr, error) {
res, err := p.parseFactor()
if err != nil {
return nil, err
}
p.skipWhitespace()
if p.s.Peek() == '\x01' {
p.s.Next()
rhs, err := p.parsePower()
if err != nil {
return nil, err
}
res = &BinaryExpr{Op: "**", Left: res, Right: rhs}
}
return res, nil
}
func (p *Parser) parseFactor() (Expr, error) {
tok := p.s.Scan()
switch tok {
case '(':
res, err := p.parseExpression()
if err != nil {
return nil, err
}
if p.s.Scan() != ')' {
return nil, fmt.Errorf("missing )")
}
return res, nil
case '-':
res, err := p.parseFactor()
if err != nil {
return nil, err
}
return &UnaryExpr{Op: "-", Expr: res}, nil
case '~', '!':
op := string(tok)
res, err := p.parseFactor()
if err != nil {
return nil, err
}
return &UnaryExpr{Op: op, Expr: res}, nil
case '$':
return &IdentExpr{Name: "$"}, nil
case '€':
return &IdentExpr{Name: "€"}, nil
case '£':
return &IdentExpr{Name: "£"}, nil
case '¥':
return &IdentExpr{Name: "¥"}, nil
case scanner.String:
text := p.s.TokenText()
text = text[1 : len(text)-1]
return &StringExpr{Value: text}, nil
case scanner.Int, scanner.Float, scanner.Ident:
text := p.s.TokenText()
if tok == scanner.Ident {
p.skipWhitespace()
if p.s.Peek() == '(' {
p.s.Next() // consume '('
var args []Expr
for {
p.skipWhitespace()
if p.s.Peek() == ')' {
p.s.Next()
break
}
arg, err := p.parseExpression()
if err != nil {
return nil, err
}
args = append(args, arg)
p.skipWhitespace()
nextChar := p.s.Peek()
if nextChar == ',' {
p.s.Next()
continue
} else if nextChar == ')' {
p.s.Next()
break
} else {
return nil, fmt.Errorf("missing ) or comma in function arguments")
}
}
return &CallExpr{Func: text, Args: args}, nil
}
return &IdentExpr{Name: text}, nil
}
return &NumberExpr{Value: text}, nil
default:
txt := p.s.TokenText()
if tok == scanner.EOF {
return nil, fmt.Errorf("unexpected end of expression")
}
return nil, fmt.Errorf("unexpected %s", txt)
}
}
+40
View File
@@ -0,0 +1,40 @@
package main
import "github.com/shopspring/decimal"
// Dimension represents the physical dimension of a unit.
type Dimension int
const (
DimNone Dimension = iota
DimCurrency
DimLength
DimMass
DimTime
DimDigital
DimArea
DimTemp
DimAngle
DimIP
DimCIDR
DimString
)
// Result holds the evaluated numeric value and its dimension.
type Result struct {
Value decimal.Decimal
Dim Dimension
}
// UnitInfo stores the conversion factor (to the pivot unit) and its dimension.
type UnitInfo struct {
Factor decimal.Decimal
Dimension Dimension
Label string
}
// UserFunc represents a user-defined function.
type UserFunc struct {
Args []string `json:"args"`
Expr string `json:"expr"`
}
+108
View File
@@ -0,0 +1,108 @@
package main
import (
"math"
"github.com/shopspring/decimal"
)
var unitRegistry = make(map[string]UnitInfo)
func d(f float64) decimal.Decimal {
return decimal.NewFromFloat(f)
}
// initUnits populates the unitRegistry with supported units.
func initUnits() {
// Length (Pivot: meter)
unitRegistry["M"] = UnitInfo{d(1.0), DimLength, "m"}
unitRegistry["DM"] = UnitInfo{d(0.1), DimLength, "dm"}
unitRegistry["CM"] = UnitInfo{d(0.01), DimLength, "cm"}
unitRegistry["MM"] = UnitInfo{d(0.001), DimLength, "mm"}
unitRegistry["UM"] = UnitInfo{d(1e-6), DimLength, "um"}
unitRegistry["NM"] = UnitInfo{d(1e-9), DimLength, "nm"}
unitRegistry["KM"] = UnitInfo{d(1000.0), DimLength, "km"}
unitRegistry["IN"] = UnitInfo{d(0.0254), DimLength, "in"}
unitRegistry["FT"] = UnitInfo{d(0.3048), DimLength, "ft"}
unitRegistry["YD"] = UnitInfo{d(0.9144), DimLength, "yd"}
unitRegistry["MI"] = UnitInfo{d(1609.344), DimLength, "mi"}
unitRegistry["NMI"] = UnitInfo{d(1852.0), DimLength, "nmi"}
// Mass (Pivot: gram)
unitRegistry["G"] = UnitInfo{d(1.0), DimMass, "g"}
unitRegistry["MG"] = UnitInfo{d(0.001), DimMass, "mg"}
unitRegistry["KG"] = UnitInfo{d(1000.0), DimMass, "kg"}
unitRegistry["T"] = UnitInfo{d(1e6), DimMass, "t"}
unitRegistry["CT"] = UnitInfo{d(0.2), DimMass, "ct"}
unitRegistry["LB"] = UnitInfo{d(453.592), DimMass, "lb"}
unitRegistry["OZ"] = UnitInfo{d(28.3495), DimMass, "oz"}
// Time (Pivot: second)
unitRegistry["S"] = UnitInfo{d(1.0), DimTime, "s"}
unitRegistry["MS"] = UnitInfo{d(0.001), DimTime, "ms"}
unitRegistry["US"] = UnitInfo{d(1e-6), DimTime, "us"}
unitRegistry["NS"] = UnitInfo{d(1e-9), DimTime, "ns"}
unitRegistry["MIN"] = UnitInfo{d(60.0), DimTime, "min"}
unitRegistry["H"] = UnitInfo{d(3600.0), DimTime, "h"}
unitRegistry["D"] = UnitInfo{d(86400.0), DimTime, "d"}
unitRegistry["WK"] = UnitInfo{d(604800.0), DimTime, "wk"}
unitRegistry["YR"] = UnitInfo{d(31536000.0), DimTime, "yr"}
// Digital (Pivot: byte)
unitRegistry["B"] = UnitInfo{d(1.0), DimDigital, "B"}
unitRegistry["KB"] = UnitInfo{d(1024.0), DimDigital, "KB"}
unitRegistry["MB"] = UnitInfo{d(1024.0 * 1024.0), DimDigital, "MB"}
unitRegistry["GB"] = UnitInfo{d(1024.0 * 1024.0 * 1024.0), DimDigital, "GB"}
unitRegistry["TB"] = UnitInfo{d(1024.0 * 1024.0 * 1024.0 * 1024.0), DimDigital, "TB"}
unitRegistry["PB"] = UnitInfo{d(1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0), DimDigital, "PB"}
unitRegistry["EB"] = UnitInfo{d(1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0), DimDigital, "EB"}
// Area (Pivot: square meter)
unitRegistry["M2"] = UnitInfo{d(1.0), DimArea, "m²"}
unitRegistry["CM2"] = UnitInfo{d(1e-4), DimArea, "cm²"}
unitRegistry["MM2"] = UnitInfo{d(1e-6), DimArea, "mm²"}
unitRegistry["KM2"] = UnitInfo{d(1e6), DimArea, "km²"}
unitRegistry["HA"] = UnitInfo{d(10000.0), DimArea, "ha"}
unitRegistry["ACRE"] = UnitInfo{d(4046.8564), DimArea, "acre"}
// Temperature (Pivot: Kelvin)
unitRegistry["K"] = UnitInfo{d(1.0), DimTemp, "K"}
unitRegistry["C"] = UnitInfo{d(1.0), DimTemp, "°C"}
unitRegistry["F"] = UnitInfo{d(1.0), DimTemp, "°F"}
unitRegistry["KELVIN"] = UnitInfo{d(1.0), DimTemp, "K"}
unitRegistry["CELSIUS"] = UnitInfo{d(1.0), DimTemp, "°C"}
unitRegistry["FAHRENHEIT"] = UnitInfo{d(1.0), DimTemp, "°F"}
// Angle (Pivot: Radian)
unitRegistry["RAD"] = UnitInfo{d(1.0), DimAngle, "rad"}
unitRegistry["DEG"] = UnitInfo{d(math.Pi / 180.0), DimAngle, "deg"}
unitRegistry["GRAD"] = UnitInfo{d(math.Pi / 200.0), DimAngle, "grad"}
unitRegistry["RADIAN"] = UnitInfo{d(1.0), DimAngle, "rad"}
unitRegistry["DEGREE"] = UnitInfo{d(math.Pi / 180.0), DimAngle, "deg"}
unitRegistry["DEGREES"] = UnitInfo{d(math.Pi / 180.0), DimAngle, "deg"}
unitRegistry["GRADIAN"] = UnitInfo{d(math.Pi / 200.0), DimAngle, "grad"}
}
// getDimName returns a human-readable string for a dimension.
func getDimName(d Dimension) string {
switch d {
case DimCurrency:
return "Currency"
case DimLength:
return "Length"
case DimMass:
return "Mass"
case DimTime:
return "Time"
case DimDigital:
return "Digital Storage"
case DimArea:
return "Area"
case DimTemp:
return "Temperature"
case DimAngle:
return "Angle"
default:
return "Number"
}
}
+127
View File
@@ -0,0 +1,127 @@
package main
import (
"bufio"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/Masterminds/semver/v3"
"github.com/fatih/color"
"github.com/minio/selfupdate"
)
var UPDATEURL = "http://gozilla.fhi.mpg.de/goca"
// Color function variables matching tools.go in dns
var Crb func(...interface{}) string = color.New(color.Bold, color.FgRed).SprintFunc()
var Cgb func(...interface{}) string = color.New(color.Bold, color.FgGreen).SprintFunc()
var Cwb func(...interface{}) string = color.New(color.Bold, color.FgWhite).SprintFunc()
func SF(format string, a ...any) string {
return fmt.Sprintf(format, a...)
}
func PE(msg ...string) (n int, err error) {
if len(msg) == 2 {
return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Crb("ERROR"), Cwb(msg[0]), msg[1])
}
return fmt.Fprintf(os.Stdout, "%s: %s\n", Crb("ERROR"), Cwb(msg[0]))
}
func PO(msg ...string) (n int, err error) {
if len(msg) == 2 {
return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Cgb("OK"), Cwb(msg[0]), msg[1])
}
return fmt.Fprintf(os.Stdout, "%s: %s\n", Cgb("OK"), Cwb(msg[0]))
}
func checkforupdate(URL string) { // ----------------------------------------------------- check for new version
prg := prgname()
resp, err := http.Get(URL + "/version.txt")
if err == nil {
scanner := bufio.NewScanner(resp.Body)
if scanner.Scan() {
lversion := strings.TrimSpace(scanner.Text())
sv_version, err := semver.NewVersion(Version)
if err == nil {
sv_lversion, err := semver.NewVersion(lversion)
if err == nil {
if sv_lversion.GreaterThan(sv_version) {
ans := Yesno(SF("new '%s' version found (%s -> %s), update now?",
prg, sv_version, sv_lversion), true, false)
if ans {
updateurl := SF("%s/%s_%s_%s_%s", URL, prg, lversion, runtime.GOOS, runtime.GOARCH)
if err := doupdate(updateurl); err != nil {
PE(SF("Update failed: %v\n", err))
os.Exit(1)
}
PO("Update successful!", "please run your last command again")
os.Exit(0)
}
}
}
}
}
resp.Body.Close()
}
}
func doupdate(url string) error { // ----------------------------------------------------------------- do update
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("server returned status: %v", resp.Status)
}
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil {
return err
}
return nil
}
func Yesno(msg string, def bool, overwrite bool) bool { // -------------------------- AlecAivazis/survey: yes/no
if overwrite {
return true
}
var err error
tmp := ""
if def {
err = survey.AskOne(&survey.Select{Message: msg, Options: []string{"Yes", "No"}}, &tmp)
} else {
err = survey.AskOne(&survey.Select{Message: msg, Options: []string{"No", "Yes"}}, &tmp)
}
if err != nil {
if err == terminal.InterruptErr {
fmt.Fprintln(os.Stdout, Crb("Interrupted."))
os.Exit(0)
}
}
if tmp == "Yes" {
return true
} else {
return false
}
}
func prgname() string { // ---------------------------------------------------------------- program name
exepath, err := os.Executable()
if err != nil {
PE(SF("Error getting executable path: %s", err))
return ""
}
exename := filepath.Base(exepath)
return exename
}
+6
View File
@@ -0,0 +1,6 @@
package main
// Version is the current version of the application.
// It is automatically incremented by the build script.
var Version = "1.1.1"
+869
View File
@@ -0,0 +1,869 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
// Mutex to serialize calculation evaluations and capture output safely
var evalMu sync.Mutex
type CalculateResponse struct {
Output string `json:"output"`
Exit bool `json:"exit"`
}
type VariableResponse struct {
Name string `json:"name"`
Value string `json:"value"`
Dim string `json:"dim"`
}
type FunctionResponse struct {
Name string `json:"name"`
Args []string `json:"args"`
Expr string `json:"expr"`
}
// evaluateExpressionSafe redirects stdout for the duration of the evaluation to capture output safely
func evaluateExpressionSafe(line string, variablesFile string, functionsFile string) (string, bool) {
evalMu.Lock()
defer evalMu.Unlock()
// Keep backup of original stdout
oldStdout := os.Stdout
defer func() { os.Stdout = oldStdout }()
// Create pipe to intercept stdout
r, w, err := os.Pipe()
if err != nil {
return "Error capturing stdout", false
}
os.Stdout = w
// Run evaluation (prints to stdout)
exit := handleLine(line, false, variablesFile, functionsFile)
// Close write end of pipe and read captured output
w.Close()
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
_ = r.Close()
return buf.String(), exit
}
// startWebServer boots the HTTP server serving the Web GUI and APIs
func startWebServer(port string) {
home, _ := os.UserHomeDir()
variablesFile := filepath.Join(home, ".goca_variables.json")
functionsFile := filepath.Join(home, ".goca_functions.json")
// Load persisted variables and custom functions
_ = loadUserFuncs(functionsFile)
_ = loadVariables(variablesFile)
// Fetch current currency rates in the background to ensure units work
_ = fetchRates()
// Serve the interactive web dashboard
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
html := strings.ReplaceAll(indexHTML, "{{VERSION}}", Version)
_, _ = w.Write([]byte(html))
})
// Evaluation endpoint
http.HandleFunc("/api/calculate", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(CalculateResponse{Output: "Empty expression", Exit: false})
return
}
output, exit := evaluateExpressionSafe(query, variablesFile, functionsFile)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(CalculateResponse{
Output: output,
Exit: exit,
})
})
// List variables endpoint
http.HandleFunc("/api/variables", func(w http.ResponseWriter, r *http.Request) {
evalMu.Lock()
defer evalMu.Unlock()
list := []VariableResponse{}
for k, v := range variables {
// Skip the internal answer variables from bloating list
if k == "ans" || k == "_" {
continue
}
list = append(list, VariableResponse{
Name: k,
Value: fmt.Sprintf("%v", v.Value),
Dim: fmt.Sprintf("%v", v.Dim),
})
}
sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(list)
})
// List custom functions endpoint
http.HandleFunc("/api/functions", func(w http.ResponseWriter, r *http.Request) {
evalMu.Lock()
defer evalMu.Unlock()
list := []FunctionResponse{}
for k, v := range userFuncs {
list = append(list, FunctionResponse{
Name: k,
Args: v.Args,
Expr: v.Expr,
})
}
sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(list)
})
// Clear state endpoint
http.HandleFunc("/api/reset", func(w http.ResponseWriter, r *http.Request) {
evalMu.Lock()
defer evalMu.Unlock()
variables = make(map[string]Result)
_ = saveVariables(variablesFile)
userFuncs = make(map[string]UserFunc)
_ = saveUserFuncs(functionsFile)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"success"}`))
})
fmt.Printf("Go-Ca web server started on http://localhost:%s\n", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
fmt.Printf("❌ Error starting web server: %v\n", err)
}
}
// Beautiful cyber-retro dashboard HTML layout string
const indexHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go-Ca | Scientific Cyber Calculator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root {
--color-cyan: #06b6d4;
--color-pink: #f43f5e;
--color-yellow: #fbbf24;
--color-green: #10b981;
--color-blue: #3b82f6;
--color-purple: #8b5cf6;
--color-red: #ef4444;
--color-orange: #ea580c;
--bg-dark: #080c14;
--bg-card: #0f172a;
--bg-input: #020617;
--border-neon: rgba(6, 182, 212, 0.18);
--border-warm: rgba(234, 88, 12, 0.22);
--font-display: 'Outfit', -apple-system, sans-serif;
--font-mono: 'Share Tech Mono', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-dark);
background-image: radial-gradient(circle at top, #0f1c30 0%, #05080e 100%);
color: #f8fafc;
font-family: var(--font-display);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Top Navigation Header */
header {
background-color: rgba(15, 23, 42, 0.5);
border-bottom: 1px solid var(--border-neon);
backdrop-filter: blur(10px);
padding: 0.85rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
}
.header-logo {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: 0.05em;
text-shadow: 0 0 10px rgba(6, 182, 212, 0.35);
}
.header-logo span {
background: linear-gradient(135deg, var(--color-cyan) 0%, #a5f3fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-panel {
display: flex;
align-items: center;
gap: 1.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.45rem;
color: var(--color-green);
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--color-green);
border-radius: 50%;
box-shadow: 0 0 10px var(--color-green);
animation: pulse 1.8s infinite;
}
.reset-btn {
background: rgba(244, 63, 94, 0.08);
border: 1px solid var(--color-pink);
border-radius: 4px;
color: var(--color-pink);
cursor: pointer;
font-family: var(--font-display);
font-size: 0.75rem;
font-weight: bold;
padding: 0.35rem 0.75rem;
transition: all 0.2s ease;
}
.reset-btn:hover {
background: rgba(244, 63, 94, 0.2);
box-shadow: 0 0 10px rgba(244, 63, 94, 0.25);
}
.dashboard-grid {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
padding: 1.5rem 2rem;
}
/* Column 1: Terminal Console Window */
.console-card {
display: flex;
flex-direction: column;
background-color: rgba(2, 6, 23, 0.65);
border: 1px solid var(--border-neon);
border-radius: 12px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4), inset 0 0 15px rgba(255, 255, 255, 0.01);
overflow: hidden;
flex: 1;
min-height: 0;
}
.console-header {
background-color: rgba(15, 23, 42, 0.4);
border-bottom: 1px solid rgba(6, 182, 212, 0.1);
padding: 0.6rem 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--color-cyan);
}
.console-terminal {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
font-family: var(--font-mono);
font-size: 0.95rem;
line-height: 1.6;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.05) transparent;
}
.terminal-welcome {
color: var(--color-blue);
margin-bottom: 1rem;
border-left: 2px solid var(--color-blue);
padding-left: 0.75rem;
}
.history-item {
margin-bottom: 0.85rem;
}
.history-query {
color: #94a3b8;
}
.history-output {
padding-left: 0.85rem;
}
/* Interactive Command Prompt Line */
.prompt-container {
display: flex;
align-items: center;
background-color: var(--bg-input);
border-top: 1px solid rgba(6, 182, 212, 0.1);
padding: 1rem 1.25rem;
gap: 0.75rem;
}
.prompt-symbol {
color: var(--color-green);
font-family: var(--font-mono);
font-weight: bold;
font-size: 1.1rem;
}
.prompt-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #f1f5f9;
font-family: var(--font-mono);
font-size: 1.05rem;
caret-color: var(--color-green);
}
.submit-btn {
background-color: var(--color-cyan);
border: none;
border-radius: 6px;
color: #020617;
cursor: pointer;
font-family: var(--font-display);
font-size: 0.8rem;
font-weight: bold;
padding: 0.5rem 1rem;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.submit-btn:hover {
box-shadow: 0 0 15px var(--color-cyan);
transform: translateY(-1px);
}
/* Keyframe animations */
@keyframes pulse {
0% {
box-shadow: 0 0 5px var(--color-green);
opacity: 0.8;
}
50% {
box-shadow: 0 0 15px var(--color-green);
opacity: 1;
}
100% {
box-shadow: 0 0 5px var(--color-green);
opacity: 0.8;
}
}
/* Help manual formatting */
.help-card {
background-color: rgba(15, 23, 42, 0.4);
border: 1px solid rgba(6, 182, 212, 0.15);
border-radius: 8px;
padding: 1.25rem;
margin-top: 0.5rem;
width: 100%;
max-width: 900px;
}
.help-title {
font-size: 1.1rem;
font-weight: bold;
color: var(--color-cyan);
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid rgba(6, 182, 212, 0.2);
padding-bottom: 0.35rem;
}
.help-grid {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.help-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.help-group-title {
font-size: 0.85rem;
font-weight: bold;
color: var(--color-yellow);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}
.help-item {
display: flex;
flex-direction: column;
background-color: rgba(2, 6, 23, 0.4);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
line-height: 1.4;
cursor: pointer;
transition: all 0.15s ease;
}
.help-item:hover {
background-color: rgba(6, 182, 212, 0.05);
border-color: rgba(6, 182, 212, 0.15);
transform: translateY(-1px);
}
.help-syntax {
font-family: var(--font-mono);
color: var(--color-cyan);
font-weight: bold;
margin-bottom: 0.10rem;
}
.help-desc {
color: #94a3b8;
}
/* Responsive styling for tablet and mobile devices */
@media (max-width: 768px) {
.dashboard-grid {
padding: 0.75rem;
}
header {
padding: 0.75rem 1rem;
}
.header-logo {
font-size: 1.25rem;
}
.status-indicator span {
display: none; /* Hide status text on small screens */
}
.status-panel {
gap: 0.75rem;
}
.reset-btn {
padding: 0.35rem 0.6rem;
font-size: 0.7rem;
}
.console-terminal {
padding: 1rem;
font-size: 0.85rem;
}
.prompt-container {
padding: 0.75rem 0.85rem;
}
.prompt-input {
font-size: 0.95rem;
}
.submit-btn {
padding: 0.45rem 0.85rem;
font-size: 0.75rem;
}
.help-card {
padding: 0.85rem;
}
.help-title {
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.help-grid {
gap: 0.75rem;
}
.help-item {
padding: 0.45rem 0.6rem;
}
}
</style>
</head>
<body>
<header>
<div class="header-logo">
<span>Go-Ca</span> SYSTEM
</div>
<div class="status-panel">
<div class="status-indicator">
<div class="status-dot"></div>
<span>SERVER CONSOLE ACTIVE</span>
</div>
<button class="reset-btn" id="btn-reset" title="Deletes all user variables & functions">RESET STATE</button>
</div>
</header>
<div class="dashboard-grid">
<!-- Calculator shell terminal -->
<div class="console-card">
<div class="console-header">
<span></span>
<span id="session-time">12:00:00</span>
</div>
<div class="console-terminal" id="terminal-scroller">
<div class="terminal-welcome">
goca {{VERSION}}, type 'help' for examples.
</div>
<div id="terminal-history"></div>
</div>
<div class="prompt-container">
<span class="prompt-symbol">goca&gt;</span>
<input type="text" id="cmd-input" class="prompt-input" autocomplete="off" autofocus placeholder="Type expression (e.g. 10 mi to km)">
<button class="submit-btn" id="btn-submit">RUN</button>
</div>
</div>
</div>
<script>
// Local input history list tracker
const cmdInput = document.getElementById('cmd-input');
const btnSubmit = document.getElementById('btn-submit');
const btnReset = document.getElementById('btn-reset');
const historyContainer = document.getElementById('terminal-history');
const terminalScroller = document.getElementById('terminal-scroller');
const inputHistory = [];
let historyIdx = -1;
// Tick clock
setInterval(function() {
const d = new Date();
document.getElementById('session-time').textContent = d.toTimeString().split(' ')[0];
}, 1000);
// Trigger evaluate
btnSubmit.addEventListener('click', runCommand);
cmdInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
runCommand();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (inputHistory.length > 0) {
historyIdx = Math.min(historyIdx + 1, inputHistory.length - 1);
cmdInput.value = inputHistory[inputHistory.length - 1 - historyIdx];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIdx > 0) {
historyIdx--;
cmdInput.value = inputHistory[inputHistory.length - 1 - historyIdx];
} else if (historyIdx === 0) {
historyIdx = -1;
cmdInput.value = '';
}
}
});
btnReset.addEventListener('click', function() {
if (confirm("Clear all user-defined variables and custom functions?")) {
fetch('/api/reset', { method: 'POST' })
.then(function(res) { return res.json(); })
.then(function() {
appendHistoryLine('unset *', '<span style="color: var(--color-pink);">All variables and functions deleted.</span>');
});
}
});
function runCommand() {
const line = cmdInput.value.trim();
if (!line) return;
inputHistory.push(line);
historyIdx = -1;
cmdInput.value = '';
if (line.toLowerCase() === 'clear') {
historyContainer.innerHTML = '';
return;
}
if (line.toLowerCase() === 'help') {
appendHistoryLine(line, getInteractiveHelpHTML());
return;
}
fetch('/api/calculate?q=' + encodeURIComponent(line))
.then(function(res) { return res.json(); })
.then(function(data) {
const formattedOutput = ansiToHtml(data.output);
appendHistoryLine(line, formattedOutput);
})
.catch(function(err) {
appendHistoryLine(line, '<span style="color: var(--color-red);">Error: Failed to connect to server.</span>');
});
}
function appendHistoryLine(query, output) {
const div = document.createElement('div');
div.className = 'history-item';
div.innerHTML = '<div class="history-query">goca&gt; ' + escapeHtml(query) + '</div>' +
'<div class="history-output">' + output + '</div>';
historyContainer.appendChild(div);
terminalScroller.scrollTop = terminalScroller.scrollHeight;
}
// Convert Go CLI ANSI color tags to styled HTML spans
function ansiToHtml(text) {
return text
.replace(/\u001b\[1;33m/g, '<span style="color: var(--color-yellow); font-weight: bold;">')
.replace(/\u001b\[1;31m/g, '<span style="color: var(--color-red); font-weight: bold;">')
.replace(/\u001b\[1;32m/g, '<span style="color: var(--color-green); font-weight: bold;">')
.replace(/\u001b\[1;34m/g, '<span style="color: var(--color-cyan); font-weight: bold;">') // Cyan style logo
.replace(/\u001b\[1;30m/g, '<span style="color: #475569;">')
.replace(/\u001b\[1m/g, '<span style="font-weight: bold;">')
.replace(/\u001b\[0m/g, '</span>')
.replace(/\n/g, '<br>');
}
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function insertSample(expr) {
cmdInput.value = expr;
cmdInput.focus();
}
function getInteractiveHelpHTML() {
var html = '';
html += '<div class="help-card">';
html += ' <div class="help-title">Go-Ca Interactive Manual</div>';
html += ' <div class="help-grid">';
html += ' <div class="help-group">';
html += ' <div class="help-group-title">Arithmetic &amp; Operators</div>';
html += ' <div class="help-item" onclick="insertSample(\'5 + 3 * 2\')">';
html += ' <div class="help-syntax">5 + 3 * 2</div>';
html += ' <div class="help-desc">Basic: +, -, *, /, % (modulo)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'10 ** 3\')">';
html += ' <div class="help-syntax">10 ** 3</div>';
html += ' <div class="help-desc">Power: a ** b or pow(a, b)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'0xFF &amp; 0b1010\')">';
html += ' <div class="help-syntax">0xFF &amp; 0b1010</div>';
html += ' <div class="help-desc">Bitwise: &amp;, |, ^, ~, &lt;&lt; (Left Shift), &gt;&gt; (Right Shift)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'10 > 5\')">';
html += ' <div class="help-syntax">10 &gt; 5</div>';
html += ' <div class="help-desc">Logic: ==, !=, &lt;, &gt;, &lt;=, &gt;=, ! (NOT)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'2(3 + 4)\')">';
html += ' <div class="help-syntax">2(3 + 4)</div>';
html += ' <div class="help-desc">Implicit: 2(3 + 4) or 2m (implicit multiplication)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'0xFF\')">';
html += ' <div class="help-syntax">0xFF</div>';
html += ' <div class="help-desc">Numbers: Decimal (10), Hex (0xFF), Binary (0b1010), Octal (0o77)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'x = 5.5\')">';
html += ' <div class="help-syntax">x = 5.5</div>';
html += ' <div class="help-desc">Variables: Assign (e.g. x = 5), Use \'ans\' or \'_\' for last result</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'f(x, y) = x * y + 2\')">';
html += ' <div class="help-syntax">f(x, y) = x * y + 2</div>';
html += ' <div class="help-desc">Custom Fn: f(x, y) = x * y + 2</div>';
html += ' </div>';
html += ' </div>';
html += ' <div class="help-group">';
html += ' <div class="help-group-title">Scientific Functions</div>';
html += ' <div class="help-item" onclick="insertSample(\'sin(90 deg)\')">';
html += ' <div class="help-syntax">sin(90 deg)</div>';
html += ' <div class="help-desc">Trig: sin(x), cos(x), tan(x) (accepts angles like 90 deg or pi rad)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'asin(1) to deg\')">';
html += ' <div class="help-syntax">asin(1) to deg</div>';
html += ' <div class="help-desc">Inv Trig: asin(x), acos(x), atan(x) (returns angles, e.g. asin(1) to deg)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'sinh(1)\')">';
html += ' <div class="help-syntax">sinh(1)</div>';
html += ' <div class="help-desc">Hyper: sinh(x), cosh(x), tanh(x)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'sqrt(16)\')">';
html += ' <div class="help-syntax">sqrt(16)</div>';
html += ' <div class="help-desc">General: sqrt(x), abs(x), exp(x), ln(x) (natural), log(x) (base 10), log2(x)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'round(5.5)\')">';
html += ' <div class="help-syntax">round(5.5)</div>';
html += ' <div class="help-desc">Rounding: ceil(x), floor(x), round(x)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'min(10, 20, 5)\')">';
html += ' <div class="help-syntax">min(10, 20, 5)</div>';
html += ' <div class="help-desc">Stats: min(a, b, ...), max(a, b, ...), mod(a, b), fact(x) (factorial)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'if(5 > 3, PI, E)\')">';
html += ' <div class="help-syntax">if(5 &gt; 3, PI, E)</div>';
html += ' <div class="help-desc">Logic: if(cond, true_val, false_val)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'PI\')">';
html += ' <div class="help-syntax">PI</div>';
html += ' <div class="help-desc">Constants: PI, E</div>';
html += ' </div>';
html += ' </div>';
html += ' <div class="help-group">';
html += ' <div class="help-group-title">IP &amp; Subnet</div>';
html += ' <div class="help-item" onclick="insertSample(\'ip(\\"192.168.1.1\\")\')">';
html += ' <div class="help-syntax">ip("192.168.1.1")</div>';
html += ' <div class="help-desc">Parse: ip("192.168.1.1"), cidr("10.0.0.0/24")</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'network(cidr(\\"10.0.0.0/24\\"))\')">';
html += ' <div class="help-syntax">network(cidr("10.0.0.0/24"))</div>';
html += ' <div class="help-desc">Subnet: network(c), broadcast(c), mask(c), hosts(c), range(c)</div>';
html += ' </div>';
html += ' </div>';
html += ' <div class="help-group">';
html += ' <div class="help-group-title">Units &amp; Conversions</div>';
html += ' <div class="help-item" onclick="insertSample(\'10 mi to km\')">';
html += ' <div class="help-syntax">10 mi to km</div>';
html += ' <div class="help-desc">Syntax: &lt;value&gt; &lt;unit&gt; to/in &lt;unit&gt; (e.g. 10 mi to km, 1 GB in MB)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'units\')">';
html += ' <div class="help-syntax">units</div>';
html += ' <div class="help-desc">Types: Length, Mass, Time, Digital, Area, Temperature, Angle</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'0 C to K\')">';
html += ' <div class="help-syntax">0 C to K</div>';
html += ' <div class="help-desc">Example: 0 C to K, 90 deg to rad, 32 F to C</div>';
html += ' </div>';
html += ' </div>';
html += ' <div class="help-group">';
html += ' <div class="help-group-title">Currencies (Live Rates)</div>';
html += ' <div class="help-item" onclick="insertSample(\'100 USD to EUR\')">';
html += ' <div class="help-syntax">100 USD to EUR</div>';
html += ' <div class="help-desc">Usage: Exchange currency codes (e.g. 100 USD to EUR)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'10$ to €\')">';
html += ' <div class="help-syntax">10$ to €</div>';
html += ' <div class="help-desc">Symbols: $, €, £, ¥ (e.g. 10$ to €)</div>';
html += ' </div>';
html += ' </div>';
html += ' <div class="help-group">';
html += ' <div class="help-group-title">Commands</div>';
html += ' <div class="help-item" onclick="insertSample(\'help\')">';
html += ' <div class="help-syntax">help</div>';
html += ' <div class="help-desc">help: Show this help information</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'units\')">';
html += ' <div class="help-syntax">units</div>';
html += ' <div class="help-desc">units: List all supported measurement units</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'rates\')">';
html += ' <div class="help-syntax">rates</div>';
html += ' <div class="help-desc">rates: Show currency exchange rates relative to base</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'cur\')">';
html += ' <div class="help-syntax">cur</div>';
html += ' <div class="help-desc">cur: List all supported live currency codes</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'var\')">';
html += ' <div class="help-syntax">var</div>';
html += ' <div class="help-desc">var: List all user-defined variables</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'funcs\')">';
html += ' <div class="help-syntax">funcs</div>';
html += ' <div class="help-desc">funcs: List all user-defined custom functions</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'unset x\')">';
html += ' <div class="help-syntax">unset x</div>';
html += ' <div class="help-desc">unset &lt;v&gt;: Delete variable &lt;v&gt; (or \'unset *\' to delete all)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'base USD\')">';
html += ' <div class="help-syntax">base USD</div>';
html += ' <div class="help-desc">base &lt;C&gt;: Change base currency (e.g. base USD)</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'clear\')">';
html += ' <div class="help-syntax">clear</div>';
html += ' <div class="help-desc">clear: Clear the terminal screen</div>';
html += ' </div>';
html += ' <div class="help-item" onclick="insertSample(\'exit\')">';
html += ' <div class="help-syntax">exit</div>';
html += ' <div class="help-desc">exit/quit: Exit goca</div>';
html += ' </div>';
html += ' </div>';
html += ' </div>';
html += '</div>';
return html;
}
</script>
</body>
</html>`