destination/lexisnexis.rs
1//! The `lexisnexis` module produces address range reports for the LexisNexis dispatch service.
2use crate::{
3 from_bin, from_csv, to_bin, to_csv, Address, AddressError, AddressErrorKind, Addresses,
4 Bincode, Builder, IntoBin, IntoCsv, Io,
5};
6use derive_more::{Deref, DerefMut};
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use std::path::Path;
10
11/// The `LexisNexisItemBuilder` struct provides a framework to create and modify the required fields in the LexisNexis spreadsheet.
12#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
13pub struct LexisNexisItemBuilder {
14 /// The `address_number_from` field represents the lower bound on the address number range for
15 /// the row.
16 pub address_number_from: Option<i64>,
17 /// The `address_number_to` field represents the upper bound on the address number range for
18 /// the row.
19 pub address_number_to: Option<i64>,
20 /// The `street_name_pre_directional` represents the street name pre-directional using the
21 /// standard postal abbreviation.
22 pub street_name_pre_directional: Option<String>,
23 /// The `street_name` field represents the street name.
24 pub street_name: Option<String>,
25 /// The `street_name_post_type` field represents the street name post type.
26 pub street_name_post_type: Option<String>,
27 /// The `street_name_post_directional` field represents the street name post-directional.
28 /// Grants Pass does not use street name post directional designations.
29 pub street_name_post_directional: Option<String>,
30 /// The `postal_community` field represents the city or postal community in an address.
31 pub postal_community: Option<String>,
32 /// The `beat` field is a required field in LexisNexis, but not used by the city.
33 pub beat: Option<String>,
34 /// The `area` field is a required field in LexisNexis, but not used by the city.
35 pub area: Option<String>,
36 /// The `district` field is a required field in LexisNexis, but not used by the city.
37 pub district: Option<String>,
38 /// The `zone` field is a required field in LexisNexis, but not used by the city.
39 pub zone: Option<String>,
40 /// The `zip_code` field represents the 5-digit postal zip code for addresses.
41 pub zip_code: Option<i64>,
42 /// The `commonplace` field is a required field in LexisNexis, but not used by the city.
43 pub commonplace: Option<String>,
44 /// The `address_number` field is a required field in LexisNexis, but not used by the city.
45 pub address_number: Option<i64>,
46}
47
48impl LexisNexisItemBuilder {
49 /// Creates a new `LexisNexisItemBuilder`, with fields initialized to default values. Because
50 /// of the number of fields in the [`LexisNexisItem`] struct, we use a builder to initialize a
51 /// struct with default values, and then modify the values of the fields before calling
52 /// *build*.
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 /// The `build` method converts a `LexisNexisItemBuilder` into a [`LexisNexisItem`]. Returns
58 /// an error if a required field is missing, or set to None when a value is required.
59 pub fn build(self) -> Result<LexisNexisItem, Builder> {
60 let target = "LexisNexisItem".to_string();
61 let address_number_from = if let Some(num) = self.address_number_from {
62 num
63 } else {
64 tracing::warn!("Missing address number from.");
65 let error = Builder::new(
66 "address_number_from field is None".to_string(),
67 target.clone(),
68 line!(),
69 file!().to_string(),
70 );
71 return Err(error);
72 };
73 let address_number_to = if let Some(num) = self.address_number_to {
74 num
75 } else {
76 tracing::warn!("Missing address number to.");
77 let error = Builder::new(
78 "address_number_to field is None".to_string(),
79 target.clone(),
80 line!(),
81 file!().to_string(),
82 );
83 return Err(error);
84 };
85 let street_name = if let Some(s) = self.street_name {
86 s
87 } else {
88 tracing::warn!("Missing street name.");
89 let error = Builder::new(
90 "street_name field is None".to_string(),
91 target.clone(),
92 line!(),
93 file!().to_string(),
94 );
95 return Err(error);
96 };
97 let street_name_post_type = if let Some(s) = self.street_name_post_type {
98 s
99 } else {
100 tracing::warn!("Street name post type missing.");
101 let error = Builder::new(
102 "street_name_post_type field is None".to_string(),
103 target.clone(),
104 line!(),
105 file!().to_string(),
106 );
107 return Err(error);
108 };
109 let postal_community = if let Some(s) = self.postal_community {
110 s
111 } else {
112 tracing::warn!("Postal community missing.");
113 let error = Builder::new(
114 "postal_community field is None".to_string(),
115 target.clone(),
116 line!(),
117 file!().to_string(),
118 );
119 return Err(error);
120 };
121 let zip_code = if let Some(num) = self.zip_code {
122 num
123 } else {
124 tracing::warn!("Zip code missing.");
125 let error = Builder::new(
126 "zip_code field is None".to_string(),
127 target.clone(),
128 line!(),
129 file!().to_string(),
130 );
131 return Err(error);
132 };
133 Ok(LexisNexisItem {
134 address_number_from,
135 address_number_to,
136 street_name_pre_directional: self.street_name_pre_directional,
137 street_name,
138 street_name_post_type,
139 street_name_post_directional: self.street_name_post_directional,
140 postal_community,
141 beat: self.beat,
142 area: self.area,
143 district: self.district,
144 zone: self.zone,
145 zip_code,
146 commonplace: self.commonplace,
147 address_number: self.address_number,
148 id: uuid::Uuid::new_v4(),
149 })
150 }
151}
152
153/// The `LexisNexisItem` struct contains the required fields in the LexisNexis spreadsheet.
154#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
155pub struct LexisNexisItem {
156 /// The `address_number_from` field represents the lower range of address numbers associated
157 /// with the service area.
158 #[serde(rename(serialize = "StNumFrom"))]
159 pub address_number_from: i64,
160 /// The `address_number_to` field represents the upper range of address numbers associated
161 /// with the service area.
162 #[serde(rename(serialize = "StNumTo"))]
163 pub address_number_to: i64,
164 /// The `street_name_pre_directional` field represents the street name pre directional
165 /// associated with the service area.
166 #[serde(rename(serialize = "StPreDirection"))]
167 pub street_name_pre_directional: Option<String>,
168 /// The `street_name` field represents the street name component of the complete street name
169 /// associated with the service area.
170 #[serde(rename(serialize = "StName"))]
171 pub street_name: String,
172 /// The `street_name_post_type` field represents the street name post type component of the
173 /// complete street name associated with the service area.
174 #[serde(rename(serialize = "StType"))]
175 pub street_name_post_type: String,
176 /// The `street_name_post_directional` field represents the street name post directional component of
177 /// the complete street name. The City of Grants Pass does not issue addresses using a street
178 /// name post directional component, but Josephine County does have some examples in their
179 /// records.
180 #[serde(rename(serialize = "StPostDirection"))]
181 pub street_name_post_directional: Option<String>,
182 /// The `postal_community` field represents either the unincorporated or incorporated
183 /// municipality name associated with the service area.
184 #[serde(rename(serialize = "City"))]
185 pub postal_community: String,
186 /// The `beat` field represents the police response jurisdiction associated with the service
187 /// area. The City of Grants Pass does not use this field directly, but its presence is a
188 /// requirement of the LexisNexis schema.
189 #[serde(rename(serialize = "Beat"))]
190 pub beat: Option<String>,
191 /// The `area` field represents the service
192 /// area. The City of Grants Pass does not use this field directly, but its presence is a
193 /// requirement of the LexisNexis schema.
194 #[serde(rename(serialize = "Area"))]
195 pub area: Option<String>,
196 /// The `district` field represents the service
197 /// district. The City of Grants Pass does not use this field directly, but its presence is a
198 /// requirement of the LexisNexis schema.
199 #[serde(rename(serialize = "District"))]
200 pub district: Option<String>,
201 /// The `zone` field represents the service
202 /// zone. The City of Grants Pass does not use this field directly, but its presence is a
203 /// requirement of the LexisNexis schema.
204 #[serde(rename(serialize = "Zone"))]
205 pub zone: Option<String>,
206 /// The `zip_code` field represents the postal zip code associated with the service area.
207 #[serde(rename(serialize = "Zipcode"))]
208 pub zip_code: i64,
209 /// The `commonplace` field represents a common name associated with the service area. The
210 /// City of Grants Pass does not use this field directly, but its presence is a requirement of
211 /// the LexisNexis schema.
212 #[serde(rename(serialize = "CommonPlace"))]
213 pub commonplace: Option<String>,
214 /// The `address_number` field may possibly serve to represent a service area with an address
215 /// range of one, but the City of Grants Pass reports these ranges using a single value for the
216 /// _from and _to fields, so this field is currently unused. Its presence is a requirement of
217 /// the LexisNexis schema.
218 #[serde(rename(serialize = "StNum"))]
219 pub address_number: Option<i64>,
220 /// The `id` field is an internal unique id.
221 #[serde(skip_serializing)]
222 pub id: uuid::Uuid,
223}
224
225/// The `LexisNexis` struct holds a vector of [`LexisNexisItem`] objects, for serialization into a
226/// .csv file.
227#[derive(
228 Default,
229 Debug,
230 Clone,
231 PartialEq,
232 Eq,
233 PartialOrd,
234 Ord,
235 Hash,
236 Deserialize,
237 Serialize,
238 Deref,
239 DerefMut,
240 derive_new::new,
241)]
242pub struct LexisNexis(Vec<LexisNexisItem>);
243
244impl LexisNexis {
245 /// The `from_addresses` method creates a [`LexisNexis`] struct from a set of addresses to
246 /// include in the range selection `include`, and a set of addresses to exclude from the range
247 /// selection `exclude`.
248 pub fn from_addresses<T: Address + Clone + Send + Sync, U: Addresses<T>>(
249 include: &U,
250 exclude: &U,
251 ) -> Result<LexisNexis, Builder> {
252 // List of unique street names processed so far.
253 let mut seen = HashSet::new();
254 // Vector to hold Lexis Nexis results.
255 let mut records = Vec::new();
256 // For each address in the inclusion list...
257 for address in include.iter() {
258 // Get the complete street name.
259 let comp_street = address.complete_street_name(false);
260 // If comp_street is a new street name...
261 if !seen.contains(&comp_street) {
262 // Add the new name to the list of seen names.
263 seen.insert(comp_street.clone());
264 // Obtain mutable clone of include group.
265 let mut inc = include.clone();
266 // Filter include group by current street name.
267 inc.filter_field("complete_street_name", &comp_street);
268 // Obtain mutable clone of exclude group.
269 let mut exl = exclude.clone();
270 // Filter exclude group by current street name.
271 exl.filter_field("complete_street_name", &comp_street);
272 tracing::trace!(
273 "After street name filter, inc: {}, exl: {}",
274 inc.len(),
275 exl.len()
276 );
277 let items = LexisNexisRange::from_addresses(&inc, &exl);
278 let ranges = items.ranges();
279 for rng in ranges {
280 let mut builder = LexisNexisItemBuilder::new();
281 builder.address_number_from = Some(rng.0);
282 builder.address_number_to = Some(rng.1);
283 builder.street_name_pre_directional = address.directional_abbreviated();
284 builder.street_name = Some(address.common_street_name().clone());
285 if let Some(street_type) = address.street_type() {
286 builder.street_name_post_type = Some(street_type.abbreviate());
287 }
288 builder.postal_community = Some(address.postal_community().clone());
289 builder.zip_code = Some(address.zip());
290 if let Ok(built) = builder.build() {
291 records.push(built);
292 }
293 }
294 }
295 }
296 Ok(LexisNexis(records))
297 }
298}
299
300impl IntoBin<LexisNexis> for LexisNexis {
301 fn load<P: AsRef<Path>>(path: P) -> Result<Self, AddressError> {
302 match from_bin(path) {
303 Ok(records) => bincode::deserialize::<Self>(&records)
304 .map_err(|source| Bincode::new(source, line!(), file!().into()).into()),
305 Err(source) => Err(source.into()),
306 }
307 }
308
309 fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), AddressError> {
310 to_bin(self, path)
311 }
312}
313
314impl IntoCsv<LexisNexis> for LexisNexis {
315 fn from_csv<P: AsRef<Path>>(path: P) -> Result<Self, Io> {
316 let records = from_csv(path)?;
317 Ok(Self(records))
318 }
319
320 fn to_csv<P: AsRef<Path>>(&mut self, path: P) -> Result<(), AddressErrorKind> {
321 to_csv(&mut self.0, path.as_ref().into())
322 }
323}
324
325/// The `LexisNexisRangeItem` represents an address number `num`, and whether to include the number
326/// in the range selection.
327#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
328pub struct LexisNexisRangeItem {
329 /// The `num` field represents an address number observation.
330 pub num: i64,
331 /// The `include` field represents whether to include the number in the range selection.
332 pub include: bool,
333}
334
335impl LexisNexisRangeItem {
336 /// Creates a new `LexisNexisRangeItem` from an address number `num` and a boolean `include` indicating
337 /// whether to include the address number in the range.
338 pub fn new(num: i64, include: bool) -> Self {
339 Self { num, include }
340 }
341}
342
343/// The `LexisNexisRange` struct holds a vector of address number observations associated with a given complete
344/// street name. The `include` field is *true* for addresses within the city limits or with a public
345/// safety agreement, and *false* for addresses outside of city limits or without a public safety
346/// agreement. Used to produce valid ranges of addresses in the city service area.
347#[derive(
348 Default,
349 Debug,
350 Clone,
351 PartialEq,
352 Eq,
353 PartialOrd,
354 Ord,
355 Hash,
356 Deserialize,
357 Serialize,
358 Deref,
359 DerefMut,
360)]
361pub struct LexisNexisRange(Vec<LexisNexisRangeItem>);
362
363impl LexisNexisRange {
364 /// The `from_addresses` method creates a [`LexisNexisRange`] from a set of addresses to
365 /// include in the range selection `include`, and a set of addresses to exclude from the range
366 /// selection `exclude`.
367 pub fn from_addresses<T: Address + Clone + Send + Sync, U: Addresses<T>>(
368 include: &U,
369 exclude: &U,
370 ) -> Self {
371 let mut records = include
372 .iter()
373 .map(|v| LexisNexisRangeItem::new(v.number(), true))
374 .collect::<Vec<LexisNexisRangeItem>>();
375 records.extend(
376 exclude
377 .iter()
378 .map(|v| LexisNexisRangeItem::new(v.number(), false))
379 .collect::<Vec<LexisNexisRangeItem>>(),
380 );
381 records.sort_by_key(|v| v.num);
382 // tracing::info!("Record: {:#?}", &records);
383 Self(records)
384 }
385
386 /// The `ranges` method returns the ranges of addresses within the service area, as marked by
387 /// the `include` field.
388 pub fn ranges(&self) -> Vec<(i64, i64)> {
389 let mut rngs = Vec::new();
390 let mut min = 0;
391 let mut max = 0;
392 let mut open = false;
393 for item in self.iter() {
394 if item.include {
395 if !open {
396 open = true;
397 min = item.num;
398 }
399 max = item.num;
400 } else if open {
401 open = false;
402 rngs.push((min, max));
403 }
404 }
405 if open {
406 rngs.push((min, max));
407 }
408 rngs
409 }
410}