bank 0.4.0

Various bank related traits and implementation. An abstract bank-transaction parser; easily add your own bank. SEPA RF implementation for easy formatting and checking of RF fields.
use std::str::{FromStr};
use std::fmt::{self, Display, Formatter};
use num::{BigUint, Num, One, ToPrimitive, Zero};

// We can use derive(Eq) because BigDecimal implements a decent Eq.
#[derive(Debug,Eq,PartialEq)]
pub struct RF {
    data: String,
}

impl RF {
    pub fn calculate_check(&self) -> u8 {
        // Definition: 100 * data + check = 1 mod 97
        //
        // digits <- 100 * data - 1   mod 97
        // result = 97 - digits

        let data = self.calculate_int();
        let digits = (BigUint::from(100u32) * data - BigUint::one()) % BigUint::from(97u32);
        97 - digits.to_u8().unwrap()
    }

    fn calculate_int(&self) -> BigUint {
        let mut data = BigUint::zero();

        for c in self.data.bytes() {
            match c {
                b'0'...b'9' => {
                    data = data * BigUint::from(10u32) + BigUint::from(c - b'0');
                },
                b'A'...b'Z' => {
                    data = data * BigUint::from(100u32) + BigUint::from(c - b'A' + 10);
                }
                _ => unreachable!("RF data structure contains invalid character"),
            }
        }
        data = data * BigUint::from(100u32) + BigUint::from(27u32);
        data = data * BigUint::from(100u32) + BigUint::from(15u32);
        data
    }

    /// Creates a SEPA `RF` from a seed.
    ///
    /// The seed's length needs to be at least one,
    /// and at most 21.
    ///
    /// # Example
    ///
    /// ```
    /// let rf = bank::scr::RF::new("I201709").unwrap();
    /// assert_eq!(rf.to_string(), "RF93 I20 1709");
    /// ```
    pub fn new(input: &str) -> Result<RF, &'static str> {
        let input = input.trim().replace(' ', "");
        if input.len() > 21 {
            return Err("Input too long")
        }
        if input.len() == 0 {
            return Err("Input too short")
        }

        let input = input.to_uppercase();
        let valid_data = input.chars().all(|x| (x >= 'A' && x <= 'Z') || (x >= '0' && x <= '9') );

        if !valid_data {
            return Err("Invalid input character");
        }

        let rf = RF {
            data: input,
        };

        Ok(rf)
    }
}

impl FromStr for RF {
    type Err = &'static str;
    fn from_str(input: &str) -> Result<RF, Self::Err> {
        if input.len() < 5 {
            return Err("Formatted RF should be at least 5 characters long");
        }
        if ! input.starts_with("RF") {
            return Err("Input does not start with RF");
        }

        let rf = RF::new(&input[4..])?;

        let check = input[2..4].parse().map_err(|_| "Cannot parse RF check digits.")?;

        if rf.calculate_check() != check {
            return Err("Check digits are incorrect.");
        }

        Ok(rf)
    }
}

impl Display for RF {
    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
        let formatted = format!("{}", self.data);

        // Insert spaces/make groups
        let first_group_length = formatted.len() % 4;
        let mut i = first_group_length;

        let mut new_formatted = String::with_capacity(formatted.len() + 1 + formatted.len() / 4);
        new_formatted.push_str(&formatted[0..first_group_length]);
        while i < formatted.len() {
            new_formatted.push(' ');
            new_formatted.push_str(&formatted[i..i+4]);
            i += 4;
        }
        write!(f, "RF{:02} {}", self.calculate_check(), new_formatted.trim())
    }
}

#[test]
fn test_check_digits() {
    let tests = vec![
        ("RF45G72UUR", true),
        ("RF6518K5", true),
        ("RF35C4", false),
        ("RF214377", false),
        ("RF18 5390 0754 7034", true),
    ];

    for (input, output) in tests {
        let parsed = input.parse::<RF>();
        assert_eq!(parsed.is_ok(), output, "{}", input)
    }
}

#[test]
fn test_parse_and_back() {
    let tests = vec![
        "RF74 1",
        "RF53 F43",
        "RF49 F4XX",
    ];

    for input in tests {
        let parsed = input.parse::<RF>().unwrap();
        assert_eq!(input, parsed.to_string());
    }
}