simple_decimal

Architecture & Design

Design Philosophy

simple_decimal follows the simple_* library philosophy: wrap complex underlying implementations with a clean, intuitive API. The library provides a facade over Gobo's MA_DECIMAL while hiding the complexity of context management and providing financial-friendly operations out of the box.

Core Principles

  • Simplicity - One class, intuitive methods, sensible defaults
  • Immutability - All operations return new values
  • Safety - Design by Contract throughout
  • Precision - No floating-point representation errors
  • Financial Focus - Built-in currency and percentage operations

Class Structure

SIMPLE_DECIMAL

The library consists of a single public class:

class
    SIMPLE_DECIMAL

inherit
    ANY
        redefine
            default_create, out, is_equal
        end

    COMPARABLE
        undefine
            default_create, out, is_equal
        end

feature -- Access
    decimal: MA_DECIMAL
        -- Underlying Gobo decimal

    context: MA_DECIMAL_CONTEXT
        -- Decimal context for operations

invariant
    decimal_exists: decimal /= Void
    context_exists: context /= Void

end

Inheritance Rationale

Underlying Implementation: Gobo MA_DECIMAL

simple_decimal wraps Gobo's MA_DECIMAL, a mature implementation of the General Decimal Arithmetic Specification. This specification was developed by IBM and forms the basis of IEEE 754-2008's decimal floating-point types.

Why Gobo MA_DECIMAL?

What simple_decimal Adds

MA_DECIMAL SIMPLE_DECIMAL
Requires explicit context management Context managed automatically
Method-based arithmetic Operator aliases (+, -, *, /)
No currency formatting to_currency_string, dollars, cents
No percentage operations add_percent, subtract_percent, from_percentage
No bill splitting split(n) with penny-perfect distribution
Complex string parsing Smart parsing handles $, commas

Immutability Pattern

All arithmetic operations in SIMPLE_DECIMAL return new instances. The original values are never modified. This design choice provides several benefits:

Benefits

Implementation

add alias "+" (other: like Current): SIMPLE_DECIMAL
        -- Sum of current and other
    require
        other_not_void: other /= Void
    do
        -- Create NEW decimal with result
        create Result.make_from_decimal (decimal.add (other.decimal, context))
    ensure
        result_not_void: Result /= Void
        -- Original values unchanged (immutability)
    end

Usage Pattern

-- Each operation creates a new value
a := a + b          -- 'a' now references new sum; old 'a' unchanged

-- Chaining is natural
total := (price * quantity - discount).round_cents

Context Management

The underlying MA_DECIMAL requires a context object for every operation. The context controls precision, rounding mode, and error handling. simple_decimal manages this automatically.

Default Context

SIMPLE_DECIMAL uses make_double_extended context by default, providing:

Rounding Mode Override

For operations requiring different rounding, temporary contexts are created:

round_up (a_places: INTEGER): SIMPLE_DECIMAL
        -- Round up (away from zero)
    local
        l_ctx: MA_DECIMAL_CONTEXT
    do
        -- Create temporary context with different rounding
        create l_ctx.make_double_extended
        l_ctx.set_rounding_mode (l_ctx.round_up)
        create Result.make_from_decimal (decimal.rescale (-a_places, l_ctx))
    end

String Parsing

The make creation procedure accepts various string formats and normalizes them before passing to MA_DECIMAL:

Parsing Algorithm

clean_string (a_value: READABLE_STRING_GENERAL): STRING
        -- Remove currency symbols and commas
    do
        create Result.make (a_value.count)
        across a_value as c loop
            if c /= '$' and c /= ',' and c /= ' ' then
                Result.append_character (c)
            end
        end
    end

Accepted Formats

Input After Cleaning Value
"19.99""19.99"19.99
"$19.99""19.99"19.99
"$1,234.56""1234.56"1234.56
"-$99.99""-99.99"-99.99
"1,000,000""1000000"1000000

Bill Splitting Algorithm

The split feature demonstrates careful handling of remainders:

split (n: INTEGER): ARRAYED_LIST [SIMPLE_DECIMAL]
        -- Split into n equal parts, handling remainder
    require
        n_positive: n > 0
    local
        l_base, l_total, l_remainder, l_one_cent: SIMPLE_DECIMAL
    do
        -- Step 1: Calculate base amount per person
        l_base := (Current / n).round_cents

        -- Step 2: Calculate what the total would be
        l_total := l_base * n

        -- Step 3: Find the remainder (could be positive or negative)
        l_remainder := Current - l_total

        -- Step 4: Distribute pennies to first recipients
        -- Result: [$33.34, $33.33, $33.33] for $100/3
    ensure
        correct_count: Result.count = n
        sum_equals_original: sum(Result) = Current.round_cents
    end

Example: $100.00 / 3

  1. Base amount: $100 / 3 = $33.33 (rounded)
  2. Total with base: $33.33 × 3 = $99.99
  3. Remainder: $100.00 - $99.99 = $0.01
  4. First person gets extra penny: [$33.34, $33.33, $33.33]
  5. Sum: $33.34 + $33.33 + $33.33 = $100.00 ✓

Design by Contract

SIMPLE_DECIMAL uses comprehensive contracts to ensure correctness:

Class Invariant

invariant
    decimal_exists: decimal /= Void
    context_exists: context /= Void

Representative Preconditions

-- Division requires non-zero divisor
divide alias "/" (other: like Current): SIMPLE_DECIMAL
    require
        other_not_void: other /= Void
        other_not_zero: not other.is_zero

-- Conversion requires valid number
to_integer: INTEGER
    require
        not_nan: not is_nan
        not_infinity: not is_infinity

-- Currency creation requires valid cents
make_currency (a_dollars, a_cents: INTEGER)
    require
        cents_valid: a_cents >= 0 and a_cents <= 99

Representative Postconditions

-- Arithmetic always produces result
add alias "+" (other: like Current): SIMPLE_DECIMAL
    ensure
        result_not_void: Result /= Void

-- Negation flips sign (except zero)
negate: SIMPLE_DECIMAL
    ensure
        sign_flipped: Result.is_negative /= is_negative or is_zero

-- Absolute value is never negative
absolute: SIMPLE_DECIMAL
    ensure
        result_not_negative: not Result.is_negative

-- Split produces correct count and sum
split (n: INTEGER): ARRAYED_LIST [SIMPLE_DECIMAL]
    ensure
        correct_count: Result.count = n
        sum_equals_original: sum_list(Result).round_cents.is_equal(round_cents)

SCOOP Compatibility

simple_decimal is designed for SCOOP (Simple Concurrent Object-Oriented Programming):

ECF Configuration

<capability>
    <concurrency support="scoop" use="scoop"/>
    <void_safety support="all" use="all"/>
</capability>

Dependencies

Required Libraries

No External Dependencies

simple_decimal has no dependencies outside of what ships with EiffelStudio. No additional installation or configuration required.

File Structure

simple_decimal/
├── simple_decimal.ecf      -- Project configuration
├── src/
│   └── simple_decimal.e    -- Main class (~700 lines)
├── tests/
│   └── simple_decimal_tests.e  -- Test suite (~950 lines)
├── docs/
│   ├── index.html          -- Documentation home
│   ├── user-guide.html     -- Tutorial
│   ├── api-reference.html  -- API docs
│   ├── architecture.html   -- This document
│   ├── cookbook.html       -- Recipes
│   ├── css/style.css       -- Styling
│   └── images/logo.svg     -- Logo
├── README.md               -- Quick start
└── LICENSE                 -- MIT License