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
+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
}
}
}