package calc import ( "errors" "math" "strconv" "strings" "unicode" ) // 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 indicator. // Entry model: classic "in-progress entry buffer" (you see what you are typing). type Engine struct { // committed accumulator (result of previous operations) accumulator float64 // current in-progress entry as string (for display and for next operand) entry string // pending operator to apply when next operand is committed ("", "+", "-", "*", "/", "mod") pendingOp string // the left-hand value for the pending operation pendingLeft float64 base Base } // NewEngine creates a fresh calculator engine starting in DEC with 0. func NewEngine() *Engine { return &Engine{ accumulator: 0, entry: "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 *displayed* numeric value is effectively an integer // (within epsilon to tolerate fp noise from operations like 1/3 or 0.1+0.2). // We use the committed accumulator when there is no active entry, otherwise the parsed entry. func (e *Engine) IsInteger() bool { val := e.currentNumericValue() _, frac := math.Modf(val) return math.Abs(frac) < 1e-10 } // currentNumericValue returns the number that should be considered "current" // for operations and for BASE cycling / formatting. // For active entry, parses according to the current base (tight scope supports // decimal for DEC and base-16 for HEX). func (e *Engine) currentNumericValue() float64 { if e.entry != "" && e.entry != "0" { switch e.base { case BaseDEC: if v, err := strconv.ParseFloat(e.entry, 64); err == nil { return v } case BaseHEX: if i, err := strconv.ParseInt(e.entry, 16, 64); err == nil { return float64(i) } case BaseBIN: if i, err := strconv.ParseInt(e.entry, 2, 64); err == nil { return float64(i) } case BaseOCT: if i, err := strconv.ParseInt(e.entry, 8, 64); err == nil { return float64(i) } } } return e.accumulator } // CycleBase advances to the next base in the cycle (DEC→HEX→BIN→OCT→DEC). // Returns ErrConversionNotPossible (and does not change base) if the current // numeric value has a fractional part. The UI should trigger a "CERR" flash. // // For tight-scope HEX entry: if the user is mid-entry, we parse the entry // using the *old* base (committing its value), clear the entry, then change // base. This keeps the entry string always valid for the current base indicator. func (e *Engine) CycleBase() error { if !e.IsInteger() { return ErrConversionNotPossible } // Commit any active entry under the *current* base before switching. // This ensures "1A" typed in HEX becomes 26 (decimal) when you Tab away, // and the new base's display formatting applies to the committed value. if e.entry != "" && e.entry != "0" { val := e.currentNumericValue() e.accumulator = val e.entry = "0" } for i, b := range basesCycle { if b == e.base { e.base = basesCycle[(i+1)%len(basesCycle)] return nil } } e.base = BaseDEC return nil } // FormatForDisplay returns the string to render in the large number area // according to the current base and the current numeric value. func (e *Engine) FormatForDisplay() string { // Tight scope for HEX entry: while the user is actively typing (entry // buffer active) and the base indicator says HEX, show the raw hex digits // they typed (uppercased). This is the key UX for "HEX entry via keyboard". if e.entry != "" && e.entry != "0" { if e.base == BaseHEX { return strings.ToUpper(e.entry) } if e.base == BaseDEC { return e.entry } // For other bases we fall through to numeric formatting below. } val := e.currentNumericValue() // For committed values or DEC entry, use the previous logic. if e.base == BaseDEC { // For DEC committed results, show a reasonable representation. if math.Abs(val) < 1e12 { return strconv.FormatFloat(val, 'f', -1, 64) } return strconv.FormatFloat(val, 'e', -1, 64) } // Non-DEC bases: only make sense for integers (CERR guards this). // We coerce to int64 here. Negative numbers shown as positive for MVP. i := int64(val) if i < 0 { i = 0 } switch e.base { case BaseHEX: return strings.ToUpper(strconv.FormatInt(i, 16)) case BaseBIN: return strconv.FormatInt(i, 2) case BaseOCT: return strconv.FormatInt(i, 8) default: return strconv.FormatFloat(val, 'f', -1, 64) } } // EnterDigit appends a digit to the current entry buffer. // For HEX base, accepts 0-9 and A-F (case-insensitive). // For other bases (tight scope: only DEC for now), accepts 0-9. func (e *Engine) EnterDigit(d rune) { d = unicode.ToUpper(d) if e.base == BaseHEX { if (d >= '0' && d <= '9') || (d >= 'A' && d <= 'F') { if e.entry == "0" || e.entry == "" { e.entry = string(d) } else { e.entry += string(d) } } return } // Default (DEC): decimal digits only (validate to keep entry clean) if d >= '0' && d <= '9' { if e.entry == "0" || e.entry == "" { e.entry = string(d) } else { e.entry += string(d) } } } // EnterDecimalPoint adds a '.' to the entry if one is not already present. // Disabled for non-DEC bases in tight scope (HEX entry is integer-only). func (e *Engine) EnterDecimalPoint() { if e.base != BaseDEC { return } if !strings.Contains(e.entry, ".") { if e.entry == "" { e.entry = "0." } else { e.entry += "." } } } // SetOperator commits the current entry (if any) as the left operand and // records the operator for the next Equals / next operator. func (e *Engine) SetOperator(op string) { // Commit whatever is in the entry as the left side for this op. left := e.currentNumericValue() e.pendingLeft = left e.pendingOp = op e.entry = "0" // prepare for right-hand side entry e.accumulator = left } // Equals / commit the pending operation using the current entry as right-hand side. func (e *Engine) Equals() { right := e.currentNumericValue() result := e.applyPending(e.pendingLeft, right, e.pendingOp) e.accumulator = result e.entry = formatResultEntryForBase(result, e.base) e.pendingOp = "" e.pendingLeft = 0 } // applyPending performs the arithmetic for the given operator. func (e *Engine) applyPending(left, right float64, op string) float64 { switch op { case "+": return left + right case "-": return left - right case "*": return left * right case "/": if right == 0 { return 0 // MVP: avoid panic, could later surface "ERR" } return left / right case "mod", "MOD": return math.Mod(left, right) default: return right } } // formatResultEntry produces a clean entry string for a committed result (DEC). func formatResultEntry(v float64) string { if math.IsInf(v, 0) || math.IsNaN(v) { return "0" } s := strconv.FormatFloat(v, 'f', -1, 64) // Trim trailing .000 etc for cleanliness while keeping decimal when needed. if strings.Contains(s, ".") { s = strings.TrimRight(s, "0") s = strings.TrimRight(s, ".") } if s == "" { s = "0" } return s } // formatResultEntryForBase produces the result string formatted for the current display base. // For HEX/BIN/OCT this yields the appropriate integer digit string (so that FormatForDisplay's // raw-entry short-circuit for HEX and currentNumericValue's base-aware parsing both see correct // digits for the result). This makes results appear in the active base (e.g. 1FFE not 8190 in HEX). func formatResultEntryForBase(v float64, b Base) string { if b == BaseDEC { return formatResultEntry(v) } if math.IsInf(v, 0) || math.IsNaN(v) { return "0" } i := int64(v) if i < 0 { i = 0 } switch b { case BaseHEX: return strings.ToUpper(strconv.FormatInt(i, 16)) case BaseBIN: return strconv.FormatInt(i, 2) case BaseOCT: return strconv.FormatInt(i, 8) default: return formatResultEntry(v) } } // Mod performs immediate modulo using the current entry as the right operand // (useful for a dedicated MOD button that acts like = for modulo). func (e *Engine) Mod() { right := e.currentNumericValue() result := math.Mod(e.accumulator, right) e.accumulator = result e.entry = formatResultEntryForBase(result, e.base) e.pendingOp = "" } // ClearEntry clears only the current entry buffer (C button). func (e *Engine) ClearEntry() { e.entry = "0" } // AllClear resets everything (AC button). func (e *Engine) AllClear() { e.accumulator = 0 e.entry = "0" e.pendingOp = "" e.pendingLeft = 0 e.base = BaseDEC // or keep current base? MVP resets for simplicity; can relax later } // ChangeSign toggles the sign of the current entry (or accumulator if no entry). func (e *Engine) ChangeSign() { if e.entry != "" && e.entry != "0" { if strings.HasPrefix(e.entry, "-") { e.entry = strings.TrimPrefix(e.entry, "-") } else { e.entry = "-" + e.entry } return } e.accumulator = -e.accumulator e.entry = formatResultEntryForBase(e.accumulator, e.base) } // Backspace removes the last character from the current entry. func (e *Engine) Backspace() { if len(e.entry) <= 1 { e.entry = "0" return } e.entry = e.entry[:len(e.entry)-1] if e.entry == "" || e.entry == "-" { e.entry = "0" } } // TODO for future: support for the exact "pending on display" formatting // when a base change happens mid-operation (reformat the left operand).