Skip to main content

use_routing_number/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common ABA routing number primitives.
8pub mod prelude {
9    pub use crate::{RoutingNumber, RoutingNumberError};
10}
11
12/// A validated 9-digit ABA routing number.
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct RoutingNumber(String);
15
16impl RoutingNumber {
17    /// Creates a routing number after shape and checksum validation.
18    ///
19    /// # Errors
20    ///
21    /// Returns [`RoutingNumberError::InvalidLength`] when the trimmed input is not nine bytes,
22    /// [`RoutingNumberError::NotDigits`] when any byte is not a digit, and
23    /// [`RoutingNumberError::InvalidChecksum`] when the ABA checksum fails.
24    pub fn new(value: impl AsRef<str>) -> Result<Self, RoutingNumberError> {
25        let value = value.as_ref().trim();
26        if value.len() != 9 {
27            return Err(RoutingNumberError::InvalidLength);
28        }
29
30        if !value.bytes().all(|byte| byte.is_ascii_digit()) {
31            return Err(RoutingNumberError::NotDigits);
32        }
33
34        if !has_valid_checksum(value) {
35            return Err(RoutingNumberError::InvalidChecksum);
36        }
37
38        Ok(Self(value.to_string()))
39    }
40
41    /// Returns the validated routing number.
42    #[must_use]
43    pub fn as_str(&self) -> &str {
44        &self.0
45    }
46
47    /// Consumes the routing number and returns its owned string.
48    #[must_use]
49    pub fn into_string(self) -> String {
50        self.0
51    }
52}
53
54impl AsRef<str> for RoutingNumber {
55    fn as_ref(&self) -> &str {
56        self.as_str()
57    }
58}
59
60impl fmt::Display for RoutingNumber {
61    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62        formatter.write_str(self.as_str())
63    }
64}
65
66impl FromStr for RoutingNumber {
67    type Err = RoutingNumberError;
68
69    fn from_str(value: &str) -> Result<Self, Self::Err> {
70        Self::new(value)
71    }
72}
73
74impl TryFrom<&str> for RoutingNumber {
75    type Error = RoutingNumberError;
76
77    fn try_from(value: &str) -> Result<Self, Self::Error> {
78        Self::new(value)
79    }
80}
81
82/// Errors returned while constructing routing numbers.
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum RoutingNumberError {
85    /// ABA routing numbers must be exactly nine digits.
86    InvalidLength,
87    /// ABA routing numbers must contain only digits.
88    NotDigits,
89    /// The ABA routing checksum failed.
90    InvalidChecksum,
91}
92
93impl fmt::Display for RoutingNumberError {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Self::InvalidLength => formatter.write_str("routing number must be exactly 9 digits"),
97            Self::NotDigits => formatter.write_str("routing number must contain only digits"),
98            Self::InvalidChecksum => formatter.write_str("routing number checksum is invalid"),
99        }
100    }
101}
102
103impl Error for RoutingNumberError {}
104
105fn has_valid_checksum(value: &str) -> bool {
106    let mut digits = [0_u32; 9];
107    for (index, byte) in value.bytes().enumerate() {
108        digits[index] = u32::from(byte - b'0');
109    }
110
111    let checksum = 3 * (digits[0] + digits[3] + digits[6])
112        + 7 * (digits[1] + digits[4] + digits[7])
113        + digits[2]
114        + digits[5]
115        + digits[8];
116
117    checksum % 10 == 0
118}
119
120#[cfg(test)]
121mod tests {
122    use super::{RoutingNumber, RoutingNumberError};
123
124    #[test]
125    fn accepts_valid_routing_numbers() -> Result<(), RoutingNumberError> {
126        for value in ["021000021", "011000015", "121000248"] {
127            let routing = RoutingNumber::new(value)?;
128            assert_eq!(routing.as_str(), value);
129        }
130        Ok(())
131    }
132
133    #[test]
134    fn rejects_bad_checksum() {
135        assert_eq!(
136            RoutingNumber::new("021000022"),
137            Err(RoutingNumberError::InvalidChecksum)
138        );
139    }
140
141    #[test]
142    fn rejects_non_digits_and_bad_lengths() {
143        assert_eq!(
144            RoutingNumber::new("02100002"),
145            Err(RoutingNumberError::InvalidLength)
146        );
147        assert_eq!(
148            RoutingNumber::new("02100002A"),
149            Err(RoutingNumberError::NotDigits)
150        );
151    }
152}