use_routing_number/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
9 pub use crate::{RoutingNumber, RoutingNumberError};
10}
11
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct RoutingNumber(String);
15
16impl RoutingNumber {
17 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 #[must_use]
43 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46
47 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum RoutingNumberError {
85 InvalidLength,
87 NotDigits,
89 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}