gralculator/internal/calc/calc.go
Grok 2a26148c8a chore(skeleton): minimal Go project layout + build system
- go.mod (1.24.2 + initial bubbletea/lipgloss)
- Makefile (build, install to ~/.local/bin/gralculator, test, clean; ldflags version injection)
- internal/version (ldflags-compatible String())
- main.go stub (version flag + placeholder)
- internal/calc/ (Engine skeleton with Base, CycleBase, IsInteger, ErrConversionNotPossible, FormatForDisplay stub + basic tests for CERR path)
- internal/ui/ (App model stub with Tab handling placeholder + lipgloss import for future rendering)

This establishes the three-phase foundation. Next: flesh out engine (phase 2), then TUI spike (phase 3). Paper trail continues.
2026-06-06 14:28:59 +01:00

88 lines
2.4 KiB
Go

package calc
import (
"errors"
"math"
)
// Base represents the current display formatting mode.
type Base string
const (
BaseDEC Base = "DEC"
BaseHEX Base = "HEX"
BaseBIN Base = "BIN"
BaseOCT Base = "OCT"
)
var basesCycle = []Base{BaseDEC, BaseHEX, BaseBIN, BaseOCT}
// ErrConversionNotPossible is returned by CycleBase when the current value
// has a fractional part and cannot be cleanly displayed in a non-DEC base.
var ErrConversionNotPossible = errors.New("conversion not possible (CERR)")
// Engine holds the calculator state. All math is decimal (float64).
// Bases affect only formatting via FormatForDisplay and the small row.
type Engine struct {
value float64
base Base
pendingOp string // "+", "-", "*", "/", "mod", or ""
pendingVal float64
// entry buffer / more state will be added in engine implementation
}
// NewEngine creates a fresh calculator engine starting in DEC.
func NewEngine() *Engine {
return &Engine{
value: 0,
base: BaseDEC,
}
}
// CurrentBase returns the active display base for the small row.
func (e *Engine) CurrentBase() Base {
return e.base
}
// IsInteger reports whether the current value is effectively an integer
// (within epsilon to tolerate fp noise from operations like 1/3).
func (e *Engine) IsInteger() bool {
_, frac := math.Modf(e.value)
return math.Abs(frac) < 1e-10
}
// CycleBase advances to the next base in the cycle (DEC→HEX→BIN→OCT→DEC).
// Returns ErrConversionNotPossible (and does not change base) if the value
// has a fractional part. The caller (UI) should trigger a "CERR" flash.
func (e *Engine) CycleBase() error {
if !e.IsInteger() {
return ErrConversionNotPossible
}
for i, b := range basesCycle {
if b == e.base {
e.base = basesCycle[(i+1)%len(basesCycle)]
return nil
}
}
// fallback
e.base = BaseDEC
return nil
}
// FormatForDisplay returns the string to show in the large number area
// according to the current base. For MVP only integer values are expected
// in non-DEC bases (callers should have checked via CycleBase or IsInteger).
func (e *Engine) FormatForDisplay() string {
// Placeholder implementation. Real version in phase 2 will handle
// proper integer formatting for hex/bin/oct (no 0x prefix, etc.)
// and fall back gracefully.
if e.base == BaseDEC {
// simple for skeleton
return "0"
}
return "0"
}
// TODO (phase 2): Add EnterDigit, SetOperator, Equals, Mod, ClearEntry,
// AllClear, ChangeSign, Backspace, etc. + full FormatForDisplay.