#![doc = include_str!("../README.md")]
use chrono::{NaiveDate, ParseResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct CurrencyDoc {
#[serde(alias = "CcyTbl")]
table: CurrencyTable,
#[serde(alias = "@Pblshd")]
published: String,
}
impl CurrencyDoc {
#[must_use]
pub fn table(&self) -> &CurrencyTable {
&self.table
}
pub fn published(&self) -> ParseResult<NaiveDate> {
NaiveDate::parse_from_str(&self.published, "%Y-%m-%d")
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct CurrencyTable {
#[serde(alias = "CcyNtry")]
entries: Vec<CurrencyEntry>,
}
impl CurrencyTable {
#[must_use]
pub fn entries(&self) -> &[CurrencyEntry] {
&self.entries
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct CurrencyEntry {
#[serde(alias = "CtryNm")]
country: String,
#[serde(alias = "CcyNm")]
name: Option<CurrencyName>,
#[serde(alias = "Ccy")]
currency: Option<String>,
#[serde(alias = "CcyNbr")]
number: Option<u16>,
#[serde(alias = "CcyMnrUnts")]
minor_unit: Option<String>,
}
impl CurrencyEntry {
#[must_use]
pub fn country(&self) -> &str {
self.country.trim()
}
#[must_use]
pub fn name(&self) -> Option<&CurrencyName> {
self.name.as_ref()
}
#[must_use]
pub fn currency(&self) -> Option<&str> {
self.currency.as_deref().map(str::trim)
}
#[must_use]
pub fn number(&self) -> Option<u16> {
self.number
}
#[must_use]
pub fn minor_unit(&self) -> Option<u8> {
self.minor_unit.as_deref().and_then(|mu| match mu.trim() {
"N.A." | "" => None,
other => other.parse::<u8>().ok(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct CurrencyName {
#[serde(alias = "@IsFund")]
is_fund: Option<bool>,
#[serde(alias = "$value")]
name: String,
}
impl CurrencyName {
#[must_use]
pub fn is_fund(&self) -> bool {
self.is_fund.unwrap_or_default()
}
#[must_use]
pub fn name(&self) -> &str {
self.name.trim()
}
}
#[cfg(test)]
mod test {
use super::*;
use quick_xml::de;
use std::{fs::File, io::BufReader, path::PathBuf};
const BASE_PATH: &str = env!("CARGO_MANIFEST_DIR");
const SRC_DIR: &str = "src";
#[yare::parameterized(
xml20260101 = { "2026-01-01.xml", 280 }
)]
fn counts(filename: &str, count: usize) {
let mut path = PathBuf::from(BASE_PATH);
path.push(SRC_DIR);
path.push(filename);
let file = File::open(path).expect("file");
let reader = BufReader::new(file);
let contents = de::from_reader::<_, CurrencyDoc>(reader).expect("XML reader");
let entries = contents.table().entries();
assert_eq!(count, entries.len());
for entry in entries {
if let Some(code) = entry.currency()
&& code == "USN"
{
assert!(entry.name().unwrap().is_fund());
}
}
}
}