Skip to main content

bamcensus_lehd/model/
lodes_dataset.rs

1use super::{LodesEdition, LodesJobType, OdPart, WorkplaceSegment, BASE_URL, LATEST_YEAR};
2use bamcensus_core::model::identifier::{Geoid, GeoidType, StateCode};
3use serde::{Deserialize, Serialize};
4use std::fmt::Display;
5
6#[derive(Deserialize, Serialize, Clone, Copy, Debug)]
7#[serde(rename_all = "snake_case", tag = "type")]
8pub enum LodesDataset {
9    OD {
10        edition: LodesEdition,
11        job_type: LodesJobType,
12        od_part: OdPart,
13        year: u64,
14    },
15    RAC {
16        edition: LodesEdition,
17        job_type: LodesJobType,
18        segment: WorkplaceSegment,
19        year: u64,
20    },
21    WAC {
22        edition: LodesEdition,
23        job_type: LodesJobType,
24        segment: WorkplaceSegment,
25        year: u64,
26    },
27}
28
29impl Default for LodesDataset {
30    fn default() -> Self {
31        let year = LATEST_YEAR;
32        Self::WAC {
33            edition: LodesEdition::default(),
34            job_type: LodesJobType::default(),
35            segment: WorkplaceSegment::default(),
36            year,
37        }
38    }
39}
40
41impl Display for LodesDataset {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{self:?}")
44    }
45}
46
47impl LodesDataset {
48    pub fn description(&self) -> String {
49        match self {
50            LodesDataset::OD {
51                edition,
52                job_type,
53                od_part,
54                year,
55            } => {
56                format!("{year} {edition} {od_part} Origin-Destination data, {job_type} job totals are associated with both a home Census Block and a work Census Block")
57            }
58            LodesDataset::RAC {
59                edition: _,
60                job_type: _,
61                segment: _,
62                year: _,
63            } => String::from(
64                "Residence Area Characteristic data, jobs are totaled by home Census Block",
65            ),
66            LodesDataset::WAC {
67                edition,
68                job_type,
69                segment,
70                year,
71            } => format!(
72                "{year} {edition} {segment} Workplace Area Characteristic data, {job_type} jobs are totaled by work Census Block"
73            ),
74        }
75    }
76
77    pub fn dataset_directory(&self) -> String {
78        match self {
79            LodesDataset::OD {
80                edition: _,
81                job_type: _,
82                od_part: _,
83                year: _,
84            } => String::from("od"),
85            LodesDataset::RAC {
86                edition: _,
87                job_type: _,
88                segment: _,
89                year: _,
90            } => String::from("rac"),
91            LodesDataset::WAC {
92                edition: _,
93                job_type: _,
94                segment: _,
95                year: _,
96            } => String::from("wac"),
97        }
98    }
99
100    /// creates a URI to a LODES datasets based on the directory and file
101    /// naming conventions described in the LODESTechDoc8.1.pdf file.
102    /// see <https://lehd.ces.census.gov/data/lodes/LODES8/LODESTechDoc8.1.pdf>
103    pub fn create_uri(&self, geoid: &Geoid) -> Result<String, String> {
104        let sc: StateCode = geoid.to_state().try_into()?;
105        let state_code = sc.to_state_abbreviation();
106        match self {
107            LodesDataset::OD {
108                edition,
109                job_type,
110                od_part,
111                year,
112            } => {
113                validate_availability(*year, &sc)?;
114                let filename = format!(
115                    "{}_od_{}_{}_{}.csv.gz",
116                    state_code.to_lowercase(),
117                    od_part,
118                    job_type,
119                    year
120                );
121                let uri = format!(
122                    "{}/{}/{}/{}/{}",
123                    BASE_URL,
124                    edition,
125                    state_code.to_lowercase(),
126                    self.dataset_directory(),
127                    filename
128                );
129                Ok(uri)
130            }
131            LodesDataset::RAC {
132                edition,
133                job_type,
134                segment,
135                year,
136            } => {
137                let filename = format!(
138                    "{}_rac_{}_{}_{}.csv.gz",
139                    state_code.to_lowercase(),
140                    segment,
141                    job_type,
142                    year
143                );
144                let uri = format!(
145                    "{}/{}/{}/{}/{}",
146                    BASE_URL,
147                    edition,
148                    state_code.to_lowercase(),
149                    self.dataset_directory(),
150                    filename
151                );
152                Ok(uri)
153            }
154            LodesDataset::WAC {
155                edition,
156                job_type,
157                segment,
158                year,
159            } => {
160                validate_availability(*year, &sc)?;
161                let filename = format!(
162                    "{}_wac_{}_{}_{}.csv.gz",
163                    state_code.to_lowercase(),
164                    segment,
165                    job_type,
166                    year
167                );
168                let uri = format!(
169                    "{}/{}/{}/{}/{}",
170                    BASE_URL,
171                    edition,
172                    state_code.to_lowercase(),
173                    self.dataset_directory(),
174                    filename
175                );
176                Ok(uri)
177            }
178        }
179    }
180
181    pub fn output_filename(&self, wildcard: &Option<GeoidType>) -> String {
182        match self {
183            LodesDataset::OD {
184                edition,
185                job_type,
186                od_part,
187                year,
188            } => {
189                let out_res = wildcard.unwrap_or(GeoidType::Block);
190                format!("{edition}_od_{year}_{job_type}_{od_part}_{out_res}.csv")
191            }
192            LodesDataset::RAC {
193                edition,
194                job_type,
195                segment,
196                year,
197            } => {
198                let out_res = wildcard.unwrap_or(GeoidType::Block);
199                format!("{edition}_rac_{year}_{job_type}_{segment}_{out_res}.csv")
200            }
201            LodesDataset::WAC {
202                edition,
203                job_type,
204                segment,
205                year,
206            } => {
207                let out_res = wildcard.unwrap_or(GeoidType::Block);
208                format!("{edition}_wac_{year}_{job_type}_{segment}_{out_res}.csv")
209            }
210        }
211    }
212
213    /// LODES editions correspond to specific TIGER/Lines datasets. see
214    /// [`LodesEdition::tiger_year`] for details. this year value should
215    /// be used when downloading complimentary TIGER/Lines datasets.
216    pub fn tiger_year(&self) -> u64 {
217        match self {
218            LodesDataset::OD {
219                edition,
220                job_type: _,
221                od_part: _,
222                year: _,
223            } => edition.tiger_year(),
224            LodesDataset::RAC {
225                edition,
226                job_type: _,
227                segment: _,
228                year: _,
229            } => edition.tiger_year(),
230            LodesDataset::WAC {
231                edition,
232                job_type: _,
233                segment: _,
234                year: _,
235            } => edition.tiger_year(),
236        }
237    }
238}
239
240/// as outlined in the tech doc, some states do not have WAC or OD data for certain years
241/// see <https://lehd.ces.census.gov/data/lodes/LODES8/LODESTechDoc8.1.pdf>
242fn validate_availability(year: u64, state_code: &StateCode) -> Result<(), String> {
243    let err = || {
244        Err(format!(
245            "WAC is not available in {} for {} (code {})",
246            year,
247            state_code.to_full_name(),
248            state_code.to_fips_string()
249        ))
250    };
251    match (year, state_code) {
252        (2002, StateCode::Arkansas) => err(),
253        (2002, StateCode::NewHampshire) => err(),
254        (y, StateCode::Arizona) if in_range_exclusive(y, 2002, 2003) => err(),
255        (y, StateCode::Mississippi) if in_range_exclusive(y, 2002, 2003) => err(),
256        (y, StateCode::DistrictOfColumbia) if in_range_exclusive(y, 2002, 2009) => err(),
257        (y, StateCode::Massachusetts) if in_range_exclusive(y, 2002, 2010) => err(),
258        (y, StateCode::Alaska) if in_range_exclusive(y, 2017, 2020) => err(),
259        (y, StateCode::Arkansas) if in_range_exclusive(y, 2019, 2020) => err(),
260        (y, StateCode::Mississippi) if in_range_exclusive(y, 2019, 2020) => err(),
261        _ => Ok(()),
262    }
263}
264
265fn in_range_exclusive(y: u64, min: u64, max: u64) -> bool {
266    min <= y && y <= max
267}