use super::super::error::MtError;
use super::super::types::{Block4, TagField};
#[derive(Debug, Clone, PartialEq)]
pub struct Amount {
pub currency: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Account {
pub iban: Option<String>,
pub bic: Option<String>,
pub account: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PartyInfo {
pub account: Option<Account>,
pub name: Option<String>,
pub address_lines: Vec<String>,
}
pub fn parse_amount(s: &str) -> Result<Amount, MtError> {
if s.len() < 4 {
return Err(MtError::InvalidFieldValue {
tag: "amount".into(),
detail: format!("too short to contain currency + amount: '{s}'"),
});
}
let currency = s[..3].to_string();
if !currency.chars().all(|c| c.is_ascii_uppercase()) {
return Err(MtError::InvalidFieldValue {
tag: "amount".into(),
detail: format!("invalid currency code '{currency}'"),
});
}
let raw_amount = &s[3..];
let value = raw_amount.replace(',', ".");
Ok(Amount { currency, value })
}
pub fn parse_date_yymmdd(s: &str) -> Result<String, MtError> {
if s.len() != 6 || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(MtError::InvalidFieldValue {
tag: "date".into(),
detail: format!("expected 6-digit YYMMDD date, got '{s}'"),
});
}
let yy: u32 = s[..2].parse().expect("checked: string is 6 ASCII digits");
let mm = &s[2..4];
let dd = &s[4..6];
let century = if yy >= 80 { 1900u32 } else { 2000u32 };
let year = century + yy;
Ok(format!("{year:04}-{mm}-{dd}"))
}
pub fn parse_date_mmdd(year: u32, s: &str) -> Result<String, MtError> {
if s.len() != 4 || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(MtError::InvalidFieldValue {
tag: "date".into(),
detail: format!("expected 4-digit MMDD date, got '{s}'"),
});
}
let mm = &s[..2];
let dd = &s[2..4];
Ok(format!("{year:04}-{mm}-{dd}"))
}
pub fn parse_account(s: &str) -> Result<Account, MtError> {
if let Some(rest) = s.strip_prefix("//") {
if let Some((bic, acct)) = rest.split_once('/') {
Ok(Account {
iban: None,
bic: Some(bic.to_string()),
account: Some(acct.to_string()),
})
} else {
Ok(Account {
iban: None,
bic: Some(rest.to_string()),
account: None,
})
}
} else if let Some(rest) = s.strip_prefix('/') {
Ok(Account {
iban: Some(rest.to_string()),
bic: None,
account: None,
})
} else {
Ok(Account {
iban: None,
bic: None,
account: Some(s.to_string()),
})
}
}
pub fn parse_party_lines(lines: &[&str]) -> PartyInfo {
if lines.is_empty() {
return PartyInfo::default();
}
let mut iter = lines.iter().peekable();
let first = *iter.next().expect("checked: lines is non-empty");
let (account, name_from_first) = if first.starts_with('/') {
let acct = parse_account(first).ok();
(acct, None)
} else {
(None, Some(first.to_string()))
};
let mut remaining: Vec<String> = iter.map(|l| (*l).to_string()).collect();
let name = if let Some(n) = name_from_first {
Some(n)
} else if !remaining.is_empty() {
Some(remaining.remove(0))
} else {
None
};
PartyInfo {
account,
name,
address_lines: remaining,
}
}
pub fn parse_party_value(value: &str) -> PartyInfo {
let lines: Vec<&str> = value.lines().collect();
parse_party_lines(&lines)
}
pub(crate) fn require_field<'a>(
block4: &'a Block4,
tag: &str,
message_type: &str,
) -> Result<&'a TagField, MtError> {
block4.get(tag).ok_or_else(|| MtError::MissingField {
tag: tag.to_string(),
message_type: message_type.to_string(),
})
}
pub(crate) fn parse_32a(s: &str, tag: &str) -> Result<(String, String, String), MtError> {
if s.len() < 10 {
return Err(MtError::InvalidFieldValue {
tag: tag.to_string(),
detail: format!("too short: '{s}'"),
});
}
let date = parse_date_yymmdd(&s[..6]).map_err(|_| MtError::InvalidFieldValue {
tag: tag.to_string(),
detail: format!("invalid date in '{s}'"),
})?;
let rest = &s[6..];
let amt = parse_amount(rest)?;
Ok((date, amt.currency, amt.value))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_amount_basic() {
let a = parse_amount("EUR1000,50").unwrap();
assert_eq!(a.currency, "EUR");
assert_eq!(a.value, "1000.50");
}
#[test]
fn test_parse_amount_no_decimal() {
let a = parse_amount("USD50000,00").unwrap();
assert_eq!(a.currency, "USD");
assert_eq!(a.value, "50000.00");
}
#[test]
fn test_parse_amount_short_fails() {
assert!(parse_amount("EU").is_err());
}
#[test]
fn test_parse_date_yymmdd_century_20() {
assert_eq!(parse_date_yymmdd("230615").unwrap(), "2023-06-15");
}
#[test]
fn test_parse_date_yymmdd_century_19() {
assert_eq!(parse_date_yymmdd("991231").unwrap(), "1999-12-31");
}
#[test]
fn test_parse_date_yymmdd_boundary() {
assert_eq!(parse_date_yymmdd("800101").unwrap(), "1980-01-01");
assert_eq!(parse_date_yymmdd("790101").unwrap(), "2079-01-01");
}
#[test]
fn test_parse_date_invalid() {
assert!(parse_date_yymmdd("2306").is_err());
assert!(parse_date_yymmdd("23061X").is_err());
}
#[test]
fn test_parse_account_iban() {
let a = parse_account("/DE89370400440532013000").unwrap();
assert_eq!(a.iban.as_deref(), Some("DE89370400440532013000"));
assert!(a.bic.is_none());
}
#[test]
fn test_parse_account_bic_account() {
let a = parse_account("//CHASUS33/1234567890").unwrap();
assert_eq!(a.bic.as_deref(), Some("CHASUS33"));
assert_eq!(a.account.as_deref(), Some("1234567890"));
}
#[test]
fn test_parse_account_raw() {
let a = parse_account("1234567890").unwrap();
assert_eq!(a.account.as_deref(), Some("1234567890"));
}
#[test]
fn test_parse_party_lines_with_account() {
let lines = vec!["/DE89370400440532013000", "JOHN DOE", "123 MAIN ST"];
let p = parse_party_lines(&lines);
assert!(p.account.is_some());
assert_eq!(p.name.as_deref(), Some("JOHN DOE"));
assert_eq!(p.address_lines, vec!["123 MAIN ST"]);
}
#[test]
fn test_parse_party_lines_name_only() {
let lines = vec!["JOHN DOE"];
let p = parse_party_lines(&lines);
assert!(p.account.is_none());
assert_eq!(p.name.as_deref(), Some("JOHN DOE"));
assert!(p.address_lines.is_empty());
}
#[test]
fn test_parse_party_empty() {
let p = parse_party_lines(&[]);
assert!(p.account.is_none());
assert!(p.name.is_none());
}
}