use core::fmt;
#[cfg(feature = "serde")]
use serde::Serialize;
pub const NEAREST_MAX_LIMIT: usize = 20;
pub const SEARCH_MAX_LIMIT: usize = 100;
pub const CODE_PREFIX_MAX_LIMIT: usize = 1000;
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[non_exhaustive]
pub struct DataInfo {
pub source: String,
pub decree: String,
pub village_count: u32,
pub build_date: u64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[non_exhaustive]
pub struct Village {
pub code: String,
pub name: String,
pub district: String,
pub city: String,
pub province: String,
pub lat: f64,
pub lon: f64,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub dist_km: Option<f64>,
}
impl fmt::Display for Village {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} — {}, {}, {} ({})",
self.name, self.district, self.city, self.province, self.code
)
}
}
#[allow(clippy::too_many_arguments)]
impl Village {
pub fn new(
code: String,
name: String,
district: String,
city: String,
province: String,
lat: f64,
lon: f64,
) -> Self {
Self {
code,
name,
district,
city,
province,
lat,
lon,
dist_km: None,
}
}
pub fn with_dist_km(mut self, dist_km: f64) -> Self {
self.dist_km = Some(dist_km);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum LocateMethod {
Nearest,
Contained,
}
impl fmt::Display for LocateMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LocateMethod::Nearest => write!(f, "nearest"),
LocateMethod::Contained => write!(f, "contained"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct AdminLevel {
pub code: String,
pub name: String,
}
impl fmt::Display for AdminLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.code, self.name)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[non_exhaustive]
pub struct Location {
pub province: AdminLevel,
pub city: AdminLevel,
pub district: AdminLevel,
pub village: String,
pub village_code: String,
pub lat: f64,
pub lon: f64,
pub dist_km: f64,
pub method: LocateMethod,
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.province)?;
writeln!(f, " {}", self.city)?;
writeln!(f, " {}", self.district)?;
writeln!(
f,
" {} {} ({:.1} km, {})",
self.village_code, self.village, self.dist_km, self.method
)
}
}
#[allow(clippy::too_many_arguments)]
impl Location {
pub fn new(
province: AdminLevel,
city: AdminLevel,
district: AdminLevel,
village: String,
village_code: String,
lat: f64,
lon: f64,
dist_km: f64,
method: LocateMethod,
) -> Self {
Self {
province,
city,
district,
village,
village_code,
lat,
lon,
dist_km,
method,
}
}
}
pub fn location_from_village(v: &Village, dist_km: f64, method: LocateMethod) -> Option<Location> {
let parts: Vec<&str> = v.code.split('.').collect();
if parts.len() != 4 {
return None;
}
Some(Location {
province: AdminLevel {
code: parts[0].to_string(),
name: v.province.clone(),
},
city: AdminLevel {
code: format!("{}.{}", parts[0], parts[1]),
name: v.city.clone(),
},
district: AdminLevel {
code: format!("{}.{}.{}", parts[0], parts[1], parts[2]),
name: v.district.clone(),
},
village: v.name.clone(),
village_code: v.code.clone(),
lat: v.lat,
lon: v.lon,
dist_km,
method,
})
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[non_exhaustive]
pub enum LookupResult {
Found(Village),
Ambiguous(Vec<Village>),
NotFound,
}
impl fmt::Display for LookupResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LookupResult::Found(v) => write!(f, "{}", v),
LookupResult::Ambiguous(list) => {
writeln!(f, "Found {} matching villages:", list.len())?;
for (i, v) in list.iter().enumerate() {
writeln!(f, " {}. {}", i + 1, v)?;
}
write!(
f,
"Use a more specific query (e.g., include city or province)"
)
}
LookupResult::NotFound => write!(f, "No matching village found"),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[non_exhaustive]
pub struct PrefixResult {
pub villages: Vec<Village>,
pub total: usize,
pub has_more: bool,
}
impl fmt::Display for PrefixResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} result(s), total: {}, has_more: {}",
self.villages.len(),
self.total,
self.has_more,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_locate_method_display() {
assert_eq!(format!("{}", LocateMethod::Nearest), "nearest");
assert_eq!(format!("{}", LocateMethod::Contained), "contained");
}
#[test]
fn test_location_from_village() {
let v = Village {
code: "31.71.03.1001".into(),
name: "Kemayoran".into(),
district: "Kemayoran".into(),
city: "Jakarta Pusat".into(),
province: "DKI Jakarta".into(),
lat: -6.1647,
lon: 106.8453,
dist_km: None,
};
let loc =
location_from_village(&v, 1.5, LocateMethod::Nearest).expect("should parse valid code");
assert_eq!(loc.province.code, "31");
assert_eq!(loc.city.code, "31.71");
assert_eq!(loc.district.code, "31.71.03");
assert_eq!(loc.village_code, "31.71.03.1001");
assert_eq!(loc.dist_km, 1.5);
assert_eq!(loc.method, LocateMethod::Nearest);
}
#[test]
fn test_location_from_village_bad_code() {
let v = Village {
code: "invalid".into(),
name: "Test".into(),
district: "Test".into(),
city: "Test".into(),
province: "Test".into(),
lat: 0.0,
lon: 0.0,
dist_km: None,
};
assert!(location_from_village(&v, 0.0, LocateMethod::Nearest).is_none());
}
#[test]
fn test_location_from_village_three_parts() {
let v = Village {
code: "31.71.03".into(),
name: "Test".into(),
district: "Test".into(),
city: "Test".into(),
province: "Test".into(),
lat: 0.0,
lon: 0.0,
dist_km: None,
};
assert!(
location_from_village(&v, 0.0, LocateMethod::Nearest).is_none(),
"3-part code should return None"
);
}
#[test]
fn test_location_from_village_five_parts() {
let v = Village {
code: "31.71.03.1001.5".into(),
name: "Test".into(),
district: "Test".into(),
city: "Test".into(),
province: "Test".into(),
lat: 0.0,
lon: 0.0,
dist_km: None,
};
assert!(
location_from_village(&v, 0.0, LocateMethod::Nearest).is_none(),
"5-part code should return None"
);
}
}