Tutorial

Exact fractions

Use Rational when a calculation must keep fractions exact. Scaling three quarters of a cup from six servings to ten, for example, gives exactly five quarters (5/4).

rationals.mojo Download
"""Exact fractions: construction, arithmetic and powers."""

from apn_mojo import Integer, Rational, pow_rational


def main() raises:
    var flour = Rational(3, 4)
    var servings = Rational(10, 6)
    var scaled = flour * servings
    print("Scaled cups:", scaled)
    print("Components:", scaled.numerator(), scaled.denominator())
    print("Floor, ceil, trunc:", scaled.floor(), scaled.ceil(), scaled.trunc())

    var share = Rational(2, 3)
    var saved = share
    share += Rational(1, 6)
    print("Saved and updated:", saved, share)
    print("Reciprocal:", Rational(-2, 3) ** -1)
    print("Exact native value:", Int(Rational(12, 3)))
    print("Integer division:", Integer(7) / 3)
    print("Signed power:", pow_rational(2, -3))
    print("Mixed comparison:", Integer(2) < Rational(7, 3))

    try:
        share /= 0
    except error:
        print(error)
    print("After failed division:", share)

Run from the repository root pixi run mojo run -I src docs/examples/rationals.mojo

Output

Scaled cups: 5/4
Components: 5 4
Floor, ceil, trunc: 1 2 1
Saved and updated: 2/3 5/6
Reciprocal: -3/2
Exact native value: 4
Integer division: 7/3
Signed power: 1/8
Mixed comparison: True
Cannot divide Rational: divisor is 0; use a nonzero divisor. The destination is unchanged.
After failed division: 5/6

Rational(numerator, denominator) takes care of reducing the fraction and keeping its denominator positive. Arithmetic returns a Rational even when the answer is a whole number. Copies and values returned by numerator() and denominator() keep their values if you later update the original.

To convert to a native Int, use Int(value). The fraction must be a whole number that fits in Int; otherwise, the conversion raises an error. To round a fraction to an Integer, choose the direction: floor() rounds toward negative infinity, ceil() toward positive infinity, and trunc() toward zero.

Dividing integers with / also returns a Rational. For signed powers, use pow_rational; integer ** accepts only nonnegative exponents. Because Mojo variables have fixed types, integer /= 2 cannot turn an integer variable into a fraction. Start with a Rational or assign the result to a new variable.

Exact decimal input

Text lets you supply a decimal value without first rounding it to a native float. Rational("0.1") is exactly 1/10. The constructor also accepts fractions such as "3/4" and scientific notation such as "1.5e-3".

rational_input.mojo Download
"""Exact decimal input into Rationals."""

from apn_mojo import Rational, ConversionLimits


def main() raises:
    var rate = Rational("0.125")
    var amount = Rational("19.20")
    print("Exact charge:", amount * rate)
    print("Scientific notation:", Rational("1.25e-3"))
    print("Reduced fraction:", Rational("6/-8"))

    var limits = ConversionLimits(
        max_input_bytes=256, max_output_bytes=256,
        max_digits=64, max_values=1, max_allocated_bytes=16384,
    )
    var value = Rational("-7/3", limits=limits)
    var record = value.to_json(limits=limits)
    print("JSON:", record)
    print("Restored:", Rational.from_json(record, limits=limits))
    print("Exact text:", value.to_string(limits=limits))

Run from the repository root pixi run mojo run -I src docs/examples/rational_input.mojo

Output

Exact charge: 12/5
Scientific notation: 1/800
Reduced fraction: -3/4
JSON: {"version":1,"family":"rational","numerator":"-7","denominator":"3"}
Restored: -7/3
Exact text: -7/3

JSON saves the reduced numerator and positive denominator as decimal strings. To limit the input, digit counts, or allocation requests for a conversion, pass limits=ConversionLimits(...). These limits apply to that call; later arithmetic has no such budget.

Batches of fractions

Batch[Rational] uses the same indexing, slicing, and selection rules as other batches. Saved copies and slices keep their values when the original changes.

rational_batches.mojo Download
"""Batches of fractions and masked selections."""

from apn_mojo import Batch, Rational, Mask


def main() raises:
    print("mixed:", Batch[Rational]([1, Rational(1, 2)]))
    var shares = Batch[Rational](
        [Rational(1, 2), Rational(2, 3), Rational(3, 4)]
    )
    var saved = shares[:]
    shares[1:] = shares[:-1]
    print("updated:", shares)
    print("saved:", saved)
    shares[Mask([True, False, True])] = Rational(5, 6)
    print("selected fill:", shares)
    print("reverse:", shares[::-1])
    shares[0] = shares[0] + Rational(1, 6)
    print("scalar update:", shares)
    var total = Rational()
    for share in shares:
        total += share
    print("total:", total)

Run from the repository root pixi run mojo run -I src docs/examples/rational_batches.mojo

Output

mixed: [1, 1/2]
updated: [1/2, 1/2, 2/3]
saved: [1/2, 2/3, 3/4]
selected fill: [5/6, 1/2, 5/6]
reverse: [5/6, 1/2, 5/6]
scalar update: [1, 1/2, 5/6]
total: 7/3

Exact batch calculations

Dividing integer batches produces a Batch[Rational]. Mixing integer counts with rational rates also keeps the result exact. Comparisons return masks, and scalar operands broadcast across the batch. Two vectors must have equal lengths; higher-rank operations broadcast compatible trailing dimensions.

rational_batch_math.mojo Download
"""Exact elementwise batch calculations."""

from apn_mojo import Batch, Integer, Rational


def main() raises:
    var counts = Batch[Integer]([1, 2, 3])
    var portions = counts / 4
    print("portions:", portions)
    var rates = Batch[Rational]([1, Rational(1, 2), Rational(1, 3)])
    print("weighted:", counts * rates)
    print("above half:", portions[portions > Rational(1, 2)])
    print("at least half:", Rational(1, 2) <= portions)
    var saved = portions[:]
    portions = portions + Rational(1, 4)
    print("adjusted:", portions)
    print("saved:", saved)
    portions[portions > Rational(1, 2)] = (
        portions[portions > Rational(1, 2)] / 2
    )
    print("selected halves:", portions)

Run from the repository root pixi run mojo run -I src docs/examples/rational_batch_math.mojo

Output

portions: [1/4, 1/2, 3/4]
weighted: [1, 1, 1]
above half: [3/4]
at least half: [False, True, True]
adjusted: [1/2, 3/4, 1]
saved: [1/4, 1/2, 3/4]
selected halves: [1/2, 3/8, 1/2]

Updates such as values += adjustment change only the destination batch.

Exact totals and weights

sum returns an exact total, and dot computes an exact weighted sum with integer or rational weights. The result remains a Rational when a rational batch participates. Divide by a nonzero total weight to obtain a weighted mean.

rational_reductions.mojo Download
"""Exact totals and weighted sums."""

from apn_mojo import (
    Batch,
    Integer,
    Rational,
    sum,
    prod,
    min,
    max,
    dot,
)


def main() raises:
    var portions = Batch[Rational](
        [Rational(1, 2), Rational(3, 4), Rational(2, 5)]
    )
    var counts = Batch[Integer].from_native([4, 2, 5])
    var total = dot(portions, counts)
    print("Total:", total)
    print("Weighted mean:", total / sum(counts))
    print("Sum:", sum(portions))
    print("Product:", prod(portions))
    print("Range:", min(portions), max(portions))
    print("Selected sum:", sum(portions[counts > 2]))
    print("Reversed pairs:", dot(counts[::-1], portions[::-1]))
    var empty = Batch[Rational]()
    print("Empty identities:", sum(empty), prod(empty))

Run from the repository root pixi run mojo run -I src docs/examples/rational_reductions.mojo

Output

Total: 11/2
Weighted mean: 1/2
Sum: 33/20
Product: 3/20
Range: 2/5 3/4
Selected sum: 9/10
Reversed pairs: 11/2
Empty identities: 0 1

The two vectors in a dot product must have equal lengths. dot computes the total without creating an intermediate batch of products. Empty sums return zero and empty products return one; min and max need at least one value. If your values are in a Mojo list, convert it to Batch[Rational] first.

Rounding and signed powers

For a rational batch, .floor(), .ceil(), and .trunc() return a Batch[Integer]. vmap[rational.abs]() takes absolute values while keeping rational elements.

A power can use one scalar exponent or an integer batch of exponents. vmap[pow_rational]() also computes exact reciprocal powers of integer batches.

rational_batch_powers.mojo Download
"""Rounding to integers and signed powers on batches."""

from apn_mojo import Batch, Integer, Rational, pow_rational, rational, vmap


def main() raises:
    var portions = Batch[Rational]([Rational(7, 3), Rational(-7, 3), 2])
    print("Magnitudes:", vmap[rational.abs]()(portions))
    print("Signs:", portions.sign())
    print("Floor:", portions.floor())
    print("Ceiling:", portions.ceil())
    print("Truncation:", portions.trunc())

    var growth = Batch[Rational]([Rational(3, 2), Rational(5, 4)])
    var periods = Batch[Integer]([2, -2])
    print("Growth factors:", growth ** periods)
    print("Reverse growth:", growth[::-1] ** -1)
    var whole = Batch[Integer]([2, 4, 8])
    print("Integer reciprocals:", vmap[pow_rational]()(whole, -1))

Run from the repository root pixi run mojo run -I src docs/examples/rational_batch_powers.mojo

Output

Magnitudes: [7/3, 7/3, 2]
Signs: [1, -1, 1]
Floor: [2, -3, 2]
Ceiling: [3, -2, 2]
Truncation: [2, -2, 2]
Growth factors: [9/4, 16/25]
Reverse growth: [4/5, 2/3]
Integer reciprocals: [1/2, 1/4, 1/8]

Zero to a negative power raises an error identifying the element. Zero to zero is one. values **= exponent applies the signed-power rules in place.

Updating exact quantities

+=, -=, *=, /=, and integral **= update rational batches. If any element fails, the whole destination keeps its previous value.

rational_batch_updates.mojo Download
"""Transactional compound updates of exact quantities."""

from apn_mojo import Batch, Integer, Rational


def main() raises:
    var quantities = Batch[Rational]([Rational(1, 2), Rational(3, 4), 2])
    var original = quantities[:]
    quantities *= Rational(3, 2)
    quantities += Rational(1, 4)
    print(quantities)
    print(original)

    var factors = Batch[Integer]([2, 0, 4])
    try:
        quantities /= factors
    except error:
        print(error)
    print(quantities)

    factors[1] = 2
    quantities /= factors
    print(quantities)
    quantities **= -1
    print(quantities)

Run from the repository root pixi run mojo run -I src docs/examples/rational_batch_updates.mojo

Output

[1, 11/8, 13/4]
[1/2, 3/4, 2]
Cannot divide Rational batch at element 1: divisor is 0; use a nonzero divisor. The destination is unchanged.
[1, 11/8, 13/4]
[1/2, 11/16, 13/16]
[2, 16/11, 16/13]

Use Batch[Rational] when updates may produce fractions. Masked updates such as values[mask] *= rate affect only the selected elements.

Saving exact batches

JSON preserves exact fractions across languages. Saving a slice writes only its selected elements, and decoding restores a new batch. Rank-one batches use schema version 1; other ranks use version 2 with an explicit shape.

rational_batch_json.mojo Download
"""Saving and loading exact batches as JSON."""

from apn_mojo import Batch, Rational, ConversionLimits


def main() raises:
    var rates = Batch[Rational]([Rational(1, 8), Rational(-7, 3), Rational(0)])
    var saved = rates[::-2]
    var policy = ConversionLimits(
        max_values=2,
        max_digits=4,
        max_input_bytes=512,
        max_output_bytes=512,
        max_allocated_bytes=16384,
    )
    var wire = saved.to_json(limits=policy)
    print(wire)
    var restored = Batch[Rational].from_json(wire, limits=policy)
    rates += 1
    print("Restored:", restored)
    print("Updated:", rates)
    try:
        restored = Batch[Rational].from_json(
            '{"version":1,"family":"rational-batch","values":[{"numerator":"2","denominator":"4"}]}',
            limits=policy,
        )
        print("Imported:", restored)
    except:
        print("Noncanonical input rejected; kept:", restored)
    restored = Batch[Rational].from_json(wire, limits=policy)
    print("Retry:", restored)

Run from the repository root pixi run mojo run -I src docs/examples/rational_batch_json.mojo

Output

{"version":1,"family":"rational-batch","values":[{"numerator":"0","denominator":"1"},{"numerator":"1","denominator":"8"}]}
Restored: [0, 1/8]
Updated: [9/8, -4/3, 1]
Noncanonical input rejected; kept: [0, 1/8]
Retry: [0, 1/8]

One ConversionLimits budget covers the whole call, including digits in both numerators and denominators. JSON input must already be canonical; use the text constructor to reduce a fraction such as "2/4". Reusing a limits object starts a new budget for each call.

See the Rational reference for the full API.