- 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.
88 lines
2.4 KiB
Go
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.
|