use super::{fips, GeoidType, HasGeoidString, StateCode};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Geoid {
State(fips::State),
County(fips::State, fips::County),
CountySubdivision(fips::State, fips::County, fips::CountySubdivision),
Place(fips::State, fips::Place),
CensusTract(fips::State, fips::County, fips::CensusTract),
BlockGroup(
fips::State,
fips::County,
fips::CensusTract,
fips::BlockGroup,
),
Block(fips::State, fips::County, fips::CensusTract, fips::Block),
}
impl TryFrom<&str> for Geoid {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.len() {
2 => GeoidType::State.geoid_from_str(value),
5 => GeoidType::County.geoid_from_str(value),
7 => GeoidType::Place.geoid_from_str(value),
10 => GeoidType::CountySubdivision.geoid_from_str(value),
11 => GeoidType::CensusTract.geoid_from_str(value),
12 => GeoidType::BlockGroup.geoid_from_str(value),
x if x == 15 || x == 16 => GeoidType::Block.geoid_from_str(value),
x => Err(format!("unsupported GEOID type with length {x}: {value}")),
}
}
}
impl Geoid {
pub fn all_states() -> Vec<Geoid> {
StateCode::ALL
.iter()
.map(|sc| {
let s: fips::State = (*sc).into();
Geoid::State(s)
})
.collect_vec()
}
pub fn geoid_type(&self) -> GeoidType {
match self {
Geoid::State(_) => GeoidType::State,
Geoid::County(_, _) => GeoidType::County,
Geoid::CountySubdivision(_, _, _) => GeoidType::CountySubdivision,
Geoid::Place(_, _) => GeoidType::Place,
Geoid::CensusTract(_, _, _) => GeoidType::CensusTract,
Geoid::BlockGroup(_, _, _, _) => GeoidType::BlockGroup,
Geoid::Block(_, _, _, _) => GeoidType::Block,
}
}
pub fn variant_name(&self) -> String {
self.geoid_type().to_string()
}
pub fn truncate_geoid_to_type(&self, target: &GeoidType) -> Result<Geoid, String> {
fn _err(src: &GeoidType, dst: &GeoidType) -> String {
format!("{dst} not a parent type of {src}, cannot truncate geoid.")
}
match (self, target) {
(Geoid::State(_), GeoidType::State) => Ok(self.clone()),
(Geoid::State(_), _) => Err(_err(&self.geoid_type(), target)),
(Geoid::County(s, _), GeoidType::State) => Ok(Geoid::State(*s)),
(Geoid::County(_, _), GeoidType::County) => Ok(self.clone()),
(Geoid::County(_, _), _) => Err(_err(&self.geoid_type(), target)),
(Geoid::CountySubdivision(s, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
(Geoid::CountySubdivision(s, c, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
(Geoid::CountySubdivision(_, _, _), GeoidType::CountySubdivision) => Ok(self.clone()),
(Geoid::CountySubdivision(_, _, _), _) => Err(_err(&self.geoid_type(), target)),
(Geoid::Place(s, _), GeoidType::State) => Ok(Geoid::State(*s)),
(Geoid::Place(_, _), _) => Err(_err(&self.geoid_type(), target)),
(Geoid::CensusTract(s, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
(Geoid::CensusTract(s, c, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
(Geoid::CensusTract(_, _, _), GeoidType::CensusTract) => Ok(self.clone()),
(Geoid::CensusTract(_, _, _), _) => Err(_err(&self.geoid_type(), target)),
(Geoid::BlockGroup(s, _, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
(Geoid::BlockGroup(s, c, _, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
(Geoid::BlockGroup(s, c, t, _), GeoidType::CensusTract) => {
Ok(Geoid::CensusTract(*s, *c, *t))
}
(Geoid::BlockGroup(_, _, _, _), GeoidType::BlockGroup) => Ok(self.clone()),
(Geoid::BlockGroup(_, _, _, _), _) => Err(_err(&self.geoid_type(), target)),
(Geoid::Block(s, _, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
(Geoid::Block(s, c, _, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
(Geoid::Block(s, c, t, _), GeoidType::CensusTract) => {
Ok(Geoid::CensusTract(*s, *c, *t))
}
(Geoid::Block(s, c, t, b), GeoidType::BlockGroup) => {
let block_str = &b.0[0..1];
let bg = block_str
.parse::<u64>()
.map_err(|e| format!("cannot read first digit of block as integer: {e}"))?;
let geoid = Geoid::BlockGroup(*s, *c, *t, fips::BlockGroup(bg));
Ok(geoid)
}
(Geoid::Block(_, _, _, _), GeoidType::Block) => Ok(self.clone()),
(Geoid::Block(_, _, _, _), _) => Err(_err(&self.geoid_type(), target)),
}
}
pub fn is_parent_of(&self, child: &Geoid) -> bool {
match (self, child) {
(Geoid::State(s1), Geoid::County(s2, _)) => s1 == s2,
(Geoid::State(s1), Geoid::CountySubdivision(s2, _, _)) => s1 == s2,
(Geoid::State(s1), Geoid::Place(s2, _)) => s1 == s2,
(Geoid::State(s1), Geoid::CensusTract(s2, _, _)) => s1 == s2,
(Geoid::State(s1), Geoid::BlockGroup(s2, _, _, _)) => s1 == s2,
(Geoid::State(s1), Geoid::Block(s2, _, _, _)) => s1 == s2,
(Geoid::County(s1, c1), Geoid::CountySubdivision(s2, c2, _)) => s1 == s2 && c1 == c2,
(Geoid::County(s1, c1), Geoid::CensusTract(s2, c2, _)) => s1 == s2 && c1 == c2,
(Geoid::County(s1, c1), Geoid::BlockGroup(s2, c2, _, _)) => s1 == s2 && c1 == c2,
(Geoid::County(s1, c1), Geoid::Block(s2, c2, _, _)) => s1 == s2 && c1 == c2,
(Geoid::CensusTract(s1, c1, t1), Geoid::BlockGroup(s2, c2, t2, _)) => {
s1 == s2 && c1 == c2 && t1 == t2
}
(Geoid::CensusTract(s1, c1, t1), Geoid::Block(s2, c2, t2, _)) => {
s1 == s2 && c1 == c2 && t1 == t2
}
_ => false,
}
}
pub fn to_parent(&self) -> Option<Geoid> {
match self {
Geoid::State(_) => None,
Geoid::County(s, _) => Some(Geoid::State(*s)),
Geoid::CountySubdivision(s, c, _) => Some(Geoid::County(*s, *c)),
Geoid::Place(s, _) => Some(Geoid::State(*s)),
Geoid::CensusTract(s, c, _) => Some(Geoid::County(*s, *c)),
Geoid::BlockGroup(s, c, t, _) => Some(Geoid::CensusTract(*s, *c, *t)),
Geoid::Block(s, c, t, _) => Some(Geoid::CensusTract(*s, *c, *t)),
}
}
pub fn to_state(&self) -> Geoid {
match self {
Geoid::State(_) => self.clone(),
Geoid::County(st, _) => Geoid::State(*st),
Geoid::CountySubdivision(st, _, _) => Geoid::State(*st),
Geoid::Place(st, _) => Geoid::State(*st),
Geoid::CensusTract(st, _, _) => Geoid::State(*st),
Geoid::BlockGroup(st, _, _, _) => Geoid::State(*st),
Geoid::Block(st, _, _, _) => Geoid::State(*st),
}
}
pub fn to_state_abbreviation(&self) -> Result<String, String> {
let state_fips = match self.to_state() {
Geoid::State(s) => Ok(s),
_ => Err(String::from("internal error")),
}?;
let state_code = StateCode::try_from(state_fips)?;
let state_str = state_code.to_state_abbreviation();
Ok(state_str)
}
pub fn to_county(&self) -> Result<Geoid, String> {
match self {
Geoid::State(_) => Err(String::from("state geoid does not contain a county geoid")),
Geoid::County(st, ct) => Ok(Geoid::County(*st, *ct)),
Geoid::CountySubdivision(st, ct, _) => Ok(Geoid::County(*st, *ct)),
Geoid::Place(_, _) => Err(String::from("place geoid does not contain a county geoid")),
Geoid::CensusTract(st, ct, _) => Ok(Geoid::County(*st, *ct)),
Geoid::BlockGroup(st, ct, _, _) => Ok(Geoid::County(*st, *ct)),
Geoid::Block(st, ct, _, _) => Ok(Geoid::County(*st, *ct)),
}
}
pub fn to_census_tract(&self) -> Result<Geoid, String> {
match self {
Geoid::State(_) => Err(String::from(
"state geoid does not contain a census tract geoid",
)),
Geoid::County(_, _) => Err(String::from(
"county geoid does not contain a census tract geoid",
)),
Geoid::CountySubdivision(_, _, _) => Err(String::from(
"county subdivision geoid does not contain a census tract geoid",
)),
Geoid::Place(_, _) => Err(String::from(
"place geoid does not contain a census tract geoid",
)),
Geoid::CensusTract(st, ct, tr) => Ok(Geoid::CensusTract(*st, *ct, *tr)),
Geoid::BlockGroup(st, ct, tr, _) => Ok(Geoid::CensusTract(*st, *ct, *tr)),
Geoid::Block(st, ct, tr, _) => Ok(Geoid::CensusTract(*st, *ct, *tr)),
}
}
}
impl HasGeoidString for Geoid {
fn geoid_string(&self) -> String {
match self {
Geoid::State(st) => st.geoid_string(),
Geoid::County(st, ct) => format!("{}{}", st.geoid_string(), ct.geoid_string()),
Geoid::CountySubdivision(st, ct, cs) => format!(
"{}{}{}",
st.geoid_string(),
ct.geoid_string(),
cs.geoid_string()
),
Geoid::Place(st, pl) => format!("{}{}", st.geoid_string(), pl.geoid_string()),
Geoid::CensusTract(st, ct, tr) => format!(
"{}{}{}",
st.geoid_string(),
ct.geoid_string(),
tr.geoid_string()
),
Geoid::BlockGroup(st, ct, tr, bg) => format!(
"{}{}{}{}",
st.geoid_string(),
ct.geoid_string(),
tr.geoid_string(),
bg.geoid_string()
),
Geoid::Block(st, ct, tr, bl) => format!(
"{}{}{}{}",
st.geoid_string(),
ct.geoid_string(),
tr.geoid_string(),
bl.geoid_string()
),
}
}
}
impl Display for Geoid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}={}", self.variant_name(), self.geoid_string())
}
}