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}