hmrc-rates 0.3.2

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

hmrc-rates

crates.io docs.rs PyPI

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.

Disclaimer

This project is not affiliated with, endorsed by, or maintained by HMRC. Only the underlying rate data comes from HMRC.