Skip to main content

bamcensus_core/model/identifier/
geoid.rs

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
39// todo:
40// - Geoid methods to unpack/pack between types (Geoid::County.to_state())
41
42impl Geoid {
43    /// generates all state level Geoids for the U.S.
44    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    /// manipulates this GEOID via truncation to transform it's GEOID type.
71    ///
72    /// GEOID is a hierarchical numeric identifier. we can truncate the values
73    /// in order to reach a higher/larger geographic representation. this method
74    /// supports that operation, where this Geoid instance will be truncated to
75    /// transform it into some other GeoidType.
76    ///
77    /// # Examples
78    ///
79    /// converts GEOID 08059009838 (TRACT) to 08059 (COUNTY).
80    ///
81    /// ```rust
82    /// use bamcensus_core::model::identifier::{Geoid, GeoidType, fips};
83    /// let geoid = Geoid::CensusTract(
84    ///     fips::State(8),         // 08     Colorado
85    ///     fips::County(59),       // 059    Jefferson
86    ///     fips::CensusTract(9838) // 009838
87    /// );
88    /// let result = geoid.truncate_geoid_to_type(&GeoidType::County).unwrap();
89    /// assert_eq!(result, Geoid::County(fips::State(8), fips::County(59)))
90    /// ```
91    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                // special edge case of truncation, since we have no other operations for
125                // converting between Block and Block Group.
126                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    /// predicate to filter by hierarchical geoshed.
139    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    /// manipulates this GEOID via truncation to transform it's GEOID type to that
162    /// of it's parent.
163    ///
164    /// the base case is `None`, which is the parent of `State`, and signifies "no restriction"
165    /// in census queries. for all other GeoidTypes, we simply remove the lowest area type.
166    ///
167    /// # Note
168    ///
169    /// Geoid::Block.to_parent() produces a CensusTract, not a BlockGroup, based on
170    /// <https://www.census.gov/programs-surveys/geography/guidance/geo-identifiers.html>,
171    /// which does not imply that all block groups are the first digit of all blocks.
172    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}