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
- COMPARABLE - Enables standard comparison operators (<, <=, >, >=) and sorting in containers
- ANY redefinitions:
default_create- Creates zero decimal instead of undefined stateout- Returns decimal string representationis_equal- Compares by value, not reference
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?
- Standards Compliant - Implements the General Decimal Arithmetic Specification
- Mature - Part of Gobo, a well-established Eiffel library collection
- Included with EiffelStudio - No additional dependencies
- Arbitrary Precision - Can handle numbers with many decimal places
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
- Thread Safety - No shared mutable state means no race conditions
- Predictability - Values don't change unexpectedly
- Debugging - Original values preserved for inspection
- Functional Style - Enables chaining and composition
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:
- Precision - 34 significant digits (decimal128 equivalent)
- Rounding - Half-even (banker's rounding)
- Range - Exponents from -6143 to +6144
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
- Base amount: $100 / 3 = $33.33 (rounded)
- Total with base: $33.33 × 3 = $99.99
- Remainder: $100.00 - $99.99 = $0.01
- First person gets extra penny: [$33.34, $33.33, $33.33]
- 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):
- Immutable Operations - No shared mutable state to protect
- Void Safety - All attributes are attached by invariant
- No Global State - Each instance is self-contained
ECF Configuration
<capability>
<concurrency support="scoop" use="scoop"/>
<void_safety support="all" use="all"/>
</capability>
Dependencies
Required Libraries
- base - EiffelStudio standard library
- Gobo Math - Provides MA_DECIMAL (
$ISE_LIBRARY/contrib/library/gobo/library/math/library.ecf)
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