1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#[cfg(test)]
extern crate spectral;
#[cfg(test)]
mod tests;

mod countries;

extern crate regex;
#[macro_use]
extern crate lazy_static;

use regex::Regex;
use countries::COUNTRY_FORMATS;

/// Validate an IBAN number. The validation will detect the following mistakes:
/// <ul>
///     <li>The length is four or less, or longer than 34.</li>
///     <li>The number contains characters other than A-Z or 0-9</li>
///     <li>A-Z is in place of 0-9 or vice versa</li>
///     <li>The checksum is invalid</li>
/// </ul>
/// If none of these apply, the function will return true, otherwise it will return false.
/// Note that this function will not check the country format. To validate the country code and
/// the BBAN format, you should also use `validate_iban_country()`.
///
/// # Examples
/// ```rust
/// use iban::validate_iban;
///
/// // A valid address
/// assert_eq!(validate_iban("DE44500105175407324931"), true);
///
/// // An invalid address
/// assert_eq!(validate_iban("DE4450010234607324931"), false);
/// ```
pub fn validate_iban<S: Into<String>>(address: S) -> bool {

    let address_string = address.into();

    return
        // Check the characters
        validate_characters(&address_string)
        // Check the checksum
        && compute_checksum(&address_string) == 1;
}

/// Checks whether all characters in this address are valid. Returns a true if all characters are
/// valid, false otherwise.
fn validate_characters(address: &String) -> bool {
    lazy_static! {
        static ref RE: Regex = Regex::new(r"^[A-Z]{2}\d{2}[A-Z\d]{1,30}$").unwrap();
    }
    RE.is_match(address)
}

/// This function computes the checksum of an address. The function assumes the string only
/// contains 0-9 and A-Z.
///
/// # Panics
/// If the address contains any characters other than 0-9 or A-Z, this function will panic.
fn compute_checksum(address: &String) -> u8 {
    let mut digits = Vec::new();

    // Move the first four characters to the back
    let (start, end) = address.split_at(4);
    let mut changed_order = String::new();
    changed_order.push_str(end);
    changed_order.push_str(start);

    // Convert the characters to digits
    for c in changed_order.chars() {
        match c {
            d @ '0'...'9' => digits.push(d.to_digit(10).unwrap()),
            a @ 'A'...'Z' => {
                let number = a.to_digit(36).unwrap();
                digits.push(number / 10);
                digits.push(number % 10);
            }
            _ => panic!("Invalid character in address"),
        }
    }

    // Validate the checksum
    digits.iter().fold(0, |acc, d| (acc * 10 + d) % 97) as u8
}

/// The three possible results of `validate_iban_country()`.
#[derive(PartialEq, Eq, Debug)]
pub enum IbanCountryResult {
    /// The country was recognized and the code was valid
    Valid,
    /// The country was recognized and didn't fit the format
    Invalid,
    /// The country was not recognized
    CountryUnknown,
}

/// Validate the BBAN part of an IBAN account number. This function will return one of three
/// results:
/// <ul>
///     <li>If the country code is recognized and the address fits the country's format, it will
///         return `IbanCountryResult::Valid`.</li>
///     <li>If the country code is recognized and the address does not fit the country BBAN format,
///         it will return `IbanCountryResult::Invalid`.</li>
///     <li>If the country code is not recognized, it will return
///         `IbanCountryResult::CountryUnknown`.</li>
/// </ul>
/// Note that this check is not a substitute for `validate_iban()` or vice versa. This function
/// only checks the address country code and corresponding format. To verify whether the address
/// fits the IBAN specification, you should also call `validate_iban()`.
///
/// # Examples
/// ```rust
/// use iban::validate_iban_country;
/// use iban::IbanCountryResult;
///
/// // A valid address format
/// assert_eq!(validate_iban_country("DE44500105175407324931"), IbanCountryResult::Valid);
///
/// // An invalid format
/// assert_eq!(validate_iban_country("DE44ABCDE5175407324931"), IbanCountryResult::Invalid);
///
/// // An unknown country
/// assert_eq!(validate_iban_country("ZZ44500105175407324931"), IbanCountryResult::CountryUnknown);
/// ```
pub fn validate_iban_country<S: Into<String>>(address: S) -> IbanCountryResult {
    let address_string = address.into();
    let (country_code_address, address_remainder) = address_string.split_at(2);

    for &(country_code_pattern, country_regex) in COUNTRY_FORMATS.into_iter() {
        if country_code_pattern == country_code_address {
            // The country code matches
            let regex = Regex::new(country_regex).unwrap();
            return match regex.is_match(address_remainder) {
                       true => IbanCountryResult::Valid,
                       false => IbanCountryResult::Invalid,
                   };
        }
    }
    IbanCountryResult::CountryUnknown
}