simple_decimal

User Guide

Introduction

This guide will teach you everything you need to know to effectively use simple_decimal in your Eiffel applications. We'll start with the basics and progressively cover more advanced topics.

Why Decimal Arithmetic Matters

If you've ever wondered why your financial calculations are "close but not quite right," binary floating-point is likely the culprit. Computers store numbers in binary, and many common decimal fractions (like 0.1) cannot be represented exactly in binary.

The Classic Example

In virtually every programming language using binary floating-point:

0.1 + 0.2 = 0.30000000000000004

This isn't a bug - it's fundamental to how binary floating-point works. For financial applications, this is unacceptable.

Getting Started

Installation

  1. Set the environment variable pointing to the library:
    # Unix/Linux/macOS
    export SIMPLE_EIFFEL=/d/prod
    
    # Windows
    set SIMPLE_EIFFEL=D:\prod
  2. Add to your ECF configuration file:
    <library name="simple_decimal" location="$SIMPLE_EIFFEL/simple_decimal/simple_decimal.ecf"/>

Your First Decimal

local
    price: SIMPLE_DECIMAL
do
    -- Create from string (most precise method)
    create price.make ("19.99")

    -- Display it
    print (price.out)                  -- "19.99"
    print (price.to_currency_string)   -- "$19.99"
end

Creating Decimals

SIMPLE_DECIMAL offers several creation methods for different scenarios:

From String (Recommended)

Creating from strings is the most precise method because the value is parsed exactly as written:

create price.make ("19.99")
create tax_rate.make ("0.0825")    -- 8.25% as decimal
create negative.make ("-42.50")
create large.make ("1234567890.123456789")

Smart String Parsing

The parser automatically handles currency symbols and thousand separators:

create price.make ("$1,234.56")     -- Creates 1234.56
create price.make ("-$99.99")       -- Creates -99.99
create price.make ("1,000,000")    -- Creates 1000000

From Integer

create count.make_from_integer (42)
create negative.make_from_integer (-100)

From Dollars and Cents

Convenient for currency applications:

create amount.make_currency (19, 99)   -- Creates 19.99
create amount.make_currency (0, 50)    -- Creates 0.50
create amount.make_currency (-5, 25)   -- Creates -5.25

Special Values

create zero.make_zero    -- Creates 0
create one.make_one      -- Creates 1

Avoid: Creating from DOUBLE

While make_from_double exists, avoid it when precision matters:

-- NOT recommended for financial work:
create d.make_from_double (0.1)
-- The imprecision already exists in the DOUBLE literal!

Use string creation instead: create d.make ("0.1")

Arithmetic Operations

All arithmetic operations return new SIMPLE_DECIMAL objects. The original values are never modified (immutable pattern).

Basic Operations

local
    a, b, result: SIMPLE_DECIMAL
do
    create a.make ("100.00")
    create b.make ("25.50")

    result := a + b        -- Addition:       125.50
    result := a - b        -- Subtraction:     74.50
    result := a * b        -- Multiplication: 2550.00
    result := a / b        -- Division:         3.921568...
end

Integer Division and Modulo

create a.make ("100")
create b.make ("30")

result := a // b    -- Integer division: 3
result := a \\ b    -- Modulo (remainder): 10

Other Operations

result := price.negate      -- Flip sign: 19.99 -> -19.99
result := price.absolute    -- Absolute value: -19.99 -> 19.99
result := price.power (2)   -- Exponentiation: 10^2 = 100

Chaining Operations

Because operations return new decimals, you can chain them:

-- Calculate: (price * quantity) - discount
total := (price * quantity) - discount

-- Calculate: subtotal + tax, rounded to cents
total := (subtotal + tax).round_cents

Comparison

SIMPLE_DECIMAL implements COMPARABLE, so you can use standard comparison operators:

if price < budget then
    print ("Affordable")
end

if balance >= minimum then
    print ("Account in good standing")
end

if a.is_equal (b) then
    print ("Values are equal")
end

Available Comparisons

Status Queries

if amount.is_zero then ...
if amount.is_negative then ...
if amount.is_positive then ...
if amount.is_integer then ...
if amount.is_nan then ...      -- Not a Number
if amount.is_infinity then ...

Rounding

Financial applications often require specific rounding behavior. SIMPLE_DECIMAL provides multiple rounding modes:

Round to N Decimal Places

-- Default rounding (banker's rounding / half-even)
result := price.round (2)     -- 19.995 -> 20.00
result := price.round (0)     -- 19.5 -> 20 (rounds to even)

-- Shortcut for currency (2 decimal places)
result := price.round_cents   -- Same as round(2)

Rounding Modes

Method Description 2.5 becomes -2.5 becomes
round(n) Banker's rounding (half to even) 2 -2
round_up(n) Away from zero 3 -3
round_down(n) Toward zero (truncate) 2 -2
round_ceiling(n) Toward positive infinity 3 -2
round_floor(n) Toward negative infinity 2 -3
truncate Remove fractional part 2 -2

When to Use Which

Output Formatting

Basic Output

create amount.make ("1234.567")

print (amount.out)           -- "1234.567"
print (amount.to_string)     -- "1234.567"

Currency Formatting

create amount.make ("1234.56")
print (amount.to_currency_string)    -- "$1,234.56"

create negative.make ("-99.99")
print (negative.to_currency_string)  -- "-$99.99"

Extracting Parts

create price.make ("19.95")

print (price.dollars)    -- 19 (INTEGER)
print (price.cents)      -- 95 (INTEGER)

Type Conversions

-- To integer (truncates)
i := price.to_integer

-- To double (may lose precision)
d := price.to_double

Financial Operations

SIMPLE_DECIMAL includes operations specifically designed for financial calculations:

Percentage Calculations

local
    price, tax_rate, discount, result: SIMPLE_DECIMAL
do
    create price.make ("100.00")
    create tax_rate.make ("8.25")      -- 8.25%
    create discount.make ("15.0")     -- 15%

    -- Add 8.25% tax to price
    result := price.add_percent (tax_rate)        -- 108.25

    -- Subtract 15% discount from price
    result := price.subtract_percent (discount)   -- 85.00
end

Percentage Conversions

create rate.make ("8.25")

-- Convert percentage to decimal (for multiplication)
decimal_rate := rate.from_percentage    -- 0.0825

-- Convert decimal to percentage (for display)
create decimal_rate.make ("0.0825")
percent := decimal_rate.as_percentage   -- 8.25

Calculate What Percent One Value Is of Another

create tip.make ("15.00")
create bill.make ("75.00")

percentage := tip.percent_of (bill)    -- 20.0 (tip is 20% of bill)

Splitting Bills

The split feature divides an amount into equal parts while handling the remainder correctly:

create bill.make ("100.00")
parts := bill.split (3)

-- Result: [$33.34, $33.33, $33.33]
-- Sum equals exactly $100.00

The extra penny goes to the first parts, ensuring the total always equals the original amount exactly.

Best Practices

1. Always Create from Strings

When precision matters, always create decimals from strings:

-- GOOD: Exact representation
create price.make ("0.1")

-- AVOID: Imprecision may already exist
create price.make_from_double (0.1)

2. Round at the End

Perform calculations first, then round the final result:

-- GOOD: Maximum precision during calculation
total := (subtotal * tax_rate).round_cents

-- AVOID: Rounding too early loses precision
tax := (subtotal * tax_rate).round_cents
total := subtotal + tax  -- May have accumulated error

3. Use Appropriate Rounding

Choose rounding mode based on business requirements:

-- Tax calculations: often round up (in favor of tax authority)
tax := calculated_tax.round_up (2)

-- Available balance: round down (don't overspend)
available := balance.round_down (2)

4. Handle Edge Cases

-- Check before division
if not divisor.is_zero then
    result := amount / divisor
end

-- Check for special values
if not amount.is_nan and not amount.is_infinity then
    i := amount.to_integer
end

5. Use Contracts

SIMPLE_DECIMAL has extensive contracts. Enable assertions during development:

-- In your ECF:
<option>
    <assertions precondition="true" postcondition="true"/>
</option>

Next Steps