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).
"""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".
"""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.
"""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.
"""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.
"""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.
"""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.
"""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.
"""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.