Skip to main content

bamcensus_acs/model/
acs_geoid_query.rs

1use bamcensus_core::model::identifier::{fips, Geoid, GeoidType, HasGeoidString};
2use std::rc::Rc;
3
4use super::DeserializeGeoidFn;
5
6/// enumeration representing the scopes of various ACS queries.
7///
8/// when running an ACS query at a given GEOID hierarchical level, there are a set
9/// of required (aka, not `Option`al) components which can be coupled with `Option`al
10/// (wildcard) components to construct a query.
11#[derive(Debug, Clone)]
12pub enum AcsGeoidQuery {
13    State(Option<fips::State>),
14    County(Option<fips::State>, Option<fips::County>),
15    CountySubdivision(
16        fips::State,
17        Option<fips::County>,
18        Option<fips::CountySubdivision>,
19    ),
20    Place(Option<fips::State>, Option<fips::Place>),
21    CensusTract(fips::State, Option<fips::County>, Option<fips::CensusTract>),
22    BlockGroup(
23        fips::State,
24        Option<fips::County>,
25        Option<fips::CensusTract>,
26        Option<fips::BlockGroup>,
27    ),
28}
29
30impl AcsGeoidQuery {
31    /// # Examples
32    ///
33    /// when no wildcard is provided, the query is constructed for an exact location, which
34    /// should return a single result.
35    /// ```rust
36    /// use bamcensus_core::model::identifier::{fips, Geoid, GeoidType};
37    /// use bamcensus_acs::model::AcsGeoidQuery;;
38    ///
39    /// let geoid = Geoid::County(fips::State(8), fips::County(1));
40    /// let query = AcsGeoidQuery::new(Some(geoid), None).unwrap();
41    /// let key = query.to_query_key();
42    /// assert_eq!(key, String::from("&for=county:001&in=state:08"));
43    /// ```
44    ///
45    /// some combinations simply append a wildcard one level below the geoid, for example,
46    /// tacking a county=* on top of state=08.
47    /// ```rust
48    /// use bamcensus_core::model::identifier::{fips, Geoid, GeoidType};
49    /// use bamcensus_acs::model::AcsGeoidQuery;;
50    ///
51    /// let geoid = Geoid::State(fips::State(8));
52    /// let wildcard = GeoidType::County;
53    /// let query = AcsGeoidQuery::new(Some(geoid), Some(wildcard)).unwrap();
54    /// let key = query.to_query_key();
55    /// assert_eq!(key, String::from("&for=county:*&in=state:08"));
56    /// ```
57    ///
58    /// some interpolate the wildcard into a geoid in ways that do not require being
59    /// reported in the query, such as adding a wildcard to a tract query. in this case,
60    /// the query simply drops the county wildcard, as it is implied.
61    /// ```rust
62    /// use bamcensus_core::model::identifier::{fips, Geoid, GeoidType};
63    /// use bamcensus_acs::model::AcsGeoidQuery;;
64    ///
65    /// let geoid = Geoid::CensusTract(fips::State(8), fips::County(1), fips::CensusTract(1));
66    /// let wildcard = GeoidType::County;
67    /// let query = AcsGeoidQuery::new(Some(geoid), Some(wildcard)).unwrap();
68    /// let key = query.to_query_key();
69    /// assert_eq!(key, String::from("&for=tract:000001&in=state:08"));
70    /// ```
71    ///
72    /// # Returns
73    ///
74    /// URL query string for calls to the US Census ACS API "for" section, which set the
75    /// spatial scope and granularity of the query result.
76    pub fn new(geoid: Option<Geoid>, wildcard: Option<GeoidType>) -> Result<AcsGeoidQuery, String> {
77        use Geoid as G;
78        use GeoidType as GT;
79
80        match (geoid, wildcard) {
81            // ~~ errors ~~
82            // - invalid combinations of geoid/wildcard values
83            (None, None) => Err(String::from(
84                "cannot create query without at least a geoid or wildcard",
85            )),
86            (None, Some(GT::CountySubdivision)) => Err(String::from(
87                "cannot create county subdivision query without State Geoid",
88            )),
89            (None, Some(GT::CensusTract)) => Err(String::from(
90                "cannot create census tract query without State Geoid",
91            )),
92            (None, Some(GT::BlockGroup)) => Err(String::from(
93                "cannot create block group query without State + County Geoids",
94            )),
95            (_, Some(GT::Block)) => Err(String::from("acs does not support block-level queries")),
96            (Some(G::Block(_, _, _, _)), _) => {
97                Err(String::from("acs does not support block-level queries"))
98            }
99
100            (Some(Geoid::State(_)), Some(GT::BlockGroup)) => Err(String::from(
101                "cannot create block group query without County Geoid",
102            )),
103            (Some(Geoid::County(_, _)), Some(GT::Place)) => Err(String::from(
104                "cannot append a 'Place' wildcard to a County Geoid",
105            )),
106            (Some(G::CountySubdivision(_, _, _)), Some(GT::Place)) => Err(String::from(
107                "cannot append a 'Place' wildcard to a CountySubdivision Geoid",
108            )),
109            (Some(G::CountySubdivision(_, _, _)), Some(GT::CensusTract)) => Err(String::from(
110                "cannot append a 'CensusTract' wildcard to a CountySubdivision Geoid",
111            )),
112            (Some(G::CountySubdivision(_, _, _)), Some(GT::BlockGroup)) => Err(String::from(
113                "cannot append a 'BlockGroup' wildcard to a CountySubdivision Geoid",
114            )),
115            (Some(Geoid::Place(_, _)), Some(GT::County)) => Err(String::from(
116                "cannot append a 'County' wildcard to a Place Geoid",
117            )),
118            (Some(Geoid::Place(_, _)), Some(GT::CountySubdivision)) => Err(String::from(
119                "cannot append a 'CountySubdivision' wildcard to a Place Geoid",
120            )),
121            (Some(Geoid::Place(_, _)), Some(GT::CensusTract)) => Err(String::from(
122                "cannot append a 'CensusTract' wildcard to a Place Geoid",
123            )),
124            (Some(Geoid::Place(_, _)), Some(GT::BlockGroup)) => Err(String::from(
125                "cannot append a 'BlockGroup' wildcard to a Place Geoid",
126            )),
127            (Some(Geoid::CensusTract(_, _, _)), Some(GT::State)) => Err(String::from(
128                "cannot append a 'State' wildcard to a CensusTract Geoid",
129            )),
130            (Some(Geoid::CensusTract(_, _, _)), Some(GT::CountySubdivision)) => Err(String::from(
131                "cannot append a 'CountySubdivision' wildcard to a CensusTract Geoid",
132            )),
133            (Some(Geoid::CensusTract(_, _, _)), Some(GT::Place)) => Err(String::from(
134                "cannot append a 'Place' wildcard to a CensusTract Geoid",
135            )),
136            (Some(Geoid::BlockGroup(_, _, _, _)), Some(GT::State)) => Err(String::from(
137                "cannot append a 'State' wildcard to a BlockGroup Geoid",
138            )),
139            (Some(Geoid::BlockGroup(_, _, _, _)), Some(GT::CountySubdivision)) => Err(
140                String::from("cannot append a 'CountySubdivision' wildcard to a BlockGroup Geoid"),
141            ),
142            (Some(Geoid::BlockGroup(_, _, _, _)), Some(GT::Place)) => Err(String::from(
143                "cannot append a 'Place' wildcard to a BlockGroup Geoid",
144            )),
145
146            // ~~ wildcard-only queries for different GEOID levels ~~
147            (None, Some(GT::State)) => Ok(AcsGeoidQuery::State(None)),
148            (None, Some(GT::County)) => Ok(AcsGeoidQuery::County(None, None)),
149            (None, Some(GT::Place)) => Ok(AcsGeoidQuery::Place(None, None)),
150
151            // ~~ queries for wildcards inserted into specific geoids ~~
152            // - STATE -
153            (Some(Geoid::State(_)), Some(GT::State)) => Ok(AcsGeoidQuery::State(None)),
154            (Some(Geoid::State(s)), Some(GT::County)) => Ok(AcsGeoidQuery::County(Some(s), None)),
155            (Some(Geoid::State(s)), Some(GT::CountySubdivision)) => {
156                Ok(AcsGeoidQuery::CountySubdivision(s, None, None))
157            }
158            (Some(Geoid::State(s)), Some(GT::Place)) => Ok(AcsGeoidQuery::Place(Some(s), None)),
159            (Some(Geoid::State(s)), Some(GT::CensusTract)) => {
160                Ok(AcsGeoidQuery::CensusTract(s, None, None))
161            }
162
163            // - COUNTY -
164            (Some(Geoid::County(_, c)), Some(GT::State)) => {
165                Ok(AcsGeoidQuery::County(None, Some(c)))
166            }
167            (Some(Geoid::County(s, _)), Some(GT::County)) => {
168                Ok(AcsGeoidQuery::County(Some(s), None))
169            }
170            (Some(Geoid::County(s, c)), Some(GT::CountySubdivision)) => {
171                Ok(AcsGeoidQuery::CountySubdivision(s, Some(c), None))
172            }
173            (Some(Geoid::County(s, c)), Some(GT::CensusTract)) => {
174                Ok(AcsGeoidQuery::CensusTract(s, Some(c), None))
175            }
176            (Some(Geoid::County(s, c)), Some(GT::BlockGroup)) => {
177                Ok(AcsGeoidQuery::BlockGroup(s, Some(c), None, None))
178            }
179
180            // - COUNTY SUBDIVISION -
181            (Some(G::CountySubdivision(st, ct, cs)), Some(GT::State)) => {
182                Ok(AcsGeoidQuery::CountySubdivision(st, Some(ct), Some(cs)))
183            }
184            (Some(G::CountySubdivision(s, _, cs)), Some(GT::County)) => {
185                Ok(AcsGeoidQuery::CountySubdivision(s, None, Some(cs)))
186            }
187            (Some(G::CountySubdivision(s, ct, _)), Some(GT::CountySubdivision)) => {
188                Ok(AcsGeoidQuery::CountySubdivision(s, Some(ct), None))
189            }
190
191            // - PLACE -
192            (Some(Geoid::Place(_, p)), Some(GT::State)) => Ok(AcsGeoidQuery::Place(None, Some(p))),
193            (Some(Geoid::Place(s, _)), Some(GT::Place)) => Ok(AcsGeoidQuery::Place(Some(s), None)),
194
195            // - CENSUS TRACT -
196            (Some(Geoid::CensusTract(s, _, t)), Some(GT::County)) => {
197                Ok(AcsGeoidQuery::CensusTract(s, None, Some(t)))
198            }
199            (Some(Geoid::CensusTract(s, c, _)), Some(GT::CensusTract)) => {
200                Ok(AcsGeoidQuery::CensusTract(s, Some(c), None))
201            }
202            (Some(Geoid::CensusTract(s, c, t)), Some(GT::BlockGroup)) => {
203                Ok(AcsGeoidQuery::BlockGroup(s, Some(c), Some(t), None))
204            }
205
206            // - BLOCK GROUP -
207            (Some(Geoid::BlockGroup(s, _, t, b)), Some(GT::County)) => {
208                Ok(AcsGeoidQuery::BlockGroup(s, None, Some(t), Some(b)))
209            }
210            (Some(Geoid::BlockGroup(s, c, _, b)), Some(GT::CensusTract)) => {
211                Ok(AcsGeoidQuery::BlockGroup(s, Some(c), None, Some(b)))
212            }
213            (Some(Geoid::BlockGroup(s, c, t, _)), Some(GT::BlockGroup)) => {
214                Ok(AcsGeoidQuery::BlockGroup(s, Some(c), Some(t), None))
215            }
216
217            // ~~ queries for specific geoids (no wildcards) ~~
218            (Some(Geoid::State(s)), None) => Ok(AcsGeoidQuery::State(Some(s))),
219            (Some(Geoid::County(s, c)), None) => Ok(AcsGeoidQuery::County(Some(s), Some(c))),
220            (Some(Geoid::CountySubdivision(s, ct, cs)), None) => {
221                Ok(AcsGeoidQuery::CountySubdivision(s, Some(ct), Some(cs)))
222            }
223            (Some(Geoid::Place(s, p)), None) => Ok(AcsGeoidQuery::Place(Some(s), Some(p))),
224            (Some(Geoid::CensusTract(s, c, t)), None) => {
225                Ok(AcsGeoidQuery::CensusTract(s, Some(c), Some(t)))
226            }
227            (Some(Geoid::BlockGroup(s, c, t, b)), None) => {
228                Ok(AcsGeoidQuery::BlockGroup(s, Some(c), Some(t), Some(b)))
229            }
230        }
231    }
232
233    /// a query key for a unique data row in the census API. depending on the AcsGeoidQuery
234    /// and the presence/absence of FIPS values, wildcards ("*") will be inserted at any level.
235    pub fn to_query_key(&self) -> String {
236        use AcsGeoidQuery as G;
237        match self {
238            G::State(state) => match state {
239                None => String::from("&for=state:*"),
240                Some(s) => format!("&for=state:{}", s.geoid_string()),
241            },
242            G::County(state, county) => match (state, county) {
243                (None, None) => String::from("&for=county:*"),
244                (None, Some(c)) => format!("&for=county:{}", c.geoid_string()),
245                (Some(s), None) => format!("&for=county:*&in=state:{}", s.geoid_string()),
246                (Some(s), Some(c)) => format!(
247                    "&for=county:{}&in=state:{}",
248                    c.geoid_string(),
249                    s.geoid_string()
250                ),
251            },
252            G::CountySubdivision(state, county, cousub) => match (county, cousub) {
253                (None, None) => format!(
254                    "&for=county%20subdivision:*&in=state:{}&in=county:*",
255                    state.geoid_string(),
256                ),
257                (None, Some(cs)) => format!(
258                    "&for=county%20subdivision:{}&in=state:{}&in=county:*",
259                    cs.geoid_string(),
260                    state.geoid_string(),
261                ),
262                (Some(co), None) => format!(
263                    "&for=county%20subdivision:*&in=state:{}&in=county:{}",
264                    state.geoid_string(),
265                    co.geoid_string(),
266                ),
267                (Some(co), Some(cs)) => format!(
268                    "&for=county%20subdivision:{}&in=state:{}&in=county:{}",
269                    cs.geoid_string(),
270                    state.geoid_string(),
271                    co.geoid_string()
272                ),
273            },
274            G::Place(state, place) => match (state, place) {
275                (None, None) => String::from("&for=place:*"),
276                (None, Some(pl)) => format!("&for=place:{}&in=state:*", pl.geoid_string()),
277                (Some(st), None) => format!("&for=place:*&in=state:{}", st.geoid_string()),
278                (Some(st), Some(pl)) => format!(
279                    "&for=place:{}&in=state:{}",
280                    pl.geoid_string(),
281                    st.geoid_string()
282                ),
283            },
284            G::CensusTract(state, county, tract) => match (county, tract) {
285                (None, None) => format!("&for=tract:*&in=state:{}", state.geoid_string()),
286                (None, Some(tr)) => format!(
287                    "&for=tract:{}&in=state:{}",
288                    tr.geoid_string(),
289                    state.geoid_string()
290                ),
291                (Some(co), None) => format!(
292                    "&for=tract:*&in=state:{}&in=county:{}",
293                    state.geoid_string(),
294                    co.geoid_string()
295                ),
296                (Some(co), Some(tr)) => format!(
297                    "&for=tract:{}&in=state:{}&in=county:{}",
298                    tr.geoid_string(),
299                    state.geoid_string(),
300                    co.geoid_string()
301                ),
302            },
303            G::BlockGroup(state, county, tract, block_group) => {
304                match (county, tract, block_group) {
305                    (None, None, None) => format!(
306                        "&for=block%20group:*&in=state:{}&in=county:*&in=tract:*",
307                        state.geoid_string()
308                    ),
309                    (None, None, Some(b)) => format!(
310                        "&for=block%20group:{}&in=state:{}&in=county:*&in=tract:*",
311                        b.geoid_string(),
312                        state.geoid_string()
313                    ),
314                    (None, Some(t), None) => format!(
315                        "&for=block%20group:*&in=state:{}&in=county:*&in=tract:{}",
316                        state.geoid_string(),
317                        t.geoid_string(),
318                    ),
319                    (None, Some(t), Some(b)) => format!(
320                        "&for=block%20group:{}&in=state:{}&in=county:*&in=tract:{}",
321                        b.geoid_string(),
322                        state.geoid_string(),
323                        t.geoid_string(),
324                    ),
325                    (Some(c), None, None) => format!(
326                        "&for=block%20group:*&in=state:{}&in=county:{}&in=tract:*",
327                        state.geoid_string(),
328                        c.geoid_string()
329                    ),
330                    (Some(c), None, Some(b)) => format!(
331                        "&for=block%20group:{}&in=state:{}&in=county:{}&in=tract:*",
332                        b.geoid_string(),
333                        state.geoid_string(),
334                        c.geoid_string(),
335                    ),
336                    (Some(c), Some(t), None) => format!(
337                        "&for=block%20group:*&in=state:{}&in=county:{}&in=tract:{}",
338                        state.geoid_string(),
339                        c.geoid_string(),
340                        t.geoid_string(),
341                    ),
342                    (Some(c), Some(t), Some(b)) => format!(
343                        "&for=block%20group:{}&in=state:{}&in=county:{}&in=tract:{}",
344                        b.geoid_string(),
345                        state.geoid_string(),
346                        c.geoid_string(),
347                        t.geoid_string()
348                    ),
349                }
350            }
351        }
352    }
353
354    pub fn response_geoid_type(&self) -> GeoidType {
355        use AcsGeoidQuery as G;
356        match self {
357            G::State(_) => GeoidType::State,
358            G::County(_, _) => GeoidType::County,
359            G::CountySubdivision(_, _, _) => GeoidType::CountySubdivision,
360            G::Place(_, _) => GeoidType::Place,
361            G::CensusTract(_, _, _) => GeoidType::CensusTract,
362            G::BlockGroup(_, _, _, _) => GeoidType::BlockGroup,
363        }
364    }
365
366    pub fn response_column_names(&self) -> Vec<String> {
367        use AcsGeoidQuery as G;
368        match self {
369            G::State(_) => vec![String::from("state")],
370            G::County(_, _) => vec![String::from("state"), String::from("county")],
371            G::CountySubdivision(_, _, _) => vec![
372                String::from("state"),
373                String::from("county"),
374                String::from("county subdivision"),
375            ],
376            G::Place(_, _) => vec![String::from("state"), String::from("place")],
377            G::CensusTract(_, _, _) => vec![
378                String::from("state"),
379                String::from("county"),
380                String::from("tract"),
381            ],
382            G::BlockGroup(_, _, _, _) => vec![
383                String::from("state"),
384                String::from("county"),
385                String::from("tract"),
386                String::from("block group"),
387            ],
388        }
389    }
390
391    pub fn response_column_count(&self) -> usize {
392        match self {
393            AcsGeoidQuery::State(_) => 1,
394            AcsGeoidQuery::County(_, _) => 2,
395            AcsGeoidQuery::CountySubdivision(_, _, _) => 3,
396            AcsGeoidQuery::Place(_, _) => 2,
397            AcsGeoidQuery::CensusTract(_, _, _) => 3,
398            AcsGeoidQuery::BlockGroup(_, _, _, _) => 4,
399        }
400    }
401
402    pub fn get_geoid_type(&self) -> GeoidType {
403        match self {
404            AcsGeoidQuery::State(_) => GeoidType::State,
405            AcsGeoidQuery::County(_, _) => GeoidType::County,
406            AcsGeoidQuery::CountySubdivision(_, _, _) => GeoidType::CountySubdivision,
407            AcsGeoidQuery::Place(_, _) => GeoidType::Place,
408            AcsGeoidQuery::CensusTract(_, _, _) => GeoidType::CensusTract,
409            AcsGeoidQuery::BlockGroup(_, _, _, _) => GeoidType::BlockGroup,
410        }
411    }
412
413    /// builds a function that unpacks the ACS query values representing a geoid.
414    /// these return as values in an array of different lengths, depending on the scope of
415    /// the original query.
416    pub fn build_deserialize_geoid_fn(&self) -> DeserializeGeoidFn {
417        let geoid_type = self.get_geoid_type();
418        let f: DeserializeGeoidFn = Rc::new(move |vals| {
419            let strings = as_strings(&vals)?;
420            geoid_type.geoid_from_slice_of_strings(&strings)
421        });
422        f
423    }
424}
425
426/// helper function to convert a vec of JSON values to their expected String values.
427fn as_strings(arr: &[serde_json::Value]) -> Result<Vec<String>, String> {
428    arr.iter()
429        .map(|v| {
430            v.as_str()
431                .ok_or_else(|| format!("raw geoid value should be string, found {v}"))
432                .map(String::from)
433        })
434        .collect::<Result<Vec<_>, String>>()
435}