Working with Fraction<T>
Fraction<T> is an immutable, value-equatable rational number whose
backing integer type is the type parameter T. The type auto-reduces
to canonical form on construction (GCD-normalised, sign on the
numerator, denominator strictly positive), promotes intermediate
arithmetic to BigInteger for safe evaluation, and implements the
full INumber<T> / ISignedNumber<T> surface so it composes with
generic-math algorithms without bespoke wrappers.
The type works with any backing type that implements
IBinaryInteger<T>: sbyte, byte, short, ushort, int,
uint, long, ulong, Int128, UInt128, BigInteger, and
consumer-defined integer types built on the generic-math interfaces.
Use Fraction<BigInteger> whenever a calculation chains several
multiplications or divisions - overflow is not possible and there is
no narrowing step.
Creating fractions
Use the static factory methods on Fraction<T> directly when the
backing type is fixed. Unlike Interval<T>, there is no non-generic
helper class for inference - pick the backing type up front.
using Bodu.Numerics;
// Two-argument factories normalise on construction.
Fraction<int> half = Fraction<int>.Create(1, 2); // 1/2
Fraction<int> twoFourths = Fraction<int>.Create(2, 4); // 1/2 - auto-reduced
Fraction<int> negThirds = Fraction<int>.Create(3, -4); // -3/4 - sign flipped to numerator
// Single-argument factory for whole numbers.
Fraction<int> seven = new Fraction<int>(7); // 7/1
// Implicit lift from T to Fraction<T>.
Fraction<int> three = 3; // 3/1
// Non-throwing variant.
if (Fraction<int>.TryCreate(7, 0, out var bad)) { /* … */ }
Both constructors and factories reduce to canonical form before returning. Two operations are guaranteed to throw at construction:
Fraction<T>.Create(numerator, 0)throwsDivideByZeroException.- A canonical result that does not fit in
T(rare forint, common nearT.MinValue) throwsOverflowException.
TryCreate(numerator, denominator, out result) reports both
conditions through a false return without throwing.
From other numeric types
Fraction<int>.FromDecimal(0.125m); // 1/8 - exact decimal
Fraction<int>.FromDouble(0.5); // 1/2 - exact for round halves
Fraction<BigInteger>.FromDouble(Math.PI); // Very large rational - Math.PI bits
Fraction<int>.FromBigInteger(7, 3); // Narrows BigInteger → int safely
FromDecimal is exact: it decomposes the decimal's mantissa and
scale. FromDouble is exact in the IEEE 754 sense - it decomposes the
double's mantissa and exponent, which may produce a fraction with a
very large denominator for values that look "nice" in base 10
(FromDouble(0.1) is not 1/10). For a best rational
approximation to a real number within a denominator bound, use
Approximate (see below).
FromDouble throws ArgumentException on non-finite input; the
Try… variants return false instead.
Best rational approximation
Fraction<int> piApprox = Fraction<int>.Approximate(Math.PI, maxDenominator: 1000);
// 355/113 - the Zǔ Chōngzhī approximation, error ≈ 2.7×10⁻⁷
Approximate(value, maxDenominator) uses convergents of the
continued-fraction expansion to find the best rational with
denominator ≤ the bound. Overloads accept double, decimal, and
string input. There is also a streaming form,
LimitDenominator(maxDenominator), that produces the best
approximation of an existing fraction within a tighter denominator
bound.
Continued fractions
Fraction<int> phi = Fraction<int>.Create(610, 377);
int[] coeffs = phi.ToContinuedFraction();
// [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - golden-ratio convergent
Fraction<int> reconstructed = Fraction<int>.FromContinuedFraction(coeffs);
The leading coefficient carries the sign; subsequent coefficients are
strictly positive. FromContinuedFraction enforces this on input:
- A
nullcoefficient array throws ArgumentNullException. - An empty array throws ArgumentException.
- A coefficient after the first that is zero or negative throws ArgumentOutOfRangeException.
LimitDenominator(maxDenominator) returns the value unchanged when its
denominator already fits the bound, and throws
ArgumentOutOfRangeException when maxDenominator is less
than one. The Approximate(value, maxDenominator) overloads
(double, decimal, string) share the same bound contract; the
double overload additionally throws ArgumentException
on non-finite input, and the string overload throws
FormatException on non-numeric text. Both
Approximate and LimitDenominator evaluate the search at
BigInteger precision and narrow only the final result, so an exact
value too large for T is bounded before it is narrowed.
var pi = Fraction<int>.Create(355, 113);
pi.LimitDenominator(100); // 311/99 - closest with denominator ≤ 100
Fraction<int>.Approximate(0.1, 1000); // 1/10 - recovers the intended rational from a double
Canonical form
Every Fraction<T> is reduced to GCD-normalised form, with the sign
on the numerator and the denominator strictly positive. This means
2/4 and 1/2 are indistinguishable after construction - there is
no unreduced form to preserve. Equality compares the canonical
components, so Fraction<int>.Create(2, 4) == Fraction<int>.Create(1, 2)
is true.
var a = Fraction<int>.Create(2, 4); // 1/2
var b = Fraction<int>.Create(1, 2); // 1/2
var c = Fraction<int>.Create(-3, -6); // 1/2 - both negatives cancel
Console.WriteLine(a == b); // True
Console.WriteLine(a == c); // True
Console.WriteLine(a.Numerator); // 1
Console.WriteLine(a.Denominator); // 2
The default-constructed Fraction<T> is zero: the all-zero
representation (numerator: 0, denominator: 0) is interpreted as
0/1 by the Denominator property.
Properties
| Property | Meaning |
|---|---|
Numerator |
Signed canonical numerator. |
Denominator |
Strictly positive canonical denominator (interprets default-init zero as one). |
Sign |
-1, 0, or 1. |
IsZero |
Numerator is zero. |
IsInteger / IsWhole |
Canonical denominator is one. IsWhole is an alias for IsInteger. |
IsProper |
Magnitude strictly less than one (|numerator| < denominator). |
IsImproper |
Magnitude at least one - the negation of IsProper. |
IsUnit |
Numerator magnitude is one (a unit fraction such as 1/7 or -1/7). |
IsNegative / IsPositive |
Sign classification; both are false for zero. |
IsEvenInteger / IsOddInteger |
true only when the value is an integer and the numerator has the stated parity. A non-integer is neither even nor odd. |
IsCanonical |
Always true - the type maintains the invariant. |
IsReducible |
Always false - there is no unreduced form to reduce. |
Reduce() returns the value unchanged for the same reason IsReducible is false: reduction already happened at construction. It exists so generic code that expects a Reduce step compiles and behaves correctly.
Static constants
Fraction<int>.Zero; // 0/1
Fraction<int>.One; // 1/1
Fraction<int>.MinusOne; // -1/1 - throws OverflowException if T is unsigned
Fraction<int>.MinValue; // T.MinValue/1
Fraction<int>.MaxValue; // T.MaxValue/1
MinValue / MaxValue throw NotSupportedException for unbounded
backing types like BigInteger.
Arithmetic
The full operator set is supported with cross-multiplication semantics
and BigInteger intermediates:
var a = Fraction<int>.Create(1, 3);
var b = Fraction<int>.Create(1, 2);
a + b; // 5/6
a - b; // -1/6
a * b; // 1/6
a / b; // 2/3
a % b; // 1/3 - remainder of the floored quotient
-a; // -1/3
++a; // 4/3
Convenience methods cover the common patterns:
a.Abs(); // magnitude
a.Negate(); // unary negation
a.Reciprocal(); // 3/1 - throws DivideByZeroException on 0
a.Invert(); // alias for Reciprocal()
a.Pow(3); // 1/27 - negative exponents allowed via reciprocal
a.Squared(); // 1/9 - alias for a * a
a.Cubed(); // 1/27 - alias for a * a * a
a.Remainder(b); // alias for a % b
Pow is defined for the whole int exponent range:
Pow(0)returnsOnefor every value, includingFraction<T>.Zero(the conventional0⁰ = 1).- A negative exponent raises the reciprocal to the corresponding magnitude, so
Fraction<int>.Create(2, 3).Pow(-2)is9/4. Applying a negative exponent to zero throws DivideByZeroException, and an exponent magnitude exceedingint.MaxValuethrows OverflowException.
The % operator (and its Remainder alias) returns the remainder of the floored-quotient division and carries the sign of the dividend - so Fraction<int>.Create(7, 2) % Fraction<int>.Create(1, 1) is 1/2. Dividing by zero throws DivideByZeroException.
Overflow handling
Arithmetic operations promote operands to BigInteger, evaluate
exactly, then narrow back to T. Overflow on narrowing raises
OverflowException:
var huge = Fraction<int>.Create(int.MaxValue, 1);
var doubled = huge + huge; // OverflowException
Switch the backing type to BigInteger to eliminate narrowing
entirely:
var hugeBI = Fraction<BigInteger>.Create(int.MaxValue, 1);
var doubledBI = hugeBI + hugeBI; // 4294967294/1 - no overflow
Unsigned backing types
Fraction<T> accepts unsigned backing types (uint, ulong, byte,
…) but negative values cannot be represented. Any operation that
would produce a negative numerator on an unsigned backing type throws
OverflowException at runtime - including MinusOne, unary -,
Negate(), the reciprocal of a value larger than one, and certain
subtraction patterns.
Comparison and equality
var a = Fraction<int>.Create(1, 3);
var b = Fraction<int>.Create(2, 5);
a < b; // True
a.CompareTo(b); // -1
Fraction<int>.Compare(a, b); // -1 - static cross-multiply
Fraction<int>.Min(a, b); // 1/3
Fraction<int>.Max(a, b); // 2/5
Fraction<int>.Clamp(value, lo: a, hi: b); // clamps to [1/3, 2/5]
Equality compares canonical components, not raw structural fields - because the canonical form is unique, two fractions are equal exactly when they represent the same rational value. The hash code is derived from the same canonical components, so equal fractions share a hash code.
Conversion
// Implicit lift from T.
Fraction<int> three = 3;
// Explicit narrowing conversions to numeric types.
decimal d = (decimal) Fraction<int>.Create(1, 4); // 0.25m - throws on decimal overflow
double x = (double) Fraction<int>.Create(1, 3); // 0.3333333333333333
float f = (float) Fraction<int>.Create(1, 3); // 0.33333334
// Try-variants for the narrowing direction.
Fraction<long>.Create(very_large, 1).TryToDecimal(out decimal v);
// Truncated integer extraction.
Fraction<int>.Create(7, 3).ToInteger(); // 2 - truncates toward zero
Fraction<int>.Create(-7, 3).ToBigInteger(); // -2
// Cross-backing-type conversion.
Fraction<BigInteger> bigHalf = Fraction<int>.Create(1, 2).As<BigInteger>();
As<TOther>() rejects values whose canonical components do not fit
in TOther with OverflowException.
The conversion surface divides cleanly into exact and approximate directions:
| Conversion | Direction | Exactness | Failure mode |
|---|---|---|---|
FromDecimal / (Fraction<T>)decimal |
in | exact (mantissa × 10⁻ˢᶜᵃˡᵉ) | OverflowException if the canonical components exceed T |
FromDouble / (Fraction<T>)double |
in | exact in the IEEE-754 sense (mantissa × 2ᵉˣᵖ) | ArgumentException on non-finite input; OverflowException on narrowing |
ToDecimal / (decimal) |
out | rounded to decimal precision |
OverflowException outside decimal range |
ToDouble / ToSingle |
out | rounded to double / float |
never throws - saturates to ±Infinity outside the finite range; TryToDouble / TryToSingle return false in that case |
ToInteger / ToBigInteger / GetWholePart |
out | truncated toward zero | ToInteger / GetWholePart may overflow T for an out-of-range integer part |
As<TOther> |
re-backing | exact (same canonical value) | OverflowException if a component does not fit TOther |
Truncation toward zero is the rule for the integer-extraction members: Fraction<int>.Create(-7, 3).ToInteger() is -2, not -3. Use Floor() / Ceiling() / Round() (below) when you need a different rounding direction.
Rounding and mixed parts
var x = Fraction<int>.Create(7, 3); // 2.333…
x.Floor(); // 2/1
x.Ceiling(); // 3/1
x.Truncate(); // 2/1
x.Round(); // 2/1 - banker's rounding (to-even)
x.Round(MidpointRounding.AwayFromZero); // 3/1
x.GetWholePart(); // 2 (T)
x.GetFractionalPart(); // 1/3
var (whole, frac) = x.ToMixedParts(); // (2, 1/3)
var (w, fn, fd) = x; // (2, 1, 3) - three-way Deconstruct: whole, fractional numerator, denominator
var (n, d) = Fraction<int>.Create(7, 3); // (7, 3) - Deconstruct over canonical components
The full MidpointRounding enum is supported on Round.
Generic math
Fraction<T> implements INumber<Fraction<T>> and
ISignedNumber<Fraction<T>>. The standard identities and predicates
are provided - but as explicit interface implementations, so they
are not callable on the concrete type: Fraction<int>.AdditiveIdentity
or Fraction<int>.IsNaN(x) does not compile. They are reached through
a generic type parameter constrained to the interface, which is exactly
how every generic-math algorithm consumes them:
static void Inspect<T>(T x, T y) where T : INumber<T>
{
T zero = T.AdditiveIdentity; // 0/1
T one = T.MultiplicativeIdentity; // 1/1
T.IsZero(T.Zero); // True
T.IsInteger(x); // True for 4/2 - canonical is 2/1
T.IsNaN(x); // False - Fraction<T> is never NaN
T.IsFinite(x); // True
T.IsRealNumber(x); // True
T.MaxMagnitude(x, y);
T.MinMagnitude(x, y);
}
static T MinusOne<T>() where T : ISignedNumber<T> => T.NegativeOne; // -1/1
Inspect(Fraction<int>.Create(4, 2), Fraction<int>.Create(-3, 1));
Where a member has a meaning outside generic code it also has a named
public counterpart on the concrete type: Fraction<T>.Zero, One, and
MinusOne are the identities, and the instance properties IsZero,
IsInteger, IsProper, IsNegative, and IsPositive are the
classification predicates.
This means Fraction<T> slots into algorithms written against the
INumber abstractions - Sum, Aggregate, generic linear-algebra
routines - without special-casing.
MaxMagnitude / MinMagnitude compare absolute values and break a tie
by sign, mirroring the BCL convention: MaxMagnitude prefers the
positive operand on a magnitude tie, MinMagnitude the negative one.
Clamp, Max, and Min are public static members of the concrete
type; CopySign, MaxNumber, and MinNumber are again reachable only
through the interfaces. Because Fraction<T> is never NaN, the
*Number variants behave identically to their plain counterparts.
Fraction<T> also participates in generic cross-type conversion via
TSelf.CreateChecked / CreateSaturating / CreateTruncating. These
are static virtual members of INumberBase<TSelf> with default
implementations, so they too exist only on the constrained type
parameter, never on Fraction<int> directly. Integer and decimal
sources convert exactly; other finite sources convert through their
nearest double; non-finite sources fail. The checked path overflows
to OverflowException, the saturating / truncating paths
clamp to MinValue / MaxValue instead.
static T Checked<T, TOther>(TOther value)
where T : INumber<T>
where TOther : INumberBase<TOther> =>
T.CreateChecked(value);
static T Saturating<T, TOther>(TOther value)
where T : INumber<T>
where TOther : INumberBase<TOther> =>
T.CreateSaturating(value);
Checked<Fraction<int>, int>(42); // 42/1 - exact integer source
Checked<Fraction<int>, decimal>(0.25m); // 1/4 - exact decimal source
Saturating<Fraction<int>, double>(1e30); // MaxValue - clamps instead of throwing
See Generic-math constraints for the constraint sets to use when writing such routines.
The backing type is constrained as where T : IBinaryInteger<T>.
Parsing and formatting
Fraction<T> implements IParsable<T>, ISpanParsable<T>,
IUtf8SpanParsable<T>, IFormattable, ISpanFormattable, and
IUtf8SpanFormattable. The accepted input shapes are:
| Input | Parses as |
|---|---|
"3", "-5" |
Whole-number fractions: 3/1, -5/1 |
"3/4", "-7/2" |
Ratios |
"2 1/3" |
Mixed numbers: 7/3 - sign applies to whole result |
"½", "⅖" |
Unicode vulgar fractions (18 glyphs - see Formatting and parsing) |
"2⅜" |
Whole + vulgar fraction: 19/8 |
"50%", "3/4%" |
Percentage form - trailing % divides denominator by 100 |
Leading / trailing whitespace is trimmed. Numeric components parse
with NumberStyles.None, so scientific notation and group separators
are rejected.
Fraction<int>.Parse("3/4"); // 3/4
Fraction<int>.Parse("2 1/3"); // 7/3
Fraction<int>.Parse("⅗"); // 3/5
Fraction<int>.Parse("75%"); // 3/4
Fraction<int>.TryParse("nope", out var _); // false
Format specifiers
| Specifier | Output |
|---|---|
null, "", "G" |
"numerator/denominator" or bare integer if denominator is 1 |
"M" |
Mixed-number form: "2 1/3" |
"U" |
Unicode vulgar where a glyph exists (denominator ≤ 16), otherwise mixed |
"P" |
Percentage form: scales by 100, re-reduces, renders as numerator/denominator% (bare numerator% when whole) |
var x = Fraction<int>.Create(7, 3);
x.ToString(); // "7/3"
x.ToString("M"); // "2 1/3"
x.ToString("U"); // "2⅓"
x.ToString("P"); // "700/3%" - 7/3 × 100 = 700/3, already in lowest terms
The percentage form is a ratio, not a mixed number: Fraction<int>.Create(7, 4).ToString("P") is "175%" (700/4 reduces to 175/1) and Fraction<int>.Create(3, 4).ToString("P") is "75%". Any specifier other than G/M/U/P (case-insensitive) throws FormatException.
Helper convenience methods:
ToUnicodeString(provider),
ToMixedString(provider) /
ToMixedNumberString(provider),
ToPercentString(provider).
JSON
JSON support ships in the companion
Bodu.Numerics.Serialization.Json package - the core library is
serialization-agnostic. Register the converters with
AddNumericsJsonConverters; the default Strict policy emits the
canonical object form:
using System.Text.Json;
using Bodu.Numerics.Serialization.Json;
var options = new JsonSerializerOptions().AddNumericsJsonConverters();
string json = JsonSerializer.Serialize(new Fraction<int>(3, 4), options);
// {"numerator":3,"denominator":4}
Fraction<int> r = JsonSerializer.Deserialize<Fraction<int>>(json, options);
Each component is written as a raw JSON number, so a
Fraction<BigInteger> round-trips at any magnitude without losing
precision through the writer's Int64 / decimal primitives. On read,
a component may be either a JSON number or a numeric string token.
To switch to the compact single-string form "3/4", register with
AddNumericsJsonConverters(NumericsJsonPolicy.Compact); the Compact
read path delegates to
Fraction<T>.TryParse(text, CultureInfo.InvariantCulture, …). See
JSON serialization for the full policy table
and failure modes.
Convenience helpers FractionJsonExtensions.ToJson() and
FractionJsonExtensions.FromJson<T>(string) (in the companion package)
wrap JsonSerializer under a chosen policy. The core type keeps the
XML helpers ToXml() / FromXml(string), which wrap the
invariant-culture general text form in
<fraction>numerator/denominator</fraction>.
Note
The ToJson() / FromJson() helpers use the reflection-based
JsonSerializer and are annotated RequiresUnreferencedCode /
RequiresDynamicCode. For trimming or AOT, register the converters
via AddNumericsJsonConverters against a source-generated
JsonSerializerContext instead.
Equality, hashing, and Equals(object?)
Fraction<T> is value-equatable via IEquatable<Fraction<T>> and
implements IComparable<Fraction<T>> / IComparable. The static
== / != operators delegate to Equals; the ordering operators
delegate to Compare (cross-multiplication). Equals(object?) does
the type-check / dispatch.
All empty / default / canonical-equivalent representations of the same rational value compare equal. Equal fractions share a hash code.
When not to use Fraction<T>
- Continuous measurements with no need for exact arithmetic. If
you are working in physics or graphics where the inputs are already
approximate
doubles, the rational form adds storage and computation cost without giving you anything. Stay withdouble. - Tight per-iteration loops with
int-sized values. Auto-reduction costs a GCD per operation, and theBigIntegerintermediate is not free either. For hot inner loops where overflow cannot happen and exactness is not required, plainintarithmetic is faster. - NaN / infinity semantics.
Fraction<T>does not modelNaNor infinity - division by zero throws, non-finitedoubleinput toFromDoublethrows, and the IEEE 754 propagation rules do not apply. If you need NaN-aware arithmetic, stay withdouble. - Storage in an unsigned backing type when negative values are
possible.
Fraction<uint>cannot represent-1/2; certain operations onFraction<uint>values throw at runtime. Either pick a signed backing type or model the sign separately.
See also
Fraction<T>API referenceFractionJsonConverter<T>API referenceFractionJsonConverterFactoryInterval<T>guide - the otherBodu.Numericsvalue type.Money<TCurrency>guide - usesFraction<BigInteger>as the precision escape hatch viaToFraction()/FromFraction()/MultiplyExact().- Numerics & Financial guides - every guide in this topic, across Bodu.Numerics and Bodu.Financial.