1use crate::HscodeError::{HsChapterError, HsCodeLenError, InputError};
18use std::fmt;
19use std::io::Read;
20use std::str::FromStr;
21use thiserror::Error;
22
23include!(concat!(env!("OUT_DIR"), "/hs_data.rs"));
24
25#[derive(Error, Debug)]
27pub enum HscodeError {
28 #[error("Invalid input format: non-digit character")]
30 InputError,
31 #[error("Invalid HS code length: expected 6-14 even digits, got {0}")]
33 HsCodeLenError(usize),
34 #[error("Chapter out of range: expected 1-97, got {0}")]
36 HsChapterError(u8),
37}
38
39#[derive(PartialOrd, PartialEq, Debug)]
50pub struct HsCode(Vec<u8>);
51
52impl fmt::Display for HsCode {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 let s: String = self.0.iter().map(|&d| (d + b'0') as char).collect();
55 write!(f, "{}", s)
56 }
57}
58
59impl FromStr for HsCode {
69 type Err = HscodeError;
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 Self::try_new_from_str(s)
72 }
73}
74
75pub fn lookup(code: &str) -> Option<&'static str> {
86 let key = if code.len() >= 6 { &code[..6] } else { code };
87 HS_MAP.get(key).copied()
88}
89
90impl HsCode {
91 pub fn new_from_str(input: &str) -> Self {
97 Self::try_new_from_str(input).unwrap()
98 }
99
100 pub fn get_chapter(&self) -> u8 {
109 self.0[0] * 10 + self.0[1]
110 }
111
112 pub fn try_new_from_str(input: &str) -> Result<Self, HscodeError> {
117 verify_and_trans_hs_code(input).map(HsCode)
118 }
119
120 pub fn diff(&self, other: &HsCode) -> Vec<usize> {
131 self.0
132 .iter()
133 .zip(other.0.iter())
134 .enumerate()
135 .filter_map(|(idx, (x, y))| if x != y { Some(idx) } else { None })
136 .collect()
137 }
138
139 pub fn description(&self) -> Option<&'static str> {
145 let key: String = self.0.iter().take(6).map(|&d| (d + b'0') as char).collect();
146 HS_MAP.get(&key).copied()
147 }
148
149 pub fn len(&self) -> usize {
151 self.0.len()
152 }
153
154 pub fn is_empty(&self) -> bool {
156 false
157 }
158
159 pub fn is_six_digit(&self) -> bool {
161 self.len() == 6
162 }
163
164 pub fn is_ten_digit(&self) -> bool {
166 self.len() == 10
167 }
168
169 pub fn iter(&self) -> std::slice::Iter<'_, u8> {
171 self.0.iter()
172 }
173
174 pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
176 self.0.iter().map(|x| (x + b'0') as char)
177 }
178}
179
180pub(crate) fn verify_and_trans_hs_code(input: &str) -> Result<Vec<u8>, HscodeError> {
191 let bytes = input.as_bytes();
192
193 if bytes.len() < 6 || !bytes.len().is_multiple_of(2) || bytes.len() >= 16 {
194 return Err(HsCodeLenError(bytes.len()));
195 }
196 if !bytes.iter().all(|b| b.is_ascii_digit()) {
197 return Err(InputError);
198 }
199
200 let chap: Vec<_> = bytes.iter().map(|b| b - b'0').collect();
201 let chapter = chap[0] * 10 + chap[1];
202 if !(1..=97).contains(&chapter) {
203 return Err(HsChapterError(chapter));
204 }
205
206 Ok(chap)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_valid_8_digit_hscode() {
215 let code = HsCode::new_from_str("01012900");
216 assert_eq!(code.get_chapter(), 1);
217 assert_eq!(code.to_string(), "01012900");
218 }
219
220 #[test]
221 fn test_valid_10_digit_hscode() {
222 let code = HsCode::new_from_str("0101290010");
223 assert_eq!(code.get_chapter(), 1);
224 assert_eq!(code.to_string(), "0101290010");
225 }
226
227 #[test]
228 fn test_valid_12_digit_hscode() {
229 let code = HsCode::new_from_str("010121001012");
230 assert_eq!(code.get_chapter(), 1);
231 }
232
233 #[test]
234 fn test_invalid_length() {
235 let result = HsCode::try_new_from_str("123");
236 assert!(matches!(result, Err(HsCodeLenError(3))));
237 }
238
239 #[test]
240 fn test_invalid_odd_length() {
241 let result = HsCode::try_new_from_str("0101211");
242 assert!(matches!(result, Err(HsCodeLenError(7))));
243 }
244
245 #[test]
246 fn test_invalid_too_long() {
247 let result = HsCode::try_new_from_str("0101210010121416");
248 assert!(matches!(result, Err(HsCodeLenError(16))));
249 }
250
251 #[test]
252 fn test_non_digit_input() {
253 let result = HsCode::try_new_from_str("ABCDEFGH");
254 assert!(matches!(result, Err(InputError)));
255 }
256
257 #[test]
258 fn test_invalid_chapter() {
259 let result = HsCode::try_new_from_str("9912345678");
260 assert!(matches!(result, Err(HsChapterError(99))));
261 }
262
263 #[test]
264 fn test_chapter_boundary() {
265 let result = HsCode::try_new_from_str("01012900");
266 assert!(result.is_ok());
267
268 let result = HsCode::try_new_from_str("97012900");
269 assert!(result.is_ok());
270
271 let result = HsCode::try_new_from_str("98012900");
272 assert!(matches!(result, Err(HsChapterError(98))));
273 }
274
275 #[test]
276 fn test_diff() {
277 let code1 = HsCode::try_new_from_str("01012900").unwrap();
278 let code2 = HsCode::try_new_from_str("01012800").unwrap();
279 assert_eq!(code1.diff(&code2), vec![5]);
280 }
281
282 #[test]
283 fn test_fromstr() {
284 let code = "10011001".parse::<HsCode>().unwrap();
285 assert_eq!(code, HsCode::try_new_from_str("10011001").unwrap())
286 }
287
288 #[test]
289 fn test_descrip() {
290 let code = HsCode::new_from_str("01012100");
291 assert!(code.description().is_some());
292 assert_eq!(code.description().unwrap(), "Horses; live");
293 }
294}