hmrc-rates 0.3.0

HMRC exchange rates (monthly, spot, yearly average, weekly) with bundled history and GBP conversion.
Documentation

hmrc-rates

crates.io docs.rs PyPI

Official HMRC exchange rates as a Rust library. The full published history is compiled into your binary (~450 KB of read-only data), so Rates::new() is free and infallible — no parsing, no I/O, no startup cost. Conversions use exact rust_decimal arithmetic and are never rounded.

Install

[dependencies]
hmrc-rates = "0.3"
rust_decimal = "1"

Quick start

use hmrc_rates::{YearMonth, Rates, YearEnd};
use rust_decimal::Decimal;

fn main() -> Result<(), hmrc_rates::LookupError> {
    let rates = Rates::new();

    // Monthly customs/VAT rate; also accepts a chrono::NaiveDate
    let rate = rates.monthly_rate("USD", YearMonth::new(2025, 8).unwrap())?;
    let gbp = rate.to_gbp(Decimal::from(2500)); // exact — you choose the rounding
    println!("$2500 in Aug 2025 = £{}", gbp.round_dp(2));

    // Self Assessment style: the yearly average to 31 March 2025
    let eur = rates.average(YearEnd::march(2025))?.rate("EUR")?;
    println!("EUR average, year to 31 Mar 2025: {}", eur.units_per_gbp());
    Ok(())
}

Data coverage

One Rates value holds all four series HMRC has published:

Series Coverage Lookup
Monthly customs/VAT 2014-02 — present, no gaps monthly_rate(code, month)
Spot Dec 2010 — present, years ending 31 Mar / 31 Dec spot(YearEnd)
Yearly average Dec 2010 — present, years ending 31 Mar / 31 Dec average(YearEnd)
Weekly amendments 2014-01 — 2016-04, complete (discontinued by HMRC) weekly(date)

Lookups are strict: a period HMRC never published is an error, never a silently substituted older rate. The error says why: unknown currency, period not available (with the loaded range), or currency absent from that period. Fallback is opt-in and visible:

let rate = rates.monthly_rate_or_earlier("USD", month, 2)?;
rate.period() // reveals which month was actually used

Currency codes are as published by HMRC, which is not always ISO 4217 E.g., Ecuador appears as ECS. See docs/data-sources.md for where every rate comes from.

Features

Feature Default Adds
std yes
bundled yes the compiled-in history and Rates::new()
http no blocking Updater (ureq) with an on-disk cache
serde no compact string forms: "2026-07", "USD", "monthly"
cli no the hmrc-rates binary

The core is no_std + alloc: default-features = false, features = ["bundled"] builds on wasm32-unknown-unknown.

Fresh rates (http)

Updater fetches whatever HMRC has published since the crate release and caches the files verbatim in the system cache directory. Past periods are served from disk forever; amendable periods get a 24-hour TTL.

use hmrc_rates::Updater;

let updater = Updater::new();
// Offline fallback is explicit: stale rates should be a visible choice
let rates = updater.refreshed().unwrap_or_else(|e| {
    eprintln!("warning: possibly stale rates: {e}");
    updater.cached()
});

Python

The same library is on PyPI as hmrc-rates (Python 3.10+):

pip install hmrc-rates
from decimal import Decimal
from hmrc_rates import YearMonth, Rates, YearEnd

rates = Rates()
rate = rates.monthly_rate("USD", YearMonth(2025, 8))  # also accepts datetime.date or "2025-08"
gbp = rate.to_gbp(Decimal("2500"))                # exact decimal.Decimal, you choose the rounding
eur = rates.average(YearEnd.march(2025)).rate("EUR")

See python/README.md for the full Python API.

Documentation

MSRV and licence

Rust 1.85. Licensed under MIT.