#[must_use]
pub fn validate(sheba: &str) -> bool {
let normalized = match canonicalize(sheba) {
Some(s) => s,
None => return false,
};
iban_checksum_ok(&normalized)
}
#[must_use]
pub fn bank(sheba: &str) -> Option<&'static str> {
bank_entry(sheba).map(|(_, en, _)| *en)
}
#[must_use]
pub fn bank_persian(sheba: &str) -> Option<&'static str> {
bank_entry(sheba).map(|(_, _, fa)| *fa)
}
#[must_use]
pub fn bank_code(sheba: &str) -> Option<String> {
let normalized = canonicalize(sheba)?;
if !iban_checksum_ok(&normalized) {
return None;
}
Some(normalized[4..7].to_owned())
}
#[must_use]
pub fn generate(bank_code: &str, account_type: char, account_number: &str) -> Option<String> {
if bank_code.len() != 3 || !bank_code.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if !account_type.is_ascii_digit() {
return None;
}
if account_number.is_empty()
|| account_number.len() > 18
|| !account_number.chars().all(|c| c.is_ascii_digit())
{
return None;
}
let bban = format!("{}{}{:0>18}", bank_code, account_type, account_number);
let rearranged = format!("{}IR00", bban);
let remainder = mod97_str(&rearranged);
let check = 98u32.saturating_sub(remainder as u32);
Some(format!("IR{:02}{}", check, bban))
}
fn mod97_str(s: &str) -> u64 {
let mut rem: u64 = 0;
for c in s.chars() {
let v: u64 = match c {
'0'..='9' => c.to_digit(10).unwrap() as u64,
'I' => 18,
'R' => 27,
_ => 0,
};
if v >= 10 {
rem = (rem * 100 + v) % 97;
} else {
rem = (rem * 10 + v) % 97;
}
}
rem
}
fn canonicalize(sheba: &str) -> Option<String> {
let mut out = String::with_capacity(26);
for c in sheba.chars() {
if c.is_whitespace() || c == '-' {
continue;
}
if c == 'I' || c == 'i' {
out.push('I');
} else if c == 'R' || c == 'r' {
out.push('R');
} else if c.is_ascii_digit() {
out.push(c);
} else if let Some(d) = super::persian_or_arabic_digit_to_ascii(c) {
out.push(d);
} else {
return None;
}
}
if out.len() != 26 || !out.starts_with("IR") {
return None;
}
if !out[2..].chars().all(|c| c.is_ascii_digit()) {
return None;
}
Some(out)
}
fn iban_checksum_ok(canon: &str) -> bool {
let rearranged = format!("{}IR{}", &canon[4..], &canon[2..4]);
let mut remainder: u128 = 0;
for c in rearranged.chars() {
let val = match c {
'I' => 18,
'R' => 27,
_ => c.to_digit(10).unwrap_or(0) as u128,
};
if val >= 10 {
remainder = (remainder * 100 + val) % 97;
} else {
remainder = (remainder * 10 + val) % 97;
}
}
remainder == 1
}
fn bank_entry(sheba: &str) -> Option<&'static (&'static str, &'static str, &'static str)> {
let normalized = canonicalize(sheba)?;
if !iban_checksum_ok(&normalized) {
return None;
}
let code = &normalized[4..7];
super::banks::SHEBA_CODES
.iter()
.find(|(c, _, _)| *c == code)
}
#[cfg(test)]
mod tests {
use super::*;
const VALID: &[&str] = &["IR062960000000100324200001", "IR580540105180021273113007"];
#[test]
fn valid_samples_pass() {
for s in VALID {
assert!(validate(s), "{s} should validate");
}
}
#[test]
fn rejects_bad_checksum() {
assert!(!validate("IR062960000000100324200002"));
}
#[test]
fn rejects_wrong_country() {
assert!(!validate("US062960000000100324200001"));
}
#[test]
fn rejects_short() {
assert!(!validate("IR12345"));
}
#[test]
fn accepts_spaces_and_dashes() {
assert!(validate("IR06 2960 0000 0010 0324 2000 01"));
assert!(validate("IR06-2960-0000-0010-0324-2000-01"));
}
#[test]
fn lowercase_ir_accepted() {
assert!(validate("ir062960000000100324200001"));
}
#[test]
fn bank_lookup() {
assert!(bank_code("IR062960000000100324200001").is_some());
}
}