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 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 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
240fn 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}