use crate::country;
use crate::errors::ValidationError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Bic {
bytes: [u8; Self::MAX_LENGTH],
len: u8,
}
impl Bic {
pub const SHORT_LENGTH: usize = 8;
pub const MAX_LENGTH: usize = 11;
pub fn parse(s: &str) -> Result<Self, ValidationError> {
let found = s.chars().count();
if found != Self::SHORT_LENGTH && found != Self::MAX_LENGTH {
return Err(ValidationError::Structure {
rule: "BIC length must be 8 or 11",
});
}
for (i, ch) in s.chars().enumerate() {
let legal = if i < 6 {
ch.is_ascii_uppercase()
} else {
ch.is_ascii_uppercase() || ch.is_ascii_digit()
};
if !legal {
return Err(ValidationError::InvalidCharacter {
position: i + 1,
found: ch,
});
}
}
let mut bytes = [0u8; Self::MAX_LENGTH];
let src = s.as_bytes();
if let Some(slot) = bytes.get_mut(..found) {
slot.copy_from_slice(src);
}
let country_code = core::str::from_utf8(&bytes[4..6]).unwrap_or("");
if !country::is_iso_country(country_code) {
return Err(ValidationError::InvalidCountryCode);
}
Ok(Self {
bytes,
len: u8::try_from(found).unwrap_or(0),
})
}
pub fn validate(s: &str) -> Result<(), ValidationError> {
Self::parse(s).map(|_| ())
}
#[must_use]
pub const fn from_bytes_unchecked(bytes: [u8; Self::MAX_LENGTH], len: u8) -> Self {
Self { bytes, len }
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.len as usize
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
core::str::from_utf8(self.as_bytes()).unwrap_or("")
}
#[must_use]
#[inline]
pub fn as_bytes(&self) -> &[u8] {
self.bytes.get(..self.len()).unwrap_or(&[])
}
#[must_use]
#[inline]
pub fn institution(&self) -> &str {
core::str::from_utf8(&self.bytes[0..4]).unwrap_or("")
}
#[must_use]
#[inline]
pub fn country_code(&self) -> &str {
core::str::from_utf8(&self.bytes[4..6]).unwrap_or("")
}
#[must_use]
#[inline]
pub fn location_code(&self) -> &str {
core::str::from_utf8(&self.bytes[6..8]).unwrap_or("")
}
#[must_use]
#[inline]
pub fn branch_code(&self) -> Option<&str> {
if self.has_branch() {
Some(core::str::from_utf8(&self.bytes[8..11]).unwrap_or(""))
} else {
None
}
}
#[must_use]
#[inline]
pub fn has_branch(&self) -> bool {
self.len() == Self::MAX_LENGTH
}
#[must_use]
#[inline]
pub fn is_test_bic(&self) -> bool {
self.bytes[7] == b'0'
}
#[must_use]
#[inline]
pub fn is_passive(&self) -> bool {
self.bytes[7] == b'1'
}
#[must_use]
#[inline]
pub fn location_status(&self) -> char {
char::from(self.bytes[7])
}
}
impl core::fmt::Display for Bic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for Bic {
type Err = ValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Bic {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::display;
use core::str::FromStr;
const GOLDEN: &[&str] = &[
"DEUTDEFF", "DEUTDEFF500", "CHASUS33", "BOFAUS3N", "NDEAFIHH", ];
#[test]
fn parses_golden_bics() {
for &s in GOLDEN {
let bic = Bic::parse(s).unwrap_or_else(|e| panic!("{s} should parse: {e}"));
assert_eq!(bic.as_str(), s);
}
}
#[test]
fn segment_accessors_short() {
let bic = Bic::parse("DEUTDEFF").unwrap();
assert_eq!(bic.institution(), "DEUT");
assert_eq!(bic.country_code(), "DE");
assert_eq!(bic.location_code(), "FF");
assert_eq!(bic.branch_code(), None);
assert!(!bic.has_branch());
assert_eq!(bic.len(), 8);
assert_eq!(bic.as_bytes(), b"DEUTDEFF");
}
#[test]
fn segment_accessors_with_branch() {
let bic = Bic::parse("DEUTDEFF500").unwrap();
assert_eq!(bic.institution(), "DEUT");
assert_eq!(bic.country_code(), "DE");
assert_eq!(bic.location_code(), "FF");
assert_eq!(bic.branch_code(), Some("500"));
assert!(bic.has_branch());
assert_eq!(bic.len(), 11);
assert_eq!(bic.as_bytes(), b"DEUTDEFF500");
}
#[test]
fn length_constants() {
assert_eq!(Bic::SHORT_LENGTH, 8);
assert_eq!(Bic::MAX_LENGTH, 11);
}
#[test]
fn test_bic_flag() {
let bic = Bic::parse("DEUTDEF0").unwrap();
assert!(bic.is_test_bic());
assert!(!bic.is_passive());
}
#[test]
fn passive_participant_flag() {
let bic = Bic::parse("DEUTDEF1").unwrap();
assert!(bic.is_passive());
assert!(!bic.is_test_bic());
}
#[test]
fn live_bic_is_neither_test_nor_passive() {
let bic = Bic::parse("DEUTDEFF").unwrap();
assert!(!bic.is_test_bic());
assert!(!bic.is_passive());
}
#[test]
fn location_status_returns_eighth_character() {
assert_eq!(Bic::parse("DEUTDEFF").unwrap().location_status(), 'F');
assert_eq!(Bic::parse("DEUTDEF0").unwrap().location_status(), '0');
assert_eq!(Bic::parse("DEUTDEF1").unwrap().location_status(), '1');
assert_eq!(Bic::parse("DEUTDEFF500").unwrap().location_status(), 'F');
}
#[test]
fn rejects_wrong_length() {
assert_eq!(
Bic::parse("DEUTDEFF5"),
Err(ValidationError::Structure {
rule: "BIC length must be 8 or 11",
})
);
assert_eq!(
Bic::parse("DEUTDEFF50"),
Err(ValidationError::Structure {
rule: "BIC length must be 8 or 11",
})
);
assert_eq!(
Bic::parse(""),
Err(ValidationError::Structure {
rule: "BIC length must be 8 or 11",
})
);
assert_eq!(
Bic::parse("DEUTDEFF5000"),
Err(ValidationError::Structure {
rule: "BIC length must be 8 or 11",
})
);
}
#[test]
fn rejects_digit_in_institution() {
assert!(matches!(
Bic::parse("DEU1DEFF"),
Err(ValidationError::InvalidCharacter { position: 4, .. })
));
}
#[test]
fn rejects_digit_in_country() {
assert!(matches!(
Bic::parse("DEUT1EFF"),
Err(ValidationError::InvalidCharacter { position: 5, .. })
));
}
#[test]
fn rejects_lower_case() {
assert!(matches!(
Bic::parse("deutdeff"),
Err(ValidationError::InvalidCharacter { position: 1, .. })
));
}
#[test]
fn rejects_unknown_country_code() {
assert_eq!(
Bic::parse("DEUTZZFF"),
Err(ValidationError::InvalidCountryCode)
);
}
#[test]
fn rejects_substitute_prefix_as_country() {
assert_eq!(
Bic::parse("DEUTXS33"),
Err(ValidationError::InvalidCountryCode)
);
}
#[test]
fn rejects_bad_character_in_branch() {
assert!(matches!(
Bic::parse("DEUTDEFF50/"),
Err(ValidationError::InvalidCharacter { position: 11, .. })
));
}
#[test]
fn rejects_non_ascii_without_panic() {
assert!(Bic::parse("DEUTDEFé").is_err());
assert!(Bic::parse("ÉEUTDEFF").is_err());
assert!(Bic::parse("DEUTDEFF50é").is_err());
}
#[test]
fn round_trips_through_str() {
for &s in GOLDEN {
assert_eq!(Bic::parse(s).unwrap().as_str(), s);
}
}
#[test]
fn from_str_matches_parse() {
assert_eq!(Bic::from_str("DEUTDEFF"), Bic::parse("DEUTDEFF"));
assert_eq!(Bic::from_str("DEUTDEFF500"), Bic::parse("DEUTDEFF500"));
assert!(Bic::from_str("nonsense!").is_err());
}
#[test]
fn display_renders_identifier() {
assert_eq!(
display(Bic::parse("DEUTDEFF").unwrap()).as_str(),
"DEUTDEFF"
);
assert_eq!(
display(Bic::parse("DEUTDEFF500").unwrap()).as_str(),
"DEUTDEFF500"
);
}
#[test]
fn as_ref_str() {
let bic = Bic::parse("CHASUS33").unwrap();
let s: &str = bic.as_ref();
assert_eq!(s, "CHASUS33");
}
#[test]
fn validate_agrees_with_parse() {
assert!(Bic::validate("BOFAUS3N").is_ok());
assert!(Bic::validate("BOFAUS3").is_err());
}
#[test]
fn from_bytes_unchecked_round_trip() {
let short = Bic::from_bytes_unchecked(*b"DEUTDEFF\0\0\0", 8);
assert_eq!(short, Bic::parse("DEUTDEFF").unwrap());
let long = Bic::from_bytes_unchecked(*b"DEUTDEFF500", 11);
assert_eq!(long, Bic::parse("DEUTDEFF500").unwrap());
}
#[test]
fn unused_tail_bytes_are_zeroed() {
let parsed = Bic::parse("DEUTDEFF").unwrap();
let built = Bic::from_bytes_unchecked(*b"DEUTDEFF\0\0\0", 8);
assert_eq!(parsed, built);
}
#[test]
fn is_copy_and_eq_and_hashable() {
let a = Bic::parse("DEUTDEFF").unwrap();
let b = a; assert_eq!(a, b);
assert_ne!(a, Bic::parse("CHASUS33").unwrap());
assert_ne!(a, Bic::parse("DEUTDEFF500").unwrap());
let keys = [a, b];
assert_eq!(keys[0], keys[1]);
}
}