simple_fraction

Architecture & Design

Design Philosophy

simple_fraction follows the simple_* library philosophy: provide a clean, focused API for a single purpose. Unlike simple_decimal (which wraps Gobo's MA_DECIMAL), simple_fraction is a pure Eiffel implementation with no external dependencies.

Core Principles

  • Exactness - No approximation; 1/3 is exactly 1/3
  • Auto-reduction - Fractions always in lowest terms
  • Immutability - All operations return new values
  • Safety - Design by Contract throughout
  • No dependencies - Pure Eiffel, just needs base library

Internal Representation

Data Structure

class
    SIMPLE_FRACTION

feature -- Access

    numerator: INTEGER_64
        -- The numerator (top number), may be negative

    denominator: INTEGER_64
        -- The denominator (bottom number), always positive

invariant
    denominator_positive: denominator > 0
    reduced_form: is_reduced
end

Why INTEGER_64?

INTEGER_64 provides a range of roughly ±9.2 quintillion, which handles most practical calculations. For numerator/denominator pairs:

Normalization Rules

  1. Denominator always positive - Sign stored in numerator only
  2. Always reduced - GCD of numerator and denominator is 1
  3. Zero normalized - 0/n always becomes 0/1
-- Examples of normalization:
make (2, 4)     -- Stored as 1/2 (reduced by GCD 2)
make (-3, 4)    -- Stored as -3/4 (sign in numerator)
make (3, -4)    -- Stored as -3/4 (sign moved to numerator)
make (-3, -4)   -- Stored as 3/4 (negatives cancel)
make (0, 5)     -- Stored as 0/1 (zero normalized)

GCD Algorithm

Fraction reduction uses the Euclidean algorithm for finding the Greatest Common Divisor:

gcd (a, b: INTEGER_64): INTEGER_64
        -- Greatest common divisor using Euclidean algorithm
    require
        a_non_negative: a >= 0
        b_non_negative: b >= 0
    local
        l_a, l_b, l_temp: INTEGER_64
    do
        l_a := a
        l_b := b
        from
        until
            l_b = 0
        loop
            l_temp := l_b
            l_b := l_a \\ l_b
            l_a := l_temp
        end
        Result := l_a
        if Result = 0 then
            Result := 1
        end
    ensure
        result_positive: Result > 0
    end

Algorithm Trace

Example: gcd(48, 18)

  1. 48 mod 18 = 12, so now find gcd(18, 12)
  2. 18 mod 12 = 6, so now find gcd(12, 6)
  3. 12 mod 6 = 0, so GCD is 6

Therefore 48/18 reduces to 8/3 (divided by GCD 6).

Arithmetic Implementation

Addition and Subtraction

-- a/b + c/d = (a*d + c*b) / (b*d)
add alias "+" (other: like Current): SIMPLE_FRACTION
    local
        l_num: INTEGER_64
    do
        l_num := numerator * other.denominator + other.numerator * denominator
        create Result.make (l_num, denominator * other.denominator)
    end

Multiplication

-- a/b * c/d = (a*c) / (b*d)
multiply alias "*" (other: like Current): SIMPLE_FRACTION
    do
        create Result.make (numerator * other.numerator, denominator * other.denominator)
    end

Division

-- a/b / c/d = a/b * d/c = (a*d) / (b*c)
divide alias "/" (other: like Current): SIMPLE_FRACTION
    require
        other_not_zero: not other.is_zero
    do
        create Result.make (numerator * other.denominator, denominator * other.numerator)
    end

Note: The make constructor automatically reduces, so results are always in lowest terms.

Comparison Implementation

Comparison uses cross-multiplication to avoid division:

-- a/b < c/d iff a*d < c*b (when denominators positive)
is_less alias "<" (other: like Current): BOOLEAN
    do
        Result := numerator * other.denominator < other.numerator * denominator
    end

Since denominators are always positive (invariant), this comparison is safe and doesn't require sign adjustment.

Mixed Number Handling

Mixed numbers are converted to improper fractions on creation:

make_mixed (a_whole, a_numerator, a_denominator: INTEGER_64)
        -- Create from mixed number: whole + numerator/denominator
    local
        l_num: INTEGER_64
    do
        if a_whole >= 0 then
            l_num := a_whole * a_denominator + a_numerator
        else
            l_num := a_whole * a_denominator - a_numerator
        end
        set_and_reduce (l_num, a_denominator)
    end

Converting Back to Mixed

to_mixed_string: STRING
        -- Format as "whole num/den" or "num/den"
    local
        l_whole, l_frac_num: INTEGER_64
    do
        if is_proper or is_zero or is_integer then
            Result := out
        else
            l_whole := whole_part                    -- 11/4 -> 2
            l_frac_num := (numerator - l_whole * denominator).abs  -- -> 3
            Result := l_whole.out + " " + l_frac_num.out + "/" + denominator.out
        end
    end

String Parsing

The parser handles multiple formats:

Format Example Detection
Simple fraction "3/4" Contains '/' but no space
Mixed number "2 3/4" Contains both ' ' and '/'
Decimal "0.5" Contains '.'
Integer "5" None of the above

Decimal Conversion

Decimals become fractions with power-of-10 denominators, then reduce:

-- "0.5" -> 5/10 -> 1/2
-- "0.25" -> 25/100 -> 1/4
-- "0.125" -> 125/1000 -> 1/8

Immutability Pattern

All arithmetic operations return new SIMPLE_FRACTION objects:

Benefits

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

-- Chaining is natural
result := (a + b) * c / d

Design by Contract

Class Invariants

invariant
    denominator_positive: denominator > 0
    reduced_form: is_reduced

Key Preconditions

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

-- Reciprocal requires non-zero
reciprocal: SIMPLE_FRACTION
    require
        not_zero: not is_zero

-- Creation requires valid denominator
make (a_numerator, a_denominator: INTEGER_64)
    require
        denominator_not_zero: a_denominator /= 0

Key Postconditions

-- Reduction always happens
make (a_numerator, a_denominator: INTEGER_64)
    ensure
        is_reduced: is_reduced
        denominator_positive: denominator > 0

-- Absolute value is non-negative
absolute: SIMPLE_FRACTION
    ensure
        result_non_negative: not Result.is_negative

-- Reciprocal property
reciprocal: SIMPLE_FRACTION
    ensure
        reciprocal_property: (Current * Result).is_equal (one)

SCOOP Compatibility

simple_fraction is designed for SCOOP:

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

Dependencies

Minimal

Unlike simple_decimal (which wraps Gobo MA_DECIMAL), simple_fraction is a pure Eiffel implementation with no external dependencies.

File Structure

simple_fraction/
├── simple_fraction.ecf       -- Project configuration
├── src/
│   └── simple_fraction.e     -- Main class (~500 lines)
├── tests/
│   └── simple_fraction_tests.e -- Test suite (66 tests)
├── docs/
│   ├── index.html
│   ├── user-guide.html
│   ├── api-reference.html
│   ├── architecture.html
│   ├── cookbook.html
│   ├── css/style.css
│   └── images/logo.svg
├── README.md
└── LICENSE