use crate::error::{Error, Result};
pub const BILL_TABLE_SIZE: usize = 24;
const ENTRY_BYTES: usize = 5;
pub const BILL_TABLE_RESPONSE_LEN: usize = BILL_TABLE_SIZE * ENTRY_BYTES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BillEntry {
pub denomination: u32,
pub country_code: [u8; 3],
}
impl BillEntry {
pub fn is_empty(&self) -> bool {
self.denomination == 0
}
pub fn country_str(&self) -> &str {
std::str::from_utf8(&self.country_code).unwrap_or("???")
}
fn from_bytes(raw: &[u8; ENTRY_BYTES]) -> Self {
let mantissa = raw[0] as u32;
let denomination = mantissa * 10_u32.pow(raw[4] as u32);
let country_code = [raw[1], raw[2], raw[3]];
Self {
denomination,
country_code,
}
}
}
impl std::fmt::Display for BillEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
return write!(f, "(empty)");
}
write!(f, "{} {}", self.denomination, self.country_str())
}
}
#[derive(Debug, Clone)]
pub struct BillTable {
entries: [BillEntry; BILL_TABLE_SIZE],
}
impl BillTable {
pub fn from_response_data(data: &[u8]) -> Result<Self> {
if data.len() < BILL_TABLE_RESPONSE_LEN {
return Err(Error::InvalidFrame("GET_BILL_TABLE response too short"));
}
let empty = BillEntry {
denomination: 0,
country_code: [0; 3],
};
let mut entries = std::array::from_fn(|_| empty.clone());
for i in 0..BILL_TABLE_SIZE {
let offset = i * ENTRY_BYTES;
let raw: &[u8; ENTRY_BYTES] = data[offset..offset + ENTRY_BYTES]
.try_into()
.expect("slice length is exact");
entries[i] = BillEntry::from_bytes(raw);
}
Ok(Self { entries })
}
pub fn get(&self, index: u8) -> Option<&BillEntry> {
self.entries.get(index as usize)
}
pub fn iter(&self) -> impl Iterator<Item = (u8, &BillEntry)> {
self.entries.iter().enumerate().map(|(i, e)| (i as u8, e))
}
pub fn active_entries(&self) -> impl Iterator<Item = (u8, &BillEntry)> {
self.iter().filter(|(_, e)| !e.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry_bytes(mantissa: u8, code: &[u8; 3], scale: u8) -> [u8; 5] {
[mantissa, code[0], code[1], code[2], scale]
}
#[test]
fn parse_single_entry_no_scale() {
let raw = make_entry_bytes(5, b"USD", 0x00);
let entry = BillEntry::from_bytes(&raw);
assert_eq!(entry.denomination, 5);
assert_eq!(&entry.country_code, b"USD");
assert_eq!(entry.country_str(), "USD");
}
#[test]
fn parse_single_entry_with_scale() {
let raw = make_entry_bytes(1, b"TKM", 0x01);
let entry = BillEntry::from_bytes(&raw);
assert_eq!(entry.denomination, 10);
assert_eq!(entry.country_str(), "TKM");
}
#[test]
fn parse_full_table() {
let mut data = vec![0u8; BILL_TABLE_RESPONSE_LEN];
let entry = make_entry_bytes(5, b"USD", 0x00);
data[..5].copy_from_slice(&entry);
let table = BillTable::from_response_data(&data).unwrap();
assert_eq!(table.get(0).unwrap().denomination, 5);
assert!(table.get(1).unwrap().is_empty());
assert_eq!(table.active_entries().count(), 1);
}
#[test]
fn short_response_is_error() {
let data = vec![0u8; 10];
assert!(matches!(
BillTable::from_response_data(&data),
Err(Error::InvalidFrame(_))
));
}
}