Table of Contents

Numerics & Financial

The Numerics & Financial topic covers the Bodu.Numerics and Bodu.Financial package families, built around one idea: exact arithmetic. Bodu.Numerics ships Fraction<T> for rational arithmetic with no binary-floating-point drift and Interval<T> for generic-math ranges with independent endpoint inclusivity, unbounded endpoints, and full set algebra - including the binary-result IntervalPair<T>, the N-ary IntervalSet<T>, and the integer-domain DiscreteInterval<T>. Bodu.Financial builds the monetary layer on top - typed and runtime-tagged money, the ISO 4217 currency catalogue, fair allocation, and an exchange-rate provider stack with audit-grade provenance. Bodu.Financial.DependencyInjection registers that stack in a Microsoft.Extensions.DependencyInjection container, Bodu.Numerics.Serialization.Json and Bodu.Financial.Serialization.Json add the System.Text.Json converters (both core libraries are serialization-agnostic), and the Bodu.Financial.ExchangeRates package family supplies the live web feeds and the caching / aggregation layer in front of them.

The dependency runs one way: Bodu.Financial references Bodu.Numerics so that Money<TCurrency> can hand off to Fraction<BigInteger> for sub-minor-unit-precise intermediate calculations. Money<TCurrency> itself is settlement-grade - every operation rounds to the currency's minor units - but calculation chains where each step's rounding would compound (interest accumulation, percentage-of-percentage, unit-rate products) escape to exact rationals via ToFraction(), evaluate without loss, and snap back to settlement precision in a single rounding event via FromFraction(...) or the one-step MultiplyExact(...). Consumers of Fraction<T> who never touch money are not penalised: the split keeps the 184-currency catalogue and the FX provider stack out of purely numeric workloads.

Both value-type libraries are generic-math first. Fraction<T> accepts any IBinaryInteger<T> backing (from sbyte through Int128 to BigInteger) and implements INumber<T> / ISignedNumber<T>; Interval<T> accepts any INumber<T> endpoint type. Code written against the .NET generic-math abstractions composes with them without bespoke wrappers.

The packages

Package Status What it provides Docs
Bodu.Numerics Stable Fraction<T> - canonical, GCD-reduced rational arithmetic with BigInteger intermediates, mixed-number and Unicode-vulgar-fraction formatting, continued fractions, best rational approximation. Interval<T> - closed / open / half-open / unbounded intervals with membership, intersection, union, adjacency, difference, and the & / | operators; plus the integer-domain DiscreteInterval<T>, the binary-result IntervalPair<T> / DiscreteIntervalPair<T>, and the N-ary IntervalSet<T>. Also BigDecimal (arbitrary-precision decimal), Complex<T> (generic complex numbers), and the streaming statistics aggregates RunningStatistics<T>, RunningQuantile<T>, MovingSum<T>, MovingMinMax<T>. Serialization-agnostic. Intro · Concepts · Get started
Bodu.Numerics.Serialization.Json Preview System.Text.Json companion: options.AddNumericsJsonConverters(...) registers the converters and factories for Fraction<T>, BigDecimal, Complex<T>, Interval<T>, DiscreteInterval<T>, and IntervalSet<T> under a NumericsJsonPolicy. Guide
Bodu.Financial Stable Money<TCurrency> (compile-time currency) and Money (runtime-tagged), MoneyBag multi-currency portfolios, the ISO 4217 catalogue of 184 currency tags, fair allocation, cash rounding, and the exchange-rate core - timeless and dated provider contracts with provenance, series and in-memory tables. Serialization-agnostic; no HTTP machinery. Intro · Concepts · Get started
Bodu.Financial.Serialization.Json Stable System.Text.Json companion: options.AddFinancialJsonConverters(policy) registers the six converters (Money<TCurrency>, Money, CalculatedMoney, MoneyBag, ExchangeRate, CurrencyPair) under one of three FinancialJsonPolicy wire shapes (Strict, Lenient, Compact); services.AddFinancialJson(policy) registers a keyed JsonSerializerOptions. Reference
Bodu.Financial.ExchangeRates Preview The web-provider machinery every live feed builds on: the abstract WebRateProvider / PairWebRateProvider<TSeries> bases, WebRateProviderOptions, single-flight fetch coalescing, and the on-disk raw-response cache base. Reference
Bodu.Financial.ExchangeRates.<Source> Stable Eleven per-source live-feed packages over that machinery - Boe, Ecb, Rba, Yahoo, Ofx, Xe, Oanda, Fixer, ExchangeRateHost, Fred, Imf - each shipping its own Add<Source>ExchangeRates DI registration. Providers guide
Bodu.Financial.ExchangeRates.Caching (+ .Sqlite, .Distributed) Stable Provider-agnostic read-through caching (CachingRateProvider over in-memory / TOML / JSON / SQLite / IDistributedCache backends) and multi-source grouping (AggregatingRateProvider - priority fallback, averaging, per-pair routing). Caching guide
Bodu.Financial.DependencyInjection Stable IServiceCollection extensions: AddFinancialService(...), the fluent IFinancialServiceBuilder, named monetary contexts, FX provider registration, and FinancialOptions binding. (JSON registration is AddFinancialJson in Bodu.Financial.Serialization.Json.) Guide

Why exact arithmetic

double cannot represent one third, and 0.1 + 0.2 != 0.3 under binary floating point. decimal fares better for money but still rounds on every division - split $1.00 three ways with naive division and a cent disappears. The types in this topic attack the problem from two directions:

  • Compute exactly, round once. Fraction<T> holds the true rational value through an entire calculation chain; the only rounding event is the explicit one at the boundary. Arithmetic promotes to BigInteger internally, so intermediate magnitudes never truncate.
  • Make the rounding rules first-class. Money<TCurrency> rounds to the currency's minor units on construction using banker's rounding by default, exposes Allocate(...) so splits never lose a penny, and snaps to coarse coin denominations via RoundToCash() only when a total becomes a physical cash payment.

And because the currency rides in the type parameter, Money<USD> + Money<JPY> fails the build, not the nightly batch:

using Bodu.Financial;
using Bodu.Financial.Currencies;

Money<USD> dinner = new(54.30m);
Money<USD> tip    = dinner * 0.18m;
Money<USD> total  = dinner + tip;       // OK - same currency

Money<JPY> sushi  = new(2500m);
var oops = dinner + sushi;              // CS0019 - fails the build

When the currency genuinely is data - a deserialised payload, a configuration-driven ledger - the runtime-tagged Money is the escape hatch, converting to typed money with As<T>() at the boundary where the currency becomes known.

Ranges as first-class values

Interval<T> is the topic's second numeric primitive: lower endpoint, upper endpoint, and the inclusivity of each side, packed into one immutable readonly struct. Reach for it whenever the range itself is the data - a validation predicate exposed by an API, a scheduling window persisted to a database, a reservation that must detect overlap with others:

using Bodu.Numerics;

var valid = Interval<int>.Closed(0, 100);          // [0, 100]
valid.Contains(100);                               // True - closed upper endpoint

var q1 = Interval<int>.ClosedOpen(0, 90);          // [0, 90)
var q2 = Interval<int>.ClosedOpen(90, 181);        // [90, 181)
q1.Overlaps(q2);                                   // False - half-open buckets partition cleanly
q1.TryUnion(q2, out var half);                     // half = [0, 181)

Endpoint inclusivity is independent on each side, so closed, open, and both half-open shapes are all expressible - see Numerics & Financial concepts for when each shape fits.

Choosing a type

Scenario Reach for Notes
Exact thirds, percentages, or ratios with no drift Fraction<T> Canonical form on construction; 2/4 and 1/2 are indistinguishable.
Long calculation chains where overflow is possible Fraction<BigInteger> Eliminates the narrowing step entirely; no OverflowException.
Best rational approximation to a double or decimal Fraction<T>.Approximate(value, maxDenominator) Walks continued-fraction convergents.
Range membership, overlap, or set algebra over numbers Interval<T> Independent endpoint inclusivity; Contains, Intersect, TryUnion, Overlaps.
Unbounded or half-bounded ranges ((-∞, 5], [0, +∞)) Interval<T>.All / AtLeast / AtMost / … Explicit endpoint metadata, not float infinities; works for int / decimal / BigInteger.
Difference or symmetric difference of two intervals Interval<T>.Difference / SymmetricDifference Returns IntervalPair<T> - at most two disjoint pieces.
Discrete integer ranges (indices, IDs, pages) DiscreteInterval<T> Successor-aware emptiness and adjacency; Open(1, 2) is empty.
An arbitrary union of disjoint ranges, with complement IntervalSet<T> Normalized N-ary Union / Intersect / Except / Complement.
An amount in a known currency with safe arithmetic Money<TCurrency> Cross-currency arithmetic fails the build.
Currency known only at runtime (deserialisation, generic invoicing) Money Runtime ISO tag; convert to typed money at the boundary with As<T>().
Multi-currency totals with aggregate-then-convert MoneyBag Zero balances pruned; one FX lookup per source currency.
Splitting an amount across N shares without remainder loss Money<T>.Allocate(parts) / Allocate(ratios) Largest-remainder distribution; the shares always sum to the original.
Sub-minor-unit-precise interest or percentage chains Money<T>.ToFraction() / FromFraction() / MultiplyExact() The bridge between the two libraries.
FX conversion with dated rates and audit provenance IDatedRateProvider + RateLookupResult Provider name, actual date used, offset days, resolution policy.
Prioritised fallback (or averaging) across multiple FX sources AggregatingRateProvider In the Bodu.Financial.ExchangeRates.Caching package; deterministic first-available (PriorityFallback) or mean (Average) via a pluggable strategy.
Registering the financial stack in a DI container AddFinancialService(...) Currency lookup, monetary contexts, providers; add AddFinancialJson(...) from Bodu.Financial.Serialization.Json for the keyed JSON options.

How the pieces compose

A representative end-to-end flow - accumulate exactly, settle once:

using Bodu.Financial;
using Bodu.Financial.Currencies;
using Bodu.Numerics;
using System.Numerics;

Money<USD> principal = new(10_000m);

// Escape to exact rationals for the compound-interest chain.
Fraction<BigInteger> exact = principal.ToFraction();
Fraction<BigInteger> monthlyRate = Fraction<BigInteger>.Create(425, 120_000); // 4.25% / 12

for (int month = 0; month < 360; month++)
{
    exact += exact * monthlyRate;          // no rounding, no drift
}

// One rounding event, at the boundary, back to settlement precision.
Money<USD> settled = Money<USD>.FromFraction(exact);

Each loop iteration is exact; the only place a cent can be gained or lost is the single, explicit FromFraction call - which is also the place an auditor looks.

Install

dotnet add package Bodu.Numerics
dotnet add package Bodu.Numerics.Serialization.Json     # JSON converters for the numeric types
dotnet add package Bodu.Financial
dotnet add package Bodu.Financial.Serialization.Json    # JSON converters for the financial types
dotnet add package Bodu.Financial.DependencyInjection
dotnet add package Bodu.Financial.ExchangeRates.Ecb     # or any other per-source provider package
dotnet add package Bodu.Financial.ExchangeRates.Caching

Bodu.Financial depends on Bodu.Numerics, and Bodu.Financial.DependencyInjection depends on Bodu.Financial - install only the topmost package your application consumes. The two Serialization.Json companions are opt-in: neither core library references System.Text.Json, so add a companion only where you serialize. Each per-source provider package pulls in Bodu.Financial.ExchangeRates; add Bodu.Financial.ExchangeRates.Caching (and its .Sqlite / .Distributed backends) only when you want caching or aggregation in front of a feed.

Where to go next