Skip to content
The Lumenary

Math laboratory

Adding fractions: right rule, wrong rule, exact answers

Using exact fractions, checks the rule a/b + c/d = (ad + cb)/(bd) on all 6,084 pairs of fractions in (0, 1] with denominators up to 12; the wrong rule (a + c)/(b + d) never gives the sum and always lands between the two fractions. Confirms the cross-multiplication test on 90,000 pairs and shows 0.1 + 0.2 != 0.3 in floating point. Complete for these cases only.

Back to the labExhaustive finite checkCounterexample
Adding fractions: right rule, wrong rule, exact answersPythonExhaustive finite checkCounterexampleComputationally tested
"""Adding fractions: the right rule, a tempting wrong rule, and computer decimals.

Python's fractions.Fraction does exact arithmetic with fractions. We use it to
compare the rule a/b + c/d = (ad + cb)/(bd) with the tempting wrong rule
"add the tops and add the bottoms", (a + c)/(b + d), on every pair of
fractions between 0 and 1 with denominators up to 12; to check the
cross-multiplication test ad = cb against exact equality; and to see why a
computer's ordinary decimals (floats) can disagree with exact fractions.
"""

from fractions import Fraction

def add_rule(a, b, c, d):
    """Numerator and denominator of a/b + c/d by the common-denominator rule."""
    return a * d + c * b, b * d

def wrong_rule(a, b, c, d):
    """The tempting mistake: add the numerators and add the denominators."""
    return a + c, b + d

def equivalent(a, b, c, d):
    """Cross-multiplication test: a/b and c/d are the same number."""
    return a * d == c * b

def fractions_in_unit_interval(max_den):
    """All pairs (a, b) with 1 <= a <= b <= max_den: fractions in (0, 1]."""
    return [(a, b) for b in range(1, max_den + 1) for a in range(1, b + 1)]

def compare_rules(max_den):
    """Return (pairs, right rule correct, wrong rule correct, wrong in between)."""
    fracs = fractions_in_unit_interval(max_den)
    right = wrong = between = 0
    for a, b in fracs:
        for c, d in fracs:
            exact = Fraction(a, b) + Fraction(c, d)
            right += Fraction(*add_rule(a, b, c, d)) == exact
            mediant = Fraction(*wrong_rule(a, b, c, d))
            wrong += mediant == exact
            low, high = sorted([Fraction(a, b), Fraction(c, d)])
            between += low <= mediant <= high
    return len(fracs) ** 2, right, wrong, between

def cross_test_agrees(max_num, max_den):
    """Return (pairs, agreements, equal pairs) for the cross-multiplication test."""
    fracs = [(a, b) for a in range(max_num + 1) for b in range(1, max_den + 1)]
    exact = {f: Fraction(*f) for f in fracs}
    pairs = agree = equal = 0
    for a, b in fracs:
        for c, d in fracs:
            pairs += 1
            same = exact[(a, b)] == exact[(c, d)]
            agree += equivalent(a, b, c, d) == same
            equal += same
    return pairs, agree, equal

def main():
    num, den = add_rule(1, 2, 1, 3)
    wn, wd = wrong_rule(1, 2, 1, 3)
    print(f"1/2 + 1/3 by the common-denominator rule: {num}/{den}")
    print(f"1/2 + 1/3 by 'add tops, add bottoms': {wn}/{wd}"
          f" = {float(Fraction(wn, wd))}, but 1/2 = 0.5")
    print("  The wrong answer is smaller than one of the parts!")
    print()
    pairs, right, wrong, between = compare_rules(12)
    print(f"All {pairs:,} pairs of fractions in (0, 1], denominators up to 12:")
    print(f"  right rule equals the exact sum:   {right:,} of {pairs:,}")
    print(f"  wrong rule equals the exact sum:   {wrong:,} of {pairs:,}")
    print(f"  wrong rule lands between the two:  {between:,} of {pairs:,}")
    pairs, agree, equal = cross_test_agrees(24, 12)
    print(f"Cross-multiplication test versus exact equality, {pairs:,} pairs:")
    print(f"  they agree {agree:,} times ({equal:,} pairs are equal numbers)")
    print()
    print(f"Floats:    0.1 + 0.2 == 0.3 is {0.1 + 0.2 == 0.3};"
          f" 0.1 + 0.2 = {0.1 + 0.2!r}")
    exact = Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10)
    print(f"Fractions: 1/10 + 2/10 == 3/10 is {exact}")
    print()
    print("Epistemic status: the checks are complete for the fractions listed,")
    print("so they prove nothing about larger denominators; the lesson's argument")
    print("(same-sized pieces, then count) covers every fraction. The float")
    print("result is a real counterexample: binary floats cannot store 0.1 exactly.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in