#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
pub mod prelude {
pub use crate::{Bic, BicError};
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Bic(String);
impl Bic {
pub fn new(value: impl AsRef<str>) -> Result<Self, BicError> {
let value = value.as_ref().trim();
if value.len() != 8 && value.len() != 11 {
return Err(BicError::InvalidLength);
}
let mut normalized = String::with_capacity(value.len());
for (index, byte) in value.bytes().enumerate() {
let uppercase = byte.to_ascii_uppercase();
match index {
0..=3 if !uppercase.is_ascii_uppercase() => {
return Err(BicError::InvalidBankCode);
},
4..=5 if !uppercase.is_ascii_uppercase() => {
return Err(BicError::InvalidCountryCode);
},
6..=7 if !uppercase.is_ascii_alphanumeric() => {
return Err(BicError::InvalidLocationCode);
},
8..=10 if !uppercase.is_ascii_alphanumeric() => {
return Err(BicError::InvalidBranchCode);
},
_ => normalized.push(char::from(uppercase)),
}
}
Ok(Self(normalized))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn bank_code(&self) -> &str {
&self.0[..4]
}
#[must_use]
pub fn country_code(&self) -> &str {
&self.0[4..6]
}
#[must_use]
pub fn location_code(&self) -> &str {
&self.0[6..8]
}
#[must_use]
pub fn branch_code(&self) -> Option<&str> {
(self.0.len() == 11).then(|| &self.0[8..11])
}
#[must_use]
pub fn is_primary_office(&self) -> bool {
matches!(self.branch_code(), None | Some("XXX"))
}
}
impl AsRef<str> for Bic {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Bic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for Bic {
type Err = BicError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<&str> for Bic {
type Error = BicError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BicError {
InvalidLength,
InvalidBankCode,
InvalidCountryCode,
InvalidLocationCode,
InvalidBranchCode,
}
impl fmt::Display for BicError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength => formatter.write_str("BIC must be exactly 8 or 11 characters"),
Self::InvalidBankCode => formatter.write_str("BIC bank code must be four letters"),
Self::InvalidCountryCode => formatter.write_str("BIC country code must be two letters"),
Self::InvalidLocationCode => {
formatter.write_str("BIC location code must be two alphanumeric characters")
},
Self::InvalidBranchCode => {
formatter.write_str("BIC branch code must be three alphanumeric characters")
},
}
}
}
impl Error for BicError {}
#[cfg(test)]
mod tests {
use super::{Bic, BicError};
#[test]
fn accepts_valid_8_character_bic() -> Result<(), BicError> {
let bic = Bic::new("DEUTDEFF")?;
assert_eq!(bic.as_str(), "DEUTDEFF");
assert_eq!(bic.bank_code(), "DEUT");
assert_eq!(bic.country_code(), "DE");
assert_eq!(bic.location_code(), "FF");
assert_eq!(bic.branch_code(), None);
assert!(bic.is_primary_office());
Ok(())
}
#[test]
fn accepts_valid_11_character_bic() -> Result<(), BicError> {
let bic = Bic::new("deutdeff500")?;
assert_eq!(bic.as_str(), "DEUTDEFF500");
assert_eq!(bic.branch_code(), Some("500"));
assert!(!bic.is_primary_office());
Ok(())
}
#[test]
fn treats_xxx_branch_as_primary_office() -> Result<(), BicError> {
let bic = Bic::new("NEDSZAJJXXX")?;
assert_eq!(bic.branch_code(), Some("XXX"));
assert!(bic.is_primary_office());
Ok(())
}
#[test]
fn rejects_invalid_lengths() {
assert_eq!(Bic::new("DEUTDEF"), Err(BicError::InvalidLength));
assert_eq!(Bic::new("DEUTDEFF5000"), Err(BicError::InvalidLength));
}
#[test]
fn rejects_invalid_character_positions() {
assert_eq!(Bic::new("D3UTDEFF"), Err(BicError::InvalidBankCode));
assert_eq!(Bic::new("DEUTD3FF"), Err(BicError::InvalidCountryCode));
assert_eq!(Bic::new("DEUTDE@F"), Err(BicError::InvalidLocationCode));
assert_eq!(Bic::new("DEUTDEFF50@"), Err(BicError::InvalidBranchCode));
}
}