use std::fmt;
#[cfg(all(direct_wasm, target_arch = "wasm32"))]
use wasm_bindgen::prelude::*;
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ParseError {
InvalidLength(usize),
NonAscii,
InvalidBusinessPartyPrefix,
InvalidCountryCode,
InvalidBusinessPartySuffix,
InvalidBranchCode,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::InvalidLength(n) => {
write!(f, "invalid BIC length {n}: expected 8 or 11 characters")
}
ParseError::NonAscii => write!(f, "BIC contains a non-ASCII character"),
ParseError::InvalidBusinessPartyPrefix => {
write!(f, "invalid business party prefix: positions 1-4 must be letters")
}
ParseError::InvalidCountryCode => {
write!(f, "invalid country code: positions 5-6 must be letters")
}
ParseError::InvalidBusinessPartySuffix => {
write!(f, "invalid business party suffix: positions 7-8 must be alphanumeric")
}
ParseError::InvalidBranchCode => {
write!(f, "invalid branch code: positions 9-11 must be alphanumeric")
}
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BIC {
code: String,
}
impl BIC {
pub fn parse(input: &str) -> Result<BIC, ParseError> {
let code = input.to_ascii_uppercase();
if !code.is_ascii() {
return Err(ParseError::NonAscii);
}
let b = code.as_bytes();
if b.len() != 8 && b.len() != 11 {
return Err(ParseError::InvalidLength(b.len()));
}
if !b[0..4].iter().all(u8::is_ascii_alphabetic) {
return Err(ParseError::InvalidBusinessPartyPrefix);
}
if !b[4..6].iter().all(u8::is_ascii_alphabetic) {
return Err(ParseError::InvalidCountryCode);
}
if !b[6..8].iter().all(u8::is_ascii_alphanumeric) {
return Err(ParseError::InvalidBusinessPartySuffix);
}
if b.len() == 11 && !b[8..11].iter().all(u8::is_ascii_alphanumeric) {
return Err(ParseError::InvalidBranchCode);
}
Ok(BIC { code })
}
pub fn code(&self) -> &str {
&self.code
}
pub fn business_party_prefix(&self) -> &str {
&self.code[0..4]
}
pub fn bank_code(&self) -> &str {
self.business_party_prefix()
}
pub fn country_code(&self) -> &str {
&self.code[4..6]
}
pub fn business_party_suffix(&self) -> &str {
&self.code[6..8]
}
pub fn location_code(&self) -> &str {
self.business_party_suffix()
}
pub fn branch_code(&self) -> Option<&str> {
if self.code.len() == 11 {
Some(&self.code[8..11])
} else {
None
}
}
pub fn bic8(&self) -> &str {
&self.code[0..8]
}
pub fn bic11(&self) -> String {
match self.branch_code() {
Some(_) => self.code.clone(),
None => format!("{}XXX", self.code),
}
}
pub fn is_primary_office(&self) -> bool {
matches!(self.branch_code(), None | Some("XXX"))
}
pub fn is_test_bic(&self) -> bool {
self.code.as_bytes()[7] == b'0'
}
pub fn is_passive(&self) -> bool {
self.code.as_bytes()[7] == b'1'
}
#[cfg(feature = "iso3166")]
pub fn country(&self) -> Option<rust_iso3166::CountryCode> {
rust_iso3166::from_alpha2(self.country_code())
}
}
#[cfg(all(direct_wasm, target_arch = "wasm32"))]
#[wasm_bindgen]
impl BIC {
#[wasm_bindgen(getter, js_name = code)]
pub fn code_js(&self) -> String {
self.code.clone()
}
#[wasm_bindgen(getter, js_name = businessPartyPrefix)]
pub fn business_party_prefix_js(&self) -> String {
self.business_party_prefix().into()
}
#[wasm_bindgen(getter, js_name = countryCode)]
pub fn country_code_js(&self) -> String {
self.country_code().into()
}
#[wasm_bindgen(getter, js_name = businessPartySuffix)]
pub fn business_party_suffix_js(&self) -> String {
self.business_party_suffix().into()
}
#[wasm_bindgen(getter, js_name = branchCode)]
pub fn branch_code_js(&self) -> Option<String> {
self.branch_code().map(Into::into)
}
#[wasm_bindgen(js_name = bic11)]
pub fn bic11_js(&self) -> String {
self.bic11()
}
#[wasm_bindgen(getter, js_name = isPrimaryOffice)]
pub fn is_primary_office_js(&self) -> bool {
self.is_primary_office()
}
}
impl fmt::Display for BIC {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.code)
}
}
impl std::str::FromStr for BIC {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
BIC::parse(s)
}
}
#[cfg(feature = "serde")]
impl Serialize for BIC {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.code)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for BIC {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
BIC::parse(&s).map_err(|e| D::Error::custom(format!("invalid BIC '{s}': {e}")))
}
}
#[cfg_attr(all(direct_wasm, target_arch = "wasm32"), wasm_bindgen)]
pub fn parse(input: &str) -> Option<BIC> {
BIC::parse(input).ok()
}
#[cfg_attr(all(direct_wasm, target_arch = "wasm32"), wasm_bindgen)]
pub fn is_valid(input: &str) -> bool {
BIC::parse(input).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_11_char_bic() {
let bic = BIC::parse("DEUTDEFF500").unwrap();
assert_eq!(bic.code(), "DEUTDEFF500");
assert_eq!(bic.business_party_prefix(), "DEUT");
assert_eq!(bic.bank_code(), "DEUT");
assert_eq!(bic.country_code(), "DE");
assert_eq!(bic.business_party_suffix(), "FF");
assert_eq!(bic.location_code(), "FF");
assert_eq!(bic.branch_code(), Some("500"));
assert_eq!(bic.bic8(), "DEUTDEFF");
assert_eq!(bic.bic11(), "DEUTDEFF500");
assert!(!bic.is_primary_office());
}
#[test]
fn parses_8_char_bic() {
let bic = BIC::parse("DEUTDEFF").unwrap();
assert_eq!(bic.branch_code(), None);
assert_eq!(bic.bic8(), "DEUTDEFF");
assert_eq!(bic.bic11(), "DEUTDEFFXXX");
assert!(bic.is_primary_office());
}
#[test]
fn primary_office_branch_xxx() {
let bic = BIC::parse("NEDSZAJJXXX").unwrap();
assert_eq!(bic.branch_code(), Some("XXX"));
assert!(bic.is_primary_office());
}
#[test]
fn lowercase_is_normalised() {
let bic = BIC::parse("deutdeff500").unwrap();
assert_eq!(bic.code(), "DEUTDEFF500");
}
#[test]
fn test_and_passive_flags() {
assert!(BIC::parse("DEUTDEF0").unwrap().is_test_bic());
assert!(BIC::parse("DEUTDEF1").unwrap().is_passive());
assert!(!BIC::parse("DEUTDEFF").unwrap().is_test_bic());
assert!(!BIC::parse("DEUTDEFF").unwrap().is_passive());
}
#[test]
fn from_str_and_display() {
let bic: BIC = "DEUTDEFF500".parse().unwrap();
assert_eq!(bic.to_string(), "DEUTDEFF500");
}
#[test]
fn rejects_bad_input() {
assert_eq!(BIC::parse("DEUTDE").unwrap_err(), ParseError::InvalidLength(6));
assert_eq!(BIC::parse("DEUTDEFF5000").unwrap_err(), ParseError::InvalidLength(12));
assert_eq!(
BIC::parse("1EUTDEFF").unwrap_err(),
ParseError::InvalidBusinessPartyPrefix
);
assert_eq!(
BIC::parse("DEUT1EFF").unwrap_err(),
ParseError::InvalidCountryCode
);
assert_eq!(
BIC::parse("DEUTDE-F").unwrap_err(),
ParseError::InvalidBusinessPartySuffix
);
assert_eq!(
BIC::parse("DEUTDEFF5-0").unwrap_err(),
ParseError::InvalidBranchCode
);
assert_eq!(BIC::parse("DEUTDÉFF").unwrap_err(), ParseError::NonAscii);
}
#[test]
fn free_functions() {
assert!(is_valid("DEUTDEFF500"));
assert!(!is_valid("nope"));
assert!(parse("DEUTDEFF").is_some());
assert!(parse("nope").is_none());
}
#[cfg(feature = "iso3166")]
#[test]
fn resolves_country() {
let bic = BIC::parse("DEUTDEFF").unwrap();
assert_eq!(bic.country().unwrap().alpha3, "DEU");
assert!(BIC::parse("DEUTZZFF").unwrap().country().is_none());
}
}