1use super::{fips, GeoidType, HasGeoidString, StateCode};
2use itertools::Itertools;
3use serde::{Deserialize, Serialize};
4use std::fmt::Display;
5
6#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
7pub enum Geoid {
8 State(fips::State),
9 County(fips::State, fips::County),
10 CountySubdivision(fips::State, fips::County, fips::CountySubdivision),
11 Place(fips::State, fips::Place),
12 CensusTract(fips::State, fips::County, fips::CensusTract),
13 BlockGroup(
14 fips::State,
15 fips::County,
16 fips::CensusTract,
17 fips::BlockGroup,
18 ),
19 Block(fips::State, fips::County, fips::CensusTract, fips::Block),
20}
21
22impl TryFrom<&str> for Geoid {
23 type Error = String;
24
25 fn try_from(value: &str) -> Result<Self, Self::Error> {
26 match value.len() {
27 2 => GeoidType::State.geoid_from_str(value),
28 5 => GeoidType::County.geoid_from_str(value),
29 7 => GeoidType::Place.geoid_from_str(value),
30 10 => GeoidType::CountySubdivision.geoid_from_str(value),
31 11 => GeoidType::CensusTract.geoid_from_str(value),
32 12 => GeoidType::BlockGroup.geoid_from_str(value),
33 x if x == 15 || x == 16 => GeoidType::Block.geoid_from_str(value),
34 x => Err(format!("unsupported GEOID type with length {x}: {value}")),
35 }
36 }
37}
38
39impl Geoid {
43 pub fn all_states() -> Vec<Geoid> {
45 StateCode::ALL
46 .iter()
47 .map(|sc| {
48 let s: fips::State = (*sc).into();
49 Geoid::State(s)
50 })
51 .collect_vec()
52 }
53
54 pub fn geoid_type(&self) -> GeoidType {
55 match self {
56 Geoid::State(_) => GeoidType::State,
57 Geoid::County(_, _) => GeoidType::County,
58 Geoid::CountySubdivision(_, _, _) => GeoidType::CountySubdivision,
59 Geoid::Place(_, _) => GeoidType::Place,
60 Geoid::CensusTract(_, _, _) => GeoidType::CensusTract,
61 Geoid::BlockGroup(_, _, _, _) => GeoidType::BlockGroup,
62 Geoid::Block(_, _, _, _) => GeoidType::Block,
63 }
64 }
65
66 pub fn variant_name(&self) -> String {
67 self.geoid_type().to_string()
68 }
69
70 pub fn truncate_geoid_to_type(&self, target: &GeoidType) -> Result<Geoid, String> {
92 fn _err(src: &GeoidType, dst: &GeoidType) -> String {
93 format!("{dst} not a parent type of {src}, cannot truncate geoid.")
94 }
95 match (self, target) {
96 (Geoid::State(_), GeoidType::State) => Ok(self.clone()),
97 (Geoid::State(_), _) => Err(_err(&self.geoid_type(), target)),
98 (Geoid::County(s, _), GeoidType::State) => Ok(Geoid::State(*s)),
99 (Geoid::County(_, _), GeoidType::County) => Ok(self.clone()),
100 (Geoid::County(_, _), _) => Err(_err(&self.geoid_type(), target)),
101 (Geoid::CountySubdivision(s, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
102 (Geoid::CountySubdivision(s, c, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
103 (Geoid::CountySubdivision(_, _, _), GeoidType::CountySubdivision) => Ok(self.clone()),
104 (Geoid::CountySubdivision(_, _, _), _) => Err(_err(&self.geoid_type(), target)),
105 (Geoid::Place(s, _), GeoidType::State) => Ok(Geoid::State(*s)),
106 (Geoid::Place(_, _), _) => Err(_err(&self.geoid_type(), target)),
107 (Geoid::CensusTract(s, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
108 (Geoid::CensusTract(s, c, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
109 (Geoid::CensusTract(_, _, _), GeoidType::CensusTract) => Ok(self.clone()),
110 (Geoid::CensusTract(_, _, _), _) => Err(_err(&self.geoid_type(), target)),
111 (Geoid::BlockGroup(s, _, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
112 (Geoid::BlockGroup(s, c, _, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
113 (Geoid::BlockGroup(s, c, t, _), GeoidType::CensusTract) => {
114 Ok(Geoid::CensusTract(*s, *c, *t))
115 }
116 (Geoid::BlockGroup(_, _, _, _), GeoidType::BlockGroup) => Ok(self.clone()),
117 (Geoid::BlockGroup(_, _, _, _), _) => Err(_err(&self.geoid_type(), target)),
118 (Geoid::Block(s, _, _, _), GeoidType::State) => Ok(Geoid::State(*s)),
119 (Geoid::Block(s, c, _, _), GeoidType::County) => Ok(Geoid::County(*s, *c)),
120 (Geoid::Block(s, c, t, _), GeoidType::CensusTract) => {
121 Ok(Geoid::CensusTract(*s, *c, *t))
122 }
123 (Geoid::Block(s, c, t, b), GeoidType::BlockGroup) => {
124 let block_str = &b.0[0..1];
127 let bg = block_str
128 .parse::<u64>()
129 .map_err(|e| format!("cannot read first digit of block as integer: {e}"))?;
130 let geoid = Geoid::BlockGroup(*s, *c, *t, fips::BlockGroup(bg));
131 Ok(geoid)
132 }
133 (Geoid::Block(_, _, _, _), GeoidType::Block) => Ok(self.clone()),
134 (Geoid::Block(_, _, _, _), _) => Err(_err(&self.geoid_type(), target)),
135 }
136 }
137
138 pub fn is_parent_of(&self, child: &Geoid) -> bool {
140 match (self, child) {
141 (Geoid::State(s1), Geoid::County(s2, _)) => s1 == s2,
142 (Geoid::State(s1), Geoid::CountySubdivision(s2, _, _)) => s1 == s2,
143 (Geoid::State(s1), Geoid::Place(s2, _)) => s1 == s2,
144 (Geoid::State(s1), Geoid::CensusTract(s2, _, _)) => s1 == s2,
145 (Geoid::State(s1), Geoid::BlockGroup(s2, _, _, _)) => s1 == s2,
146 (Geoid::State(s1), Geoid::Block(s2, _, _, _)) => s1 == s2,
147 (Geoid::County(s1, c1), Geoid::CountySubdivision(s2, c2, _)) => s1 == s2 && c1 == c2,
148 (Geoid::County(s1, c1), Geoid::CensusTract(s2, c2, _)) => s1 == s2 && c1 == c2,
149 (Geoid::County(s1, c1), Geoid::BlockGroup(s2, c2, _, _)) => s1 == s2 && c1 == c2,
150 (Geoid::County(s1, c1), Geoid::Block(s2, c2, _, _)) => s1 == s2 && c1 == c2,
151 (Geoid::CensusTract(s1, c1, t1), Geoid::BlockGroup(s2, c2, t2, _)) => {
152 s1 == s2 && c1 == c2 && t1 == t2
153 }
154 (Geoid::CensusTract(s1, c1, t1), Geoid::Block(s2, c2, t2, _)) => {
155 s1 == s2 && c1 == c2 && t1 == t2
156 }
157 _ => false,
158 }
159 }
160
161 pub fn to_parent(&self) -> Option<Geoid> {
173 match self {
174 Geoid::State(_) => None,
175 Geoid::County(s, _) => Some(Geoid::State(*s)),
176 Geoid::CountySubdivision(s, c, _) => Some(Geoid::County(*s, *c)),
177 Geoid::Place(s, _) => Some(Geoid::State(*s)),
178 Geoid::CensusTract(s, c, _) => Some(Geoid::County(*s, *c)),
179 Geoid::BlockGroup(s, c, t, _) => Some(Geoid::CensusTract(*s, *c, *t)),
180 Geoid::Block(s, c, t, _) => Some(Geoid::CensusTract(*s, *c, *t)),
181 }
182 }
183
184 pub fn to_state(&self) -> Geoid {
185 match self {
186 Geoid::State(_) => self.clone(),
187 Geoid::County(st, _) => Geoid::State(*st),
188 Geoid::CountySubdivision(st, _, _) => Geoid::State(*st),
189 Geoid::Place(st, _) => Geoid::State(*st),
190 Geoid::CensusTract(st, _, _) => Geoid::State(*st),
191 Geoid::BlockGroup(st, _, _, _) => Geoid::State(*st),
192 Geoid::Block(st, _, _, _) => Geoid::State(*st),
193 }
194 }
195
196 pub fn to_state_abbreviation(&self) -> Result<String, String> {
197 let state_fips = match self.to_state() {
198 Geoid::State(s) => Ok(s),
199 _ => Err(String::from("internal error")),
200 }?;
201 let state_code = StateCode::try_from(state_fips)?;
202 let state_str = state_code.to_state_abbreviation();
203 Ok(state_str)
204 }
205
206 pub fn to_county(&self) -> Result<Geoid, String> {
207 match self {
208 Geoid::State(_) => Err(String::from("state geoid does not contain a county geoid")),
209 Geoid::County(st, ct) => Ok(Geoid::County(*st, *ct)),
210 Geoid::CountySubdivision(st, ct, _) => Ok(Geoid::County(*st, *ct)),
211 Geoid::Place(_, _) => Err(String::from("place geoid does not contain a county geoid")),
212 Geoid::CensusTract(st, ct, _) => Ok(Geoid::County(*st, *ct)),
213 Geoid::BlockGroup(st, ct, _, _) => Ok(Geoid::County(*st, *ct)),
214 Geoid::Block(st, ct, _, _) => Ok(Geoid::County(*st, *ct)),
215 }
216 }
217
218 pub fn to_census_tract(&self) -> Result<Geoid, String> {
219 match self {
220 Geoid::State(_) => Err(String::from(
221 "state geoid does not contain a census tract geoid",
222 )),
223 Geoid::County(_, _) => Err(String::from(
224 "county geoid does not contain a census tract geoid",
225 )),
226 Geoid::CountySubdivision(_, _, _) => Err(String::from(
227 "county subdivision geoid does not contain a census tract geoid",
228 )),
229 Geoid::Place(_, _) => Err(String::from(
230 "place geoid does not contain a census tract geoid",
231 )),
232 Geoid::CensusTract(st, ct, tr) => Ok(Geoid::CensusTract(*st, *ct, *tr)),
233 Geoid::BlockGroup(st, ct, tr, _) => Ok(Geoid::CensusTract(*st, *ct, *tr)),
234 Geoid::Block(st, ct, tr, _) => Ok(Geoid::CensusTract(*st, *ct, *tr)),
235 }
236 }
237}
238
239impl HasGeoidString for Geoid {
240 fn geoid_string(&self) -> String {
241 match self {
242 Geoid::State(st) => st.geoid_string(),
243 Geoid::County(st, ct) => format!("{}{}", st.geoid_string(), ct.geoid_string()),
244 Geoid::CountySubdivision(st, ct, cs) => format!(
245 "{}{}{}",
246 st.geoid_string(),
247 ct.geoid_string(),
248 cs.geoid_string()
249 ),
250 Geoid::Place(st, pl) => format!("{}{}", st.geoid_string(), pl.geoid_string()),
251 Geoid::CensusTract(st, ct, tr) => format!(
252 "{}{}{}",
253 st.geoid_string(),
254 ct.geoid_string(),
255 tr.geoid_string()
256 ),
257 Geoid::BlockGroup(st, ct, tr, bg) => format!(
258 "{}{}{}{}",
259 st.geoid_string(),
260 ct.geoid_string(),
261 tr.geoid_string(),
262 bg.geoid_string()
263 ),
264 Geoid::Block(st, ct, tr, bl) => format!(
265 "{}{}{}{}",
266 st.geoid_string(),
267 ct.geoid_string(),
268 tr.geoid_string(),
269 bl.geoid_string()
270 ),
271 }
272 }
273}
274
275impl Display for Geoid {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 write!(f, "{}={}", self.variant_name(), self.geoid_string())
278 }
279}