use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Iban(String);
impl Iban {
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
#[must_use]
pub fn country_code(&self) -> &str {
&self.0[..2]
}
#[inline]
#[must_use]
pub fn check_digits(&self) -> &str {
&self.0[2..4]
}
#[inline]
#[must_use]
pub fn bban(&self) -> &str {
&self.0[4..]
}
}
impl std::fmt::Display for Iban {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, chunk) in group_of_four(&self.0).enumerate() {
if i > 0 {
f.write_str(" ")?;
}
f.write_str(chunk)?;
}
Ok(())
}
}
pub(crate) fn group_of_four(s: &str) -> impl Iterator<Item = &str> {
(0..s.len())
.step_by(4)
.filter_map(move |i| s.get(i..(i + 4).min(s.len())))
}
impl AsRef<str> for Iban {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for Iban {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for Iban {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<Iban> for String {
fn from(iban: Iban) -> Self {
iban.0
}
}
impl FromStr for Iban {
type Err = IbanError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate_iban(s)
}
}
impl TryFrom<&str> for Iban {
type Error = IbanError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
validate_iban(s)
}
}
impl TryFrom<String> for Iban {
type Error = IbanError;
fn try_from(s: String) -> Result<Self, Self::Error> {
validate_iban(&s)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Iban {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Iban {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
validate_iban(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum IbanError {
#[error("IBAN length {len} is outside the valid range 15–34")]
InvalidLength {
len: usize,
},
#[error("IBAN contains invalid character: {ch:?}")]
InvalidCharacter {
ch: char,
},
#[error("IBAN checksum mismatch (mod97 = {remainder}, expected 1)")]
InvalidChecksum {
remainder: u64,
},
#[error("IBAN for country {country} must be {expected} characters, got {actual}")]
WrongLengthForCountry {
country: String,
expected: usize,
actual: usize,
},
#[error("IBAN must start with a 2-letter country code, got {code:?}")]
InvalidCountryCode {
code: String,
},
#[error("IBAN check digits must be 2 digits, got {value:?}")]
NonNumericCheckDigits {
value: String,
},
#[error(
"IBAN position {position} must be {expected} for country {country}, got {found:?} \
(registry structure {structure})"
)]
InvalidBbanFormat {
country: String,
position: usize,
expected: BbanCharClass,
found: char,
structure: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BbanCharClass {
Digit,
UpperAlpha,
Alphanumeric,
}
impl BbanCharClass {
#[inline]
#[must_use]
pub const fn as_registry_symbol(self) -> &'static str {
match self {
Self::Digit => "n",
Self::UpperAlpha => "a",
Self::Alphanumeric => "c",
}
}
#[inline]
#[must_use]
pub const fn admits(self, ch: char) -> bool {
match self {
Self::Digit => ch.is_ascii_digit(),
Self::UpperAlpha => ch.is_ascii_uppercase(),
Self::Alphanumeric => ch.is_ascii_digit() || ch.is_ascii_uppercase(),
}
}
const fn from_symbol(b: u8) -> Option<Self> {
match b {
b'n' => Some(Self::Digit),
b'a' => Some(Self::UpperAlpha),
b'c' => Some(Self::Alphanumeric),
_ => None,
}
}
}
impl std::fmt::Display for BbanCharClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Digit => "a digit",
Self::UpperAlpha => "an uppercase letter",
Self::Alphanumeric => "a digit or uppercase letter",
})
}
}
#[must_use]
#[allow(clippy::match_same_arms, reason = "a data table, not control flow")]
#[allow(clippy::too_many_lines, reason = "89 registry entries, one per line")]
pub fn iban_bban_format(country: &str) -> Option<&'static str> {
match country {
"AD" => Some("nnnnnnnncccccccccccc"), "AE" => Some("nnnnnnnnnnnnnnnnnnn"), "AL" => Some("nnnnnnnncccccccccccccccc"), "AT" => Some("nnnnnnnnnnnnnnnn"), "AZ" => Some("aaaacccccccccccccccccccc"), "BA" => Some("nnnnnnnnnnnnnnnn"), "BE" => Some("nnnnnnnnnnnn"), "BG" => Some("aaaannnnnncccccccc"), "BH" => Some("aaaacccccccccccccc"), "BI" => Some("nnnnnnnnnnnnnnnnnnnnnnn"), "BR" => Some("nnnnnnnnnnnnnnnnnnnnnnnac"), "BY" => Some("ccccnnnncccccccccccccccc"), "CH" => Some("nnnnncccccccccccc"), "CR" => Some("nnnnnnnnnnnnnnnnnn"), "CY" => Some("nnnnnnnncccccccccccccccc"), "CZ" => Some("nnnnnnnnnnnnnnnnnnnn"), "DE" => Some("nnnnnnnnnnnnnnnnnn"), "DJ" => Some("nnnnnnnnnnnnnnnnnnnnnnn"), "DK" => Some("nnnnnnnnnnnnnn"), "DO" => Some("ccccnnnnnnnnnnnnnnnnnnnn"), "EE" => Some("nnnnnnnnnnnnnnnn"), "EG" => Some("nnnnnnnnnnnnnnnnnnnnnnnnn"), "ES" => Some("nnnnnnnnnnnnnnnnnnnn"), "FI" => Some("nnnnnnnnnnnnnn"), "FK" => Some("aannnnnnnnnnnn"), "FO" => Some("nnnnnnnnnnnnnn"), "FR" => Some("nnnnnnnnnncccccccccccnn"), "GB" => Some("aaaannnnnnnnnnnnnn"), "GE" => Some("aannnnnnnnnnnnnnnn"), "GI" => Some("aaaaccccccccccccccc"), "GL" => Some("nnnnnnnnnnnnnn"), "GR" => Some("nnnnnnncccccccccccccccc"), "GT" => Some("cccccccccccccccccccccccc"), "HN" => Some("aaaannnnnnnnnnnnnnnnnnnn"), "HR" => Some("nnnnnnnnnnnnnnnnn"), "HU" => Some("nnnnnnnnnnnnnnnnnnnnnnnn"), "IE" => Some("aaaannnnnnnnnnnnnn"), "IL" => Some("nnnnnnnnnnnnnnnnnnn"), "IQ" => Some("aaaannnnnnnnnnnnnnn"), "IS" => Some("nnnnnnnnnnnnnnnnnnnnnn"), "IT" => Some("annnnnnnnnncccccccccccc"), "JO" => Some("aaaannnncccccccccccccccccc"), "KW" => Some("aaaacccccccccccccccccccccc"), "KZ" => Some("nnnccccccccccccc"), "LB" => Some("nnnncccccccccccccccccccc"), "LC" => Some("aaaacccccccccccccccccccccccc"), "LI" => Some("nnnnncccccccccccc"), "LT" => Some("nnnnnnnnnnnnnnnn"), "LU" => Some("nnnccccccccccccc"), "LV" => Some("aaaaccccccccccccc"), "LY" => Some("nnnnnnnnnnnnnnnnnnnnn"), "MC" => Some("nnnnnnnnnncccccccccccnn"), "MD" => Some("cccccccccccccccccccc"), "ME" => Some("nnnnnnnnnnnnnnnnnn"), "MK" => Some("nnnccccccccccnn"), "MN" => Some("nnnnnnnnnnnnnnnn"), "MR" => Some("nnnnnnnnnnnnnnnnnnnnnnn"), "MT" => Some("aaaannnnncccccccccccccccccc"), "MU" => Some("aaaannnnnnnnnnnnnnnnnnnaaa"), "NI" => Some("aaaannnnnnnnnnnnnnnnnnnn"), "NL" => Some("aaaannnnnnnnnn"), "NO" => Some("nnnnnnnnnnn"), "OM" => Some("nnncccccccccccccccc"), "PK" => Some("aaaacccccccccccccccc"), "PL" => Some("nnnnnnnnnnnnnnnnnnnnnnnn"), "PS" => Some("aaaaccccccccccccccccccccc"), "PT" => Some("nnnnnnnnnnnnnnnnnnnnn"), "QA" => Some("aaaaccccccccccccccccccccc"), "RO" => Some("aaaacccccccccccccccc"), "RS" => Some("nnnnnnnnnnnnnnnnnn"), "RU" => Some("nnnnnnnnnnnnnnccccccccccccccc"), "SA" => Some("nncccccccccccccccccc"), "SC" => Some("aaaannnnnnnnnnnnnnnnnnnnaaa"), "SD" => Some("nnnnnnnnnnnnnn"), "SE" => Some("nnnnnnnnnnnnnnnnnnnn"), "SI" => Some("nnnnnnnnnnnnnnn"), "SK" => Some("nnnnnnnnnnnnnnnnnnnn"), "SM" => Some("annnnnnnnnncccccccccccc"), "SO" => Some("nnnnnnnnnnnnnnnnnnn"), "ST" => Some("nnnnnnnnnnnnnnnnnnnnn"), "SV" => Some("aaaannnnnnnnnnnnnnnnnnnn"), "TL" => Some("nnnnnnnnnnnnnnnnnnn"), "TN" => Some("nnnnnnnnnnnnnnnnnnnn"), "TR" => Some("nnnnnncccccccccccccccc"), "UA" => Some("nnnnnnccccccccccccccccccc"), "VA" => Some("nnnnnnnnnnnnnnnnnn"), "VG" => Some("aaaannnnnnnnnnnnnnnn"), "XK" => Some("nnnnnnnnnnnnnnnn"), "YE" => Some("aaaannnncccccccccccccccccc"), _ => None,
}
}
fn bban_class_at(country: &str, index: usize) -> Option<BbanCharClass> {
let symbol = *iban_bban_format(country)?.as_bytes().get(index)?;
BbanCharClass::from_symbol(symbol)
}
#[inline]
#[must_use]
pub fn iban_country_length(country: &str) -> Option<usize> {
Some(4 + iban_bban_format(country)?.len())
}
const SEPA_COUNTRIES: [&str; 42] = [
"AD", "AL", "AT", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GI",
"GR", "HR", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MD", "ME", "MK", "MT", "NL",
"NO", "PL", "PT", "RO", "RS", "SE", "SI", "SK", "SM", "VA",
];
#[must_use]
pub fn is_sepa_country(country: &str) -> bool {
let mut buf = [0u8; 2];
if country.len() != 2 || !country.is_ascii() {
return false;
}
buf.copy_from_slice(country.as_bytes());
buf.make_ascii_uppercase();
let upper = std::str::from_utf8(&buf).unwrap_or("");
SEPA_COUNTRIES.contains(&upper)
}
impl Iban {
#[inline]
#[must_use]
pub fn is_sepa(&self) -> bool {
is_sepa_country(self.country_code())
}
}
#[must_use = "ignoring a validated IBAN loses the result"]
pub fn validate_iban(raw: &str) -> Result<Iban, IbanError> {
let normalised: String = raw
.chars()
.filter(|c| !c.is_whitespace())
.map(|c| c.to_ascii_uppercase())
.collect();
let len = normalised.chars().count();
if !(15..=34).contains(&len) {
return Err(IbanError::InvalidLength { len });
}
for c in normalised.chars() {
if !c.is_ascii_alphanumeric() {
return Err(IbanError::InvalidCharacter { ch: c });
}
}
let country = &normalised[..2];
if !country.bytes().all(|b| b.is_ascii_uppercase()) {
return Err(IbanError::InvalidCountryCode {
code: country.to_owned(),
});
}
let check_digits = &normalised[2..4];
if !check_digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(IbanError::NonNumericCheckDigits {
value: check_digits.to_owned(),
});
}
if let Some(structure) = iban_bban_format(country) {
let expected_len = 4 + structure.len();
if len != expected_len {
return Err(IbanError::WrongLengthForCountry {
country: country.to_owned(),
expected: expected_len,
actual: len,
});
}
for (i, ch) in normalised[4..].chars().enumerate() {
let Some(class) = bban_class_at(country, i) else {
continue;
};
if !class.admits(ch) {
return Err(IbanError::InvalidBbanFormat {
country: country.to_owned(),
position: i + 5, expected: class,
found: ch,
structure,
});
}
}
}
let (header, bban) = (&normalised[..4], &normalised[4..]);
let mut remainder: u64 = 0;
for &b in bban.as_bytes().iter().chain(header.as_bytes()) {
remainder = if b.is_ascii_digit() {
remainder * 10 + u64::from(b - b'0')
} else {
remainder * 100 + u64::from(b - b'A') + 10
} % 97;
}
if remainder == 1 {
Ok(Iban(normalised))
} else {
Err(IbanError::InvalidChecksum { remainder })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn de_iban_with_spaces() {
let iban = validate_iban("DE89 3704 0044 0532 0130 00").unwrap();
assert_eq!(iban.as_str(), "DE89370400440532013000");
assert_eq!(iban.country_code(), "DE");
assert_eq!(iban.check_digits(), "89");
}
#[test]
fn de_iban_no_spaces() {
assert!(validate_iban("DE89370400440532013000").is_ok());
}
#[test]
fn de_iban_sparkasse() {
assert!(validate_iban("DE29100500005001065004").is_ok());
}
#[test]
fn nl_iban() {
assert!(validate_iban("NL91ABNA0417164300").is_ok());
}
#[test]
fn gb_iban() {
assert!(validate_iban("GB29 NWBK 6016 1331 9268 19").is_ok());
}
#[test]
fn at_iban() {
assert!(validate_iban("AT611904300234573201").is_ok());
}
#[test]
fn ch_iban() {
assert!(validate_iban("CH5604835012345678009").is_ok());
}
#[test]
fn lowercase_normalised() {
assert!(validate_iban("de89370400440532013000").is_ok());
}
#[test]
fn wrong_checksum() {
let err = validate_iban("DE89370400440532013001").unwrap_err();
assert!(matches!(err, IbanError::InvalidChecksum { .. }));
}
#[test]
fn bban_accessor() {
let iban = validate_iban("DE89370400440532013000").unwrap();
assert_eq!(iban.bban(), "370400440532013000");
}
#[test]
fn wrong_length_for_de() {
let err = validate_iban("DE89370400440532013").unwrap_err();
assert!(matches!(
err,
IbanError::WrongLengthForCountry {
expected: 22,
actual: 19,
..
}
));
}
#[test]
fn wrong_length_for_nl() {
let err = validate_iban("NL91ABNA041716430099").unwrap_err();
assert!(matches!(
err,
IbanError::WrongLengthForCountry { expected: 18, .. }
));
}
#[test]
fn unknown_country_skips_length_check() {
let result = validate_iban("XX89370400440532013000");
assert!(!matches!(
result,
Err(IbanError::WrongLengthForCountry { .. })
));
}
#[test]
fn latvia_is_registered() {
assert_eq!(iban_country_length("LV"), Some(21));
assert!(validate_iban("LV80BANK0000435195001").is_ok());
}
const REGISTRY_EXAMPLES: [&str; 78] = [
"AD1200012030200359100100",
"AE070331234567890123456",
"AL47212110090000000235698741",
"AT611904300234573201",
"AZ21NABZ00000000137010001944",
"BA391290079401028494",
"BE68539007547034",
"BG80BNBG96611020345678",
"BH67BMAG00001299123456",
"BR9700360305000010009795493P1",
"BY13NBRB3600900000002Z00AB00",
"CH9300762011623852957",
"CR05015202001026284066",
"CY17002001280000001200527600",
"CZ6508000000192000145399",
"DE89370400440532013000",
"DJ2110002010010409943020008",
"DK5000400440116243",
"DO28BAGR00000001212453611324",
"EE382200221020145685",
"EG380019000500000000263180002",
"ES9121000418450200051332",
"FI2112345600000785",
"FO2000400440116243",
"FR1420041010050500013M02606",
"GB29NWBK60161331926819",
"GE29NB0000000101904917",
"GI75NWBK000000007099453",
"GL2000400440116243",
"GR1601101250000000012300695",
"GT82TRAJ01020000001210029690",
"HN54PISA00000000000000123124",
"HR1210010051863000160",
"HU42117730161111101800000000",
"IE29AIBK93115212345678",
"IL620108000000099999999",
"IQ98NBIQ850123456789012",
"IS140159260076545510730339",
"IT60X0542811101000000123456",
"JO94CBJO0010000000000131000302",
"KW81CBKU0000000000001234560101",
"KZ86125KZT5004100100",
"LB62099900000001001901229114",
"LC55HEMM000100010012001200023015",
"LI21088100002324013AA",
"LT121000011101001000",
"LU280019400644750000",
"LV80BANK0000435195001",
"MC5811222000010123456789030",
"MD24AG000225100013104168",
"ME25505000012345678951",
"MK07250120000058984",
"MR1300020001010000123456753",
"MT84MALT011000012345MTLCAST001S",
"MU17BOMM0101101030300200000MUR",
"NL91ABNA0417164300",
"NO9386011117947",
"PK36SCBL0000001123456702",
"PL61109010140000071219812874",
"PS92PALS000000000400123456702",
"PT50000201231234567890154",
"QA58DOHB00001234567890ABCDEFG",
"RO49AAAA1B31007593840000",
"RS35260005601001611379",
"SA0380000000608010167519",
"SC18SSCB11010000000000001497USD",
"SE4550000000058398257466",
"SI56191000000123438",
"SK3112000000198742637541",
"SM86U0322509800000000270100",
"ST68000100010051845310112",
"SV62CENR00000000000000700025",
"TL380080012345678910157",
"TN5910006035183598478831",
"TR330006100519786457841326",
"UA213996220000026007233566001",
"VG96VPVG0000012345678901",
"XK051212012345678906",
];
#[test]
fn every_published_registry_example_validates() {
for example in REGISTRY_EXAMPLES {
assert!(
validate_iban(example).is_ok(),
"registry example {example} must validate: {:?}",
validate_iban(example)
);
}
}
#[test]
fn registry_structures_are_well_formed() {
for a in b'A'..=b'Z' {
for b in b'A'..=b'Z' {
let cc = String::from_utf8(vec![a, b]).unwrap();
let Some(structure) = iban_bban_format(&cc) else {
continue;
};
assert!(
structure.bytes().all(|s| matches!(s, b'n' | b'a' | b'c')),
"{cc} structure {structure:?} has an unknown symbol"
);
let len = 4 + structure.len();
assert!((15..=34).contains(&len), "{cc} length {len} is impossible");
assert_eq!(iban_country_length(&cc), Some(len));
}
}
}
#[test]
fn a_letter_typed_for_a_digit_is_caught_and_located() {
let err = validate_iban("DE8937O400440532013000").unwrap_err();
assert_eq!(
err,
IbanError::InvalidBbanFormat {
country: "DE".to_owned(),
position: 7,
expected: BbanCharClass::Digit,
found: 'O',
structure: "nnnnnnnnnnnnnnnnnn",
}
);
assert!(matches!(
validate_iban("NL914BNA0417164300"),
Err(IbanError::InvalidBbanFormat {
position: 5,
expected: BbanCharClass::UpperAlpha,
..
})
));
}
#[test]
fn the_iso_13616_header_is_checked_before_the_checksum() {
assert!(matches!(
validate_iban("1289370400440532013000"),
Err(IbanError::InvalidCountryCode { .. })
));
assert!(matches!(
validate_iban("DEX9370400440532013000"),
Err(IbanError::NonNumericCheckDigits { .. })
));
}
#[test]
fn an_unregistered_country_gets_the_checksum_and_nothing_more() {
assert_eq!(iban_bban_format("QQ"), None);
assert!(matches!(
validate_iban("QQ00ABC123"),
Err(IbanError::InvalidLength { .. })
));
let unregistered = validate_iban("XX89370400440532013000");
assert!(!matches!(
unregistered,
Err(IbanError::WrongLengthForCountry { .. } | IbanError::InvalidBbanFormat { .. })
));
}
#[test]
fn char_classes_admit_what_the_registry_says() {
assert!(BbanCharClass::Digit.admits('7'));
assert!(!BbanCharClass::Digit.admits('A'));
assert!(BbanCharClass::UpperAlpha.admits('A'));
assert!(!BbanCharClass::UpperAlpha.admits('7'));
assert!(!BbanCharClass::UpperAlpha.admits('a'));
assert!(BbanCharClass::Alphanumeric.admits('A'));
assert!(BbanCharClass::Alphanumeric.admits('7'));
assert!(!BbanCharClass::Alphanumeric.admits('-'));
assert_eq!(BbanCharClass::Digit.as_registry_symbol(), "n");
assert_eq!(BbanCharClass::UpperAlpha.to_string(), "an uppercase letter");
}
#[test]
fn registry_has_full_swift_entry_count() {
let count = (b'A'..=b'Z')
.flat_map(|a| (b'A'..=b'Z').map(move |b| [a, b]))
.filter(|cc| iban_country_length(std::str::from_utf8(cc).unwrap()).is_some())
.count();
assert_eq!(count, 89, "IBAN country registry entry count drifted");
}
#[test]
fn sepa_membership() {
assert!(is_sepa_country("DE"));
assert!(is_sepa_country("gi")); assert!(is_sepa_country("RS")); assert!(!is_sepa_country("FO"));
assert!(!is_sepa_country("GL"));
assert!(!is_sepa_country("XK")); assert!(!is_sepa_country("US"));
assert!(!is_sepa_country("D")); assert!(!is_sepa_country("DEU"));
assert!(!is_sepa_country("Ü!"));
assert!(validate_iban("DE89370400440532013000").unwrap().is_sepa());
}
#[test]
fn every_sepa_country_is_in_the_length_registry() {
for cc in SEPA_COUNTRIES {
assert!(
iban_country_length(cc).is_some(),
"{cc} is a SEPA country but has no registry length"
);
}
}
#[test]
fn territories_are_not_iban_prefixes() {
for t in [
"GP", "MQ", "RE", "YT", "GF", "BL", "MF", "PM", "PF", "TF", "NC", "WF", "JE", "GG",
"IM",
] {
assert_eq!(
iban_country_length(t),
None,
"{t} is a territory, not an IBAN country code"
);
}
}
#[test]
fn country_length_registry_spot_checks() {
use super::iban_country_length;
assert_eq!(iban_country_length("DE"), Some(22));
assert_eq!(iban_country_length("NL"), Some(18));
assert_eq!(iban_country_length("GB"), Some(22));
assert_eq!(iban_country_length("FR"), Some(27));
assert_eq!(iban_country_length("NO"), Some(15));
assert_eq!(iban_country_length("XX"), None);
}
#[test]
fn too_short() {
assert!(matches!(
validate_iban("DE89").unwrap_err(),
IbanError::InvalidLength { len: 4 }
));
}
#[test]
fn too_long() {
let long = "DE".to_string() + &"1".repeat(33);
assert!(matches!(
validate_iban(&long).unwrap_err(),
IbanError::InvalidLength { len: 35 }
));
}
#[test]
fn empty() {
assert!(matches!(
validate_iban("").unwrap_err(),
IbanError::InvalidLength { len: 0 }
));
}
#[test]
fn special_chars_rejected() {
assert!(matches!(
validate_iban("DE89@70400440532013000").unwrap_err(),
IbanError::InvalidCharacter { ch: '@' }
));
}
#[test]
fn display_groups_of_four() {
let iban = validate_iban("DE89370400440532013000").unwrap();
assert_eq!(iban.to_string(), "DE89 3704 0044 0532 0130 00");
}
#[test]
fn only_whitespace_rejected() {
assert!(matches!(
validate_iban(" ").unwrap_err(),
IbanError::InvalidLength { len: 0 }
));
}
#[test]
fn minimum_valid_length_norway() {
assert!(validate_iban("NO9386011117947").is_ok());
}
#[test]
fn from_str() {
let iban: Iban = "DE89370400440532013000".parse().unwrap();
assert_eq!(iban.as_str(), "DE89370400440532013000");
}
#[test]
fn try_from_str() {
let iban = Iban::try_from("DE89370400440532013000").unwrap();
assert_eq!(iban.country_code(), "DE");
}
#[test]
fn try_from_string() {
let iban = Iban::try_from("DE89370400440532013000".to_owned()).unwrap();
assert_eq!(iban.as_str(), "DE89370400440532013000");
}
#[test]
fn into_string() {
let iban = validate_iban("DE89370400440532013000").unwrap();
let s: String = iban.into();
assert_eq!(s, "DE89370400440532013000");
}
#[test]
fn ord() {
let a = validate_iban("AT611904300234573201").unwrap();
let b = validate_iban("DE89370400440532013000").unwrap();
assert!(a < b); }
#[test]
fn deref_to_str() {
let iban = validate_iban("DE89370400440532013000").unwrap();
assert_eq!(iban.len(), 22); }
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip() {
let iban = validate_iban("DE89370400440532013000").unwrap();
let json = serde_json::to_string(&iban).unwrap();
assert_eq!(json, r#""DE89370400440532013000""#);
let back: Iban = serde_json::from_str(&json).unwrap();
assert_eq!(back, iban);
}
#[cfg(feature = "serde")]
#[test]
fn serde_invalid_rejected() {
let result: Result<Iban, _> = serde_json::from_str(r#""NOTANIBAN""#);
assert!(result.is_err());
}
}