1use std::io::prelude::*;
9use std::sync::LazyLock;
10
11use bzip2::read::BzDecoder;
12use serde::{Deserialize, Serialize};
13
14const ZIPCODE_LENGTH: usize = 5;
15
16static ZIPCODE_BYTES_BZIP: &[u8] = include_bytes!("zips.json.bz2");
17
18static ZIPCODES: LazyLock<Vec<Zipcode>> = LazyLock::new(|| {
19 let mut decompressor = BzDecoder::new(ZIPCODE_BYTES_BZIP);
20 let mut zipcode_json = String::new();
21 decompressor
22 .read_to_string(&mut zipcode_json)
23 .expect("failed to decompress embedded zipcode database");
24 serde_json::from_str::<Vec<Zipcode>>(&zipcode_json)
25 .unwrap_or_else(|e| panic!("failed to deserialize zipcode database: {}", e))
26});
27
28#[derive(thiserror::Error, Debug)]
30pub enum Error {
31 #[error("Invalid format, zipcode must be of the format: \"#####\" or \"#####-####\"")]
32 InvalidFormat,
33 #[error("Invalid characters, zipcode may only contain digits and \"-\".")]
34 InvalidCharacters,
35}
36
37pub type Result<T> = std::result::Result<T, Error>;
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
42pub struct Zipcode {
43 pub acceptable_cities: Vec<String>,
44 pub active: bool,
45 pub area_codes: Vec<String>,
46 pub city: String,
47 pub country: String,
48 pub county: String,
49 pub lat: String,
50 pub long: String,
51 pub state: String,
52 pub timezone: String,
53 pub unacceptable_cities: Vec<String>,
54 pub world_region: String,
55 pub zip_code: String,
56 pub zip_code_type: String,
57}
58
59impl Zipcode {
60 pub fn field_matches(&self, field: &str, value: &serde_json::Value) -> bool {
64 use serde_json::Value;
65 fn eq_list(items: &[String], value: &Value) -> bool {
66 match value {
67 Value::Array(arr) => {
68 arr.len() == items.len()
69 && arr.iter().zip(items).all(|(v, s)| v.as_str() == Some(s))
70 }
71 _ => false,
72 }
73 }
74 match field {
75 "zip_code" => value.as_str() == Some(&self.zip_code),
76 "zip_code_type" => value.as_str() == Some(&self.zip_code_type),
77 "active" => value.as_bool() == Some(self.active),
78 "city" => value.as_str() == Some(&self.city),
79 "acceptable_cities" => eq_list(&self.acceptable_cities, value),
80 "unacceptable_cities" => eq_list(&self.unacceptable_cities, value),
81 "state" => value.as_str() == Some(&self.state),
82 "county" => value.as_str() == Some(&self.county),
83 "timezone" => value.as_str() == Some(&self.timezone),
84 "area_codes" => eq_list(&self.area_codes, value),
85 "world_region" => value.as_str() == Some(&self.world_region),
86 "country" => value.as_str() == Some(&self.country),
87 "lat" => value.as_str() == Some(&self.lat),
88 "long" => value.as_str() == Some(&self.long),
89 _ => false,
90 }
91 }
92}
93
94pub fn matching(zipcode: &str, zipcodes: Option<Vec<Zipcode>>) -> Result<Vec<Zipcode>> {
97 let zipcode = clean_zipcode(zipcode)?;
98 let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
99 Ok(zipcodes
100 .iter()
101 .filter(|z| z.zip_code == zipcode)
102 .cloned()
103 .collect())
104}
105
106pub fn is_real(zipcode: &str) -> Result<bool> {
108 Ok(!matching(zipcode, None)?.is_empty())
109}
110
111pub fn similar_to(prefix: &str, zipcodes: Option<Vec<Zipcode>>) -> Vec<Zipcode> {
113 let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
114 zipcodes
115 .iter()
116 .filter(|z| z.zip_code.starts_with(prefix))
117 .cloned()
118 .collect()
119}
120
121pub fn contains(fragment: &str, zipcodes: Option<Vec<Zipcode>>) -> Vec<Zipcode> {
123 let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
124 zipcodes
125 .iter()
126 .filter(|z| z.zip_code.contains(fragment))
127 .cloned()
128 .collect()
129}
130
131pub fn filter_by<F>(filters: Vec<F>, zipcodes: Option<Vec<Zipcode>>) -> Result<Vec<Zipcode>>
136where
137 F: Fn(&Zipcode) -> bool,
138{
139 let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
140 Ok(zipcodes
141 .iter()
142 .filter(|z| filters.iter().all(|f| f(z)))
143 .cloned()
144 .collect())
145}
146
147pub fn filter_by_fields(
152 filters: &[(String, serde_json::Value)],
153 zipcodes: Option<Vec<Zipcode>>,
154) -> Vec<Zipcode> {
155 let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
156 zipcodes
157 .iter()
158 .filter(|z| {
159 filters
160 .iter()
161 .all(|(field, value)| z.field_matches(field, value))
162 })
163 .cloned()
164 .collect()
165}
166
167pub fn haversine(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
170 let (lon1, lat1, lon2, lat2) = (
171 lon1.to_radians(),
172 lat1.to_radians(),
173 lon2.to_radians(),
174 lat2.to_radians(),
175 );
176 let dlon = lon2 - lon1;
177 let dlat = lat2 - lat1;
178 let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
179 let c = 2.0 * a.sqrt().asin();
180 let r = 3956.0; c * r
182}
183
184pub fn filter_by_coordinates(
188 lat: f64,
189 long: f64,
190 radius_in_miles: f64,
191 zipcodes: Option<Vec<Zipcode>>,
192) -> Vec<Zipcode> {
193 let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
194 zipcodes
195 .iter()
196 .filter(|z| match (z.lat.parse::<f64>(), z.long.parse::<f64>()) {
197 (Ok(z_lat), Ok(z_long)) => haversine(z_long, z_lat, long, lat) <= radius_in_miles,
198 _ => false,
199 })
200 .cloned()
201 .collect()
202}
203
204pub fn list_all() -> Vec<Zipcode> {
206 ZIPCODES.clone()
207}
208
209pub fn database() -> &'static [Zipcode] {
211 &ZIPCODES
212}
213
214fn clean_zipcode(zipcode: &str) -> Result<&str> {
215 let zipcode = zipcode.trim();
216 if zipcode.len() < ZIPCODE_LENGTH {
217 return Err(Error::InvalidFormat);
218 }
219 let prefix = &zipcode[..ZIPCODE_LENGTH];
220 if !prefix.chars().all(|c| c.is_ascii_digit()) {
221 return Err(Error::InvalidCharacters);
222 }
223 Ok(prefix)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use serde_json::json;
230
231 #[test]
232 fn should_find_real_zipcodes() {
233 assert!(is_real("06903").unwrap());
234 assert!(is_real("06905").unwrap());
235 }
236
237 #[test]
238 fn should_include_zips_from_issue_23() {
239 assert!(is_real("85144").unwrap());
241 }
242
243 #[test]
244 fn database_invariants() {
245 let db = database();
246 assert!(
247 db.len() > 40_000 && db.len() < 50_000,
248 "unexpected database size: {}",
249 db.len()
250 );
251 let zips: Vec<&str> = db.iter().map(|z| z.zip_code.as_str()).collect();
252 let mut sorted = zips.clone();
253 sorted.sort_unstable();
254 assert_eq!(zips, sorted, "database must be sorted by zip_code");
255 let unique: std::collections::HashSet<_> = zips.iter().collect();
256 assert_eq!(
257 unique.len(),
258 zips.len(),
259 "database must not contain duplicate zip codes"
260 );
261 }
262
263 #[test]
264 fn should_not_find_fake_zipcodes() {
265 assert!(!is_real("91239").unwrap());
266 }
267
268 #[test]
269 fn should_return_no_zipcodes() {
270 for zc in &["00000", "00000-0000", "00000 0000"] {
271 assert!(matching(zc, None).unwrap().is_empty())
272 }
273 }
274
275 #[test]
276 fn should_match_zip_plus_four_to_base_zipcode() {
277 assert_eq!(matching("06903-1234", None).unwrap().len(), 1);
278 }
279
280 #[test]
281 fn should_fail_to_find_zipcodes_not_included_in_overrides() {
282 let zc = "06903";
283 matching(zc, None).unwrap();
284 assert!(matching(zc, Some(matching("06904", None).unwrap()))
285 .unwrap()
286 .is_empty());
287 }
288
289 #[test]
290 fn should_reject_invalid_zipcodes() {
291 assert!(matches!(matching("123", None), Err(Error::InvalidFormat)));
292 assert!(matches!(
293 matching("1234a", None),
294 Err(Error::InvalidCharacters)
295 ));
296 }
297
298 #[test]
299 fn should_include_county_field() {
300 let zips = matching("06475", None).unwrap();
301 assert_eq!(zips[0].county, "Middlesex County");
302 assert_eq!(zips[0].city, "Old Saybrook");
303 }
304
305 #[test]
306 fn should_find_similar_zipcodes_by_prefix() {
307 let zips = similar_to("1018", None);
308 assert_eq!(
309 zips.iter().map(|z| z.zip_code.as_str()).collect::<Vec<_>>(),
310 vec!["10184", "10185"]
311 );
312 }
313
314 #[test]
315 fn should_find_zipcodes_containing_fragment() {
316 assert!(contains("0185", None).iter().any(|z| z.zip_code == "10185"));
317 }
318
319 #[test]
320 fn should_filter_by_fields() {
321 let filters = vec![
322 ("active".to_string(), json!(true)),
323 ("city".to_string(), json!("Windsor")),
324 ];
325 let windsor = filter_by_fields(&filters, None);
326 assert!(!windsor.is_empty());
327 assert!(windsor.iter().all(|z| z.active && z.city == "Windsor"));
328 assert_eq!(similar_to("2", Some(windsor)).len(), 3);
329 }
330
331 #[test]
332 fn should_not_match_unknown_filter_fields() {
333 let filters = vec![("nonexistent".to_string(), json!("x"))];
334 assert!(filter_by_fields(&filters, None).is_empty());
335 }
336
337 #[test]
338 fn should_filter_by_coordinates() {
339 let nearby = filter_by_coordinates(41.3015, -72.3879, 1.0, None);
341 assert!(nearby.iter().any(|z| z.zip_code == "06475"));
342 assert!(filter_by_coordinates(42.2529, 71.0023, 100.0, None).is_empty());
344 }
345
346 #[test]
347 fn haversine_known_distance() {
348 let d = haversine(-74.0060, 40.7128, -118.2437, 34.0522);
350 assert!((d - 2445.0).abs() < 15.0, "distance was {}", d);
351 }
352}