Skip to main content
The Model layer is contained in the CalcManager project and provides the core calculation logic for Windows Calculator. It operates independently of the UI, making it reusable and testable.

Three-Tier Architecture

The Model consists of three distinct layers, each with specific responsibilities:
1

CalculatorManager

High-level API managing calculator modes, history, and memory
2

CalcEngine

Core calculation logic interpreting operations and maintaining state
3

RatPack

Low-level rational number arithmetic with infinite precision

Layer 1: CalculatorManager

Location: src/CalcManager/CalculatorManager.hCalculatorManager is the entry point to the Model layer. ViewModels interact exclusively with this class.

Responsibilities

  • Manage calculator modes (Standard, Scientific, Programmer)
  • Maintain calculation history for each mode
  • Handle memory operations (M+, M-, MC, MR, MS)
  • Switch between different CalcEngine instances
  • Implement ICalcDisplay to communicate with ViewModels

Calculator Modes

enum class CalculatorMode
{
    Standard = 0,
    Scientific,
};
Programmer mode is handled through a separate engine instance, though not explicitly enumerated here.

Multiple Engine Instances

CalculatorManager maintains separate engine instances for each mode:
private:
    ICalcDisplay* const m_displayCallback;
    CCalcEngine* m_currentCalculatorEngine;
    std::unique_ptr<CCalcEngine> m_scientificCalculatorEngine;
    std::unique_ptr<CCalcEngine> m_standardCalculatorEngine;
    std::unique_ptr<CCalcEngine> m_programmerCalculatorEngine;
    IResourceProvider* const m_resourceProvider;
Each mode has its own engine instance, preserving the calculation state when switching between modes.

Memory Management

CalculatorManager.h Memory (src/CalcManager/CalculatorManager.h:47,56)
static const unsigned int m_maximumMemorySize = 100;
std::vector<CalcEngine::Rational> m_memorizedNumbers;
Calculator can store up to 100 memory values using rational numbers.

History Management

CalculatorManager maintains separate history for Standard and Scientific modes:
private:
    std::shared_ptr<CalculatorHistory> m_pStdHistory;
    std::shared_ptr<CalculatorHistory> m_pSciHistory;
    CalculatorHistory* m_pHistory;
History operations:
CalculatorManager.h History API (src/CalcManager/CalculatorManager.h:110-119)
std::vector<std::shared_ptr<HISTORYITEM>> const& GetHistoryItems() const;
std::vector<std::shared_ptr<HISTORYITEM>> const& GetHistoryItems(_In_ CalculatorMode mode) const;
void SetHistoryItems(_In_ std::vector<std::shared_ptr<HISTORYITEM>> const& historyItems);
std::shared_ptr<HISTORYITEM> const& GetHistoryItem(_In_ unsigned int uIdx);
bool RemoveHistoryItem(_In_ unsigned int uIdx);
void ClearHistory();
size_t MaxHistorySize() const
{
    return m_pHistory->MaxHistorySize();
}

ICalcDisplay Interface

CalculatorManager implements ICalcDisplay to communicate results back to ViewModels:
public:
    // ICalcDisplay
    void SetPrimaryDisplay(_In_ const std::wstring& displayString, _In_ bool isError) override;
    void SetIsInError(bool isError) override;
    void SetExpressionDisplay(
        _Inout_ std::shared_ptr<std::vector<std::pair<std::wstring, int>>> const& tokens,
        _Inout_ std::shared_ptr<std::vector<std::shared_ptr<IExpressionCommand>>> const& commands) override;
    void SetMemorizedNumbers(_In_ const std::vector<std::wstring>& memorizedNumbers) override;
    void OnHistoryItemAdded(_In_ unsigned int addedItemIndex) override;
    void SetParenthesisNumber(_In_ unsigned int parenthesisCount) override;
    void OnNoRightParenAdded() override;
    void DisplayPasteError();
    void MaxDigitsReached() override;
    void BinaryOperatorReceived() override;
    void MemoryItemChanged(unsigned int indexOfMemory) override;
    void InputChanged() override;
When the engine needs to update the display, it calls these interface methods, which the ViewModel implements.

Mode Switching

CalculatorManager.h Mode Methods (src/CalcManager/CalculatorManager.h:88-91)
void SetStandardMode();
void SetScientificMode();
void SetProgrammerMode();
These methods switch m_currentCalculatorEngine to the appropriate engine instance.

Layer 2: CalcEngine

Location: src/CalcManager/Header Files/CalcEngine.hCalcEngine is the calculation state machine. It interprets operations, maintains the calculation state, and performs mathematical operations.

Core Functionality

CalcEngine maintains calculation state:
CalcEngine.h State Variables (src/CalcManager/Header Files/CalcEngine.h:114-137)
bool m_bRecord;                // Recording or displaying mode
CalcEngine::CalcInput m_input; // Input object for decimal strings
NumberFormat m_nFE;            // Scientific notation flag

CalcEngine::Rational m_currentVal;  // Currently displayed number
CalcEngine::Rational m_lastVal;     // Number before operation (left operand)
CalcEngine::Rational m_holdVal;     // For repetitive calculations (pressing "=" continuously)

std::array<CalcEngine::Rational, MAXPRECDEPTH> m_parenVals;      // Parenthesis values
std::array<CalcEngine::Rational, MAXPRECDEPTH> m_precedenceVals; // Precedence values

bool m_bError;        // Error flag
bool m_bInv;          // Inverse on/off flag
bool m_bNoPrevEqu;    // Previous equals flag

Constructor

CCalcEngine(
    bool fPrecedence,
    bool fIntegerMode,
    CalculationManager::IResourceProvider* const pResourceProvider,
    __in_opt ICalcDisplay* pCalcDisplay,
    __in_opt std::shared_ptr<IHistoryDisplay> pHistoryDisplay
);
  • fPrecedence: Enable operator precedence (true for Scientific mode)
  • fIntegerMode: Restrict to integer operations (true for Programmer mode)
  • pCalcDisplay: Callback interface for display updates
  • pHistoryDisplay: Callback for history updates

Command Processing

void ProcessCommand(OpCode wID);
This is the main entry point for all calculator operations. It:
  1. Receives an operation code (button press)
  2. Updates the calculation state
  3. Performs calculations if needed
  4. Calls ICalcDisplay methods to update the UI

Display Formatting

CalcEngine.h Display Methods (src/CalcManager/Header Files/CalcEngine.h:80-88)
uint32_t GetCurrentRadix();
std::wstring GetCurrentResultForRadix(uint32_t radix, int32_t precision, bool groupDigitsPerRadix);
std::wstring GroupDigitsPerRadix(std::wstring_view numberString, uint32_t radix);
std::wstring GetStringForDisplay(CalcEngine::Rational const& rat, uint32_t radix);
void UpdateMaxIntDigits();
wchar_t DecimalSeparator() const;
These methods convert internal Rational values into formatted strings for different radixes.

History Collection

CalcEngine.h History (src/CalcManager/Header Files/CalcEngine.h:92,161)
std::vector<std::shared_ptr<IExpressionCommand>> GetHistoryCollectorCommandsSnapshot() const;

private:
    CHistoryCollector m_HistoryCollector; // Accumulator of each line of history
The CHistoryCollector accumulates operations as they occur, building up history items.

State Checking

CalcEngine.h State Methods (src/CalcManager/Header Files/CalcEngine.h:66-77)
bool FInErrorState()
{
    return m_bError;
}
bool IsInputEmpty()
{
    return m_input.IsEmpty() && (m_numberString.empty() || m_numberString == L"0");
}
bool FInRecordingState()
{
    return m_bRecord;
}

Layer 3: RatPack

Location: src/CalcManager/Ratpack/ratpak.hRatPack (Rational Pack) is the mathematical core providing infinite precision arithmetic using rational numbers.

Infinite Precision Arithmetic

RatPack uses arbitrary-precision arithmetic instead of floating-point, avoiding rounding errors inherent in IEEE 754 floating-point.
Instead of storing numbers as double or float, RatPack represents them as:
Rational Number = numerator / denominator
Both numerator and denominator can be arbitrarily large integers, limited only by available memory.

Rational Number Structure

Numbers are stored using the Rational type:
CalcEngine.h Rational Usage (src/CalcManager/Header Files/CalcEngine.h:127-134)
std::unique_ptr<CalcEngine::Rational> m_memoryValue; // Memory value

CalcEngine::Rational m_holdVal;     // Second operand holder
CalcEngine::Rational m_currentVal;  // Currently displayed number
CalcEngine::Rational m_lastVal;     // Left operand

std::array<CalcEngine::Rational, MAXPRECDEPTH> m_parenVals;
std::array<CalcEngine::Rational, MAXPRECDEPTH> m_precedenceVals;

Benefits of Rational Arithmetic

Exact Results

Operations like 0.1 + 0.2 produce exactly 0.3, not 0.30000000000000004

No Rounding Errors

Fractions like 1/3 are stored exactly, not approximated

Infinite Precision

Can represent numbers with thousands of digits

Deterministic

Same inputs always produce identical outputs

Mathematical Operations

RatPack provides operations on rational numbers:
  • Basic arithmetic: add, subtract, multiply, divide
  • Comparisons: equal, less than, greater than
  • Transcendental functions: sin, cos, tan, log, exp
  • Special operations: factorial, power, root
CalcEngine.h Operations (src/CalcManager/Header Files/CalcEngine.h:181-182)
CalcEngine::Rational SciCalcFunctions(CalcEngine::Rational const& rat, uint32_t op);
CalcEngine::Rational DoOperation(int operation, CalcEngine::Rational const& lhs, CalcEngine::Rational const& rhs);

Data Flow Through Model Layers

Here’s how a calculation flows through all three layers:
1

ViewModel → CalculatorManager

ViewModel calls SendCommand() with an operation
2

CalculatorManager → CalcEngine

Forwards command to active engine’s ProcessCommand()
3

CalcEngine → RatPack

Engine calls RatPack functions to perform exact arithmetic
4

RatPack → CalcEngine

Returns result as a Rational number
5

CalcEngine → CalculatorManager

Converts to display string and calls SetPrimaryDisplay()
6

CalculatorManager → ViewModel

Callback updates ViewModel property

Example: Adding Two Numbers

User presses: 5 + 3 =

Best Practices

ViewModels should only interact with CalculatorManager, never directly with CalcEngine or RatPack.
ViewModels must implement ICalcDisplay to receive calculation results from the Model.
Each calculator mode has its own engine instance to preserve state when switching modes.
Use RatPack’s infinite precision for exact calculations, converting to display strings only when needed.

Testing the Model

The Model layer is highly testable because it has no UI dependencies:
Example Test
// Create manager with mock display
CalculatorManager manager(&mockDisplay, &resourceProvider);

// Perform calculation
manager.SendCommand(Command::Five);
manager.SendCommand(Command::Add);
manager.SendCommand(Command::Three);
manager.SendCommand(Command::Equals);

// Verify display callback received "8"
Assert::AreEqual(L"8", mockDisplay.GetLastDisplayString());

Next Steps

ViewModel Layer

See how ViewModels call into CalculatorManager

Architecture Overview

Review the complete architecture

MVVM Pattern

Understand how Model fits into MVVM

Build docs developers (and LLMs) love