use std::io::prelude::*;
use std::sync::LazyLock;
use bzip2::read::BzDecoder;
use serde::{Deserialize, Serialize};
const ZIPCODE_LENGTH: usize = 5;
static ZIPCODE_BYTES_BZIP: &[u8] = include_bytes!("zips.json.bz2");
static ZIPCODES: LazyLock<Vec<Zipcode>> = LazyLock::new(|| {
let mut decompressor = BzDecoder::new(ZIPCODE_BYTES_BZIP);
let mut zipcode_json = String::new();
decompressor
.read_to_string(&mut zipcode_json)
.expect("failed to decompress embedded zipcode database");
serde_json::from_str::<Vec<Zipcode>>(&zipcode_json)
.unwrap_or_else(|e| panic!("failed to deserialize zipcode database: {}", e))
});
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid format, zipcode must be of the format: \"#####\" or \"#####-####\"")]
InvalidFormat,
#[error("Invalid characters, zipcode may only contain digits and \"-\".")]
InvalidCharacters,
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Zipcode {
pub acceptable_cities: Vec<String>,
pub active: bool,
pub area_codes: Vec<String>,
pub city: String,
pub country: String,
pub county: String,
pub lat: String,
pub long: String,
pub state: String,
pub timezone: String,
pub unacceptable_cities: Vec<String>,
pub world_region: String,
pub zip_code: String,
pub zip_code_type: String,
}
impl Zipcode {
pub fn field_matches(&self, field: &str, value: &serde_json::Value) -> bool {
use serde_json::Value;
fn eq_list(items: &[String], value: &Value) -> bool {
match value {
Value::Array(arr) => {
arr.len() == items.len()
&& arr.iter().zip(items).all(|(v, s)| v.as_str() == Some(s))
}
_ => false,
}
}
match field {
"zip_code" => value.as_str() == Some(&self.zip_code),
"zip_code_type" => value.as_str() == Some(&self.zip_code_type),
"active" => value.as_bool() == Some(self.active),
"city" => value.as_str() == Some(&self.city),
"acceptable_cities" => eq_list(&self.acceptable_cities, value),
"unacceptable_cities" => eq_list(&self.unacceptable_cities, value),
"state" => value.as_str() == Some(&self.state),
"county" => value.as_str() == Some(&self.county),
"timezone" => value.as_str() == Some(&self.timezone),
"area_codes" => eq_list(&self.area_codes, value),
"world_region" => value.as_str() == Some(&self.world_region),
"country" => value.as_str() == Some(&self.country),
"lat" => value.as_str() == Some(&self.lat),
"long" => value.as_str() == Some(&self.long),
_ => false,
}
}
}
pub fn matching(zipcode: &str, zipcodes: Option<Vec<Zipcode>>) -> Result<Vec<Zipcode>> {
let zipcode = clean_zipcode(zipcode)?;
let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
Ok(zipcodes
.iter()
.filter(|z| z.zip_code == zipcode)
.cloned()
.collect())
}
pub fn is_real(zipcode: &str) -> Result<bool> {
Ok(!matching(zipcode, None)?.is_empty())
}
pub fn similar_to(prefix: &str, zipcodes: Option<Vec<Zipcode>>) -> Vec<Zipcode> {
let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
zipcodes
.iter()
.filter(|z| z.zip_code.starts_with(prefix))
.cloned()
.collect()
}
pub fn contains(fragment: &str, zipcodes: Option<Vec<Zipcode>>) -> Vec<Zipcode> {
let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
zipcodes
.iter()
.filter(|z| z.zip_code.contains(fragment))
.cloned()
.collect()
}
pub fn filter_by<F>(filters: Vec<F>, zipcodes: Option<Vec<Zipcode>>) -> Result<Vec<Zipcode>>
where
F: Fn(&Zipcode) -> bool,
{
let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
Ok(zipcodes
.iter()
.filter(|z| filters.iter().all(|f| f(z)))
.cloned()
.collect())
}
pub fn filter_by_fields(
filters: &[(String, serde_json::Value)],
zipcodes: Option<Vec<Zipcode>>,
) -> Vec<Zipcode> {
let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
zipcodes
.iter()
.filter(|z| {
filters
.iter()
.all(|(field, value)| z.field_matches(field, value))
})
.cloned()
.collect()
}
pub fn haversine(lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> f64 {
let (lon1, lat1, lon2, lat2) = (
lon1.to_radians(),
lat1.to_radians(),
lon2.to_radians(),
lat2.to_radians(),
);
let dlon = lon2 - lon1;
let dlat = lat2 - lat1;
let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
let r = 3956.0; c * r
}
pub fn filter_by_coordinates(
lat: f64,
long: f64,
radius_in_miles: f64,
zipcodes: Option<Vec<Zipcode>>,
) -> Vec<Zipcode> {
let zipcodes = zipcodes.as_deref().unwrap_or(&ZIPCODES);
zipcodes
.iter()
.filter(|z| match (z.lat.parse::<f64>(), z.long.parse::<f64>()) {
(Ok(z_lat), Ok(z_long)) => haversine(z_long, z_lat, long, lat) <= radius_in_miles,
_ => false,
})
.cloned()
.collect()
}
pub fn list_all() -> Vec<Zipcode> {
ZIPCODES.clone()
}
pub fn database() -> &'static [Zipcode] {
&ZIPCODES
}
fn clean_zipcode(zipcode: &str) -> Result<&str> {
let zipcode = zipcode.trim();
if zipcode.len() < ZIPCODE_LENGTH {
return Err(Error::InvalidFormat);
}
let prefix = &zipcode[..ZIPCODE_LENGTH];
if !prefix.chars().all(|c| c.is_ascii_digit()) {
return Err(Error::InvalidCharacters);
}
Ok(prefix)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn should_find_real_zipcodes() {
assert!(is_real("06903").unwrap());
assert!(is_real("06905").unwrap());
}
#[test]
fn should_include_zips_from_issue_23() {
assert!(is_real("85144").unwrap());
}
#[test]
fn database_invariants() {
let db = database();
assert!(
db.len() > 40_000 && db.len() < 50_000,
"unexpected database size: {}",
db.len()
);
let zips: Vec<&str> = db.iter().map(|z| z.zip_code.as_str()).collect();
let mut sorted = zips.clone();
sorted.sort_unstable();
assert_eq!(zips, sorted, "database must be sorted by zip_code");
let unique: std::collections::HashSet<_> = zips.iter().collect();
assert_eq!(
unique.len(),
zips.len(),
"database must not contain duplicate zip codes"
);
}
#[test]
fn should_not_find_fake_zipcodes() {
assert!(!is_real("91239").unwrap());
}
#[test]
fn should_return_no_zipcodes() {
for zc in &["00000", "00000-0000", "00000 0000"] {
assert!(matching(zc, None).unwrap().is_empty())
}
}
#[test]
fn should_match_zip_plus_four_to_base_zipcode() {
assert_eq!(matching("06903-1234", None).unwrap().len(), 1);
}
#[test]
fn should_fail_to_find_zipcodes_not_included_in_overrides() {
let zc = "06903";
matching(zc, None).unwrap();
assert!(matching(zc, Some(matching("06904", None).unwrap()))
.unwrap()
.is_empty());
}
#[test]
fn should_reject_invalid_zipcodes() {
assert!(matches!(matching("123", None), Err(Error::InvalidFormat)));
assert!(matches!(
matching("1234a", None),
Err(Error::InvalidCharacters)
));
}
#[test]
fn should_include_county_field() {
let zips = matching("06475", None).unwrap();
assert_eq!(zips[0].county, "Middlesex County");
assert_eq!(zips[0].city, "Old Saybrook");
}
#[test]
fn should_find_similar_zipcodes_by_prefix() {
let zips = similar_to("1018", None);
assert_eq!(
zips.iter().map(|z| z.zip_code.as_str()).collect::<Vec<_>>(),
vec!["10184", "10185"]
);
}
#[test]
fn should_find_zipcodes_containing_fragment() {
assert!(contains("0185", None).iter().any(|z| z.zip_code == "10185"));
}
#[test]
fn should_filter_by_fields() {
let filters = vec![
("active".to_string(), json!(true)),
("city".to_string(), json!("Windsor")),
];
let windsor = filter_by_fields(&filters, None);
assert!(!windsor.is_empty());
assert!(windsor.iter().all(|z| z.active && z.city == "Windsor"));
assert_eq!(similar_to("2", Some(windsor)).len(), 3);
}
#[test]
fn should_not_match_unknown_filter_fields() {
let filters = vec![("nonexistent".to_string(), json!("x"))];
assert!(filter_by_fields(&filters, None).is_empty());
}
#[test]
fn should_filter_by_coordinates() {
let nearby = filter_by_coordinates(41.3015, -72.3879, 1.0, None);
assert!(nearby.iter().any(|z| z.zip_code == "06475"));
assert!(filter_by_coordinates(42.2529, 71.0023, 100.0, None).is_empty());
}
#[test]
fn haversine_known_distance() {
let d = haversine(-74.0060, 40.7128, -118.2437, 34.0522);
assert!((d - 2445.0).abs() < 15.0, "distance was {}", d);
}
}