use crate::{
Address, AddressErrorKind, AddressStatus, Geographic, IntoCsv, Io, PartialAddress,
PartialAddresses, SubaddressType, from_csv, to_csv,
};
use derive_more::{Deref, DerefMut};
use indicatif::ParallelProgressIterator;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::info;
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
pub enum Mismatch {
SubaddressType(String),
Floor(String),
Building(String),
Status(String),
}
impl Mismatch {
pub fn subaddress_type(from: Option<SubaddressType>, to: Option<SubaddressType>) -> Self {
let message = format!("{:?} not equal to {:?}", from, to);
Self::SubaddressType(message)
}
pub fn floor(from: Option<i64>, to: Option<i64>) -> Self {
let message = format!("{:?} not equal to {:?}", from, to);
Self::Floor(message)
}
pub fn building(from: Option<String>, to: Option<String>) -> Self {
let message = format!("{:?} not equal to {:?}", from, to);
Self::Building(message)
}
pub fn status(from: AddressStatus, to: AddressStatus) -> Self {
let message = format!("{} not equal to {}", from, to);
Self::Status(message)
}
}
#[derive(
Debug,
Default,
Clone,
PartialEq,
PartialOrd,
Eq,
Ord,
Hash,
Serialize,
Deserialize,
Deref,
DerefMut,
)]
pub struct Mismatches(Vec<Mismatch>);
impl Mismatches {
pub fn new(fields: Vec<Mismatch>) -> Self {
Mismatches(fields)
}
}
#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
pub struct AddressMatch {
pub coincident: bool,
pub mismatches: Option<Mismatches>,
}
impl AddressMatch {
pub fn new(coincident: bool, fields: Vec<Mismatch>) -> Self {
let mismatches = if fields.is_empty() {
None
} else {
Some(Mismatches::new(fields))
};
AddressMatch {
coincident,
mismatches,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
pub enum MatchStatus {
Matching,
Divergent,
#[default]
Missing,
}
#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct MatchRecord {
pub match_status: MatchStatus,
pub address_label: String,
pub subaddress_type: Option<String>,
pub floor: Option<String>,
pub building: Option<String>,
pub status: Option<String>,
pub longitude: f64,
pub latitude: f64,
pub id: uuid::Uuid,
}
impl Geographic for MatchRecord {
fn latitude(&self) -> f64 {
self.latitude
}
fn longitude(&self) -> f64 {
self.longitude
}
}
#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Serialize, Deserialize, Deref, DerefMut)]
pub struct MatchRecords(Vec<MatchRecord>);
impl MatchRecords {
pub fn new<T: Address + Geographic, U: Address + Geographic>(
self_address: &T,
other_addresses: &[U],
) -> Self {
let address_label = self_address.label();
let latitude = self_address.latitude();
let longitude = self_address.longitude();
let id = uuid::Uuid::new_v4();
let mut match_record = Vec::new();
for address in other_addresses {
let address_match = self_address.coincident(address);
if address_match.coincident {
let mut subaddress_type = None;
let mut floor = None;
let mut building = None;
let mut status = None;
match address_match.mismatches {
None => match_record.push(MatchRecord {
match_status: MatchStatus::Matching,
address_label: address_label.clone(),
subaddress_type,
floor,
building,
status,
longitude,
latitude,
id,
}),
Some(mismatches) => {
for mismatch in mismatches.iter() {
match mismatch {
Mismatch::SubaddressType(message) => {
subaddress_type = Some(message.to_owned())
}
Mismatch::Floor(message) => floor = Some(message.to_owned()),
Mismatch::Building(message) => building = Some(message.to_owned()),
Mismatch::Status(message) => status = Some(message.to_owned()),
}
}
match_record.push(MatchRecord {
match_status: MatchStatus::Divergent,
address_label: address_label.clone(),
subaddress_type,
floor,
building,
status,
longitude,
latitude,
id,
})
}
}
}
}
if match_record.is_empty() {
match_record.push(MatchRecord {
match_status: MatchStatus::Missing,
address_label,
subaddress_type: None,
floor: None,
building: None,
status: None,
longitude,
latitude,
id,
})
}
MatchRecords(match_record)
}
pub fn compare<T: Address + Geographic + Send + Sync, U: Address + Geographic + Send + Sync>(
self_addresses: &[T],
other_addresses: &[U],
) -> Self {
let style = indicatif::ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {'Comparing addresses.'}",
)
.unwrap();
let record = self_addresses
.par_iter()
.map(|address| MatchRecords::new(address, other_addresses))
.progress_with_style(style)
.collect::<Vec<MatchRecords>>();
let mut records = Vec::new();
for mut item in record {
records.append(&mut item);
}
MatchRecords(records)
}
pub fn filter(mut self, filter: &str) -> Self {
match filter {
"matching" => self.retain(|r| r.match_status == MatchStatus::Matching),
"missing" => self.retain(|r| r.match_status == MatchStatus::Missing),
"divergent" => self.retain(|r| r.match_status == MatchStatus::Divergent),
"subaddress" => self.retain(|r| {
r.match_status == MatchStatus::Divergent && r.subaddress_type.is_some()
}),
"floor" => {
self.retain(|r| r.match_status == MatchStatus::Divergent && r.floor.is_some())
}
"building" => {
self.retain(|r| r.match_status == MatchStatus::Divergent && r.building.is_some())
}
"status" => {
self.retain(|r| r.match_status == MatchStatus::Divergent && r.status.is_some())
}
_ => info!("Invalid filter provided."),
}
self
}
}
impl IntoCsv<MatchRecords> for MatchRecords {
fn from_csv<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Io> {
let records = from_csv(path)?;
Ok(Self(records))
}
fn to_csv<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), AddressErrorKind> {
to_csv(&mut self.0, path.as_ref().into())
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct MatchPartialRecord {
match_status: MatchStatus,
address_label: String,
other_label: Option<String>,
longitude: Option<f64>,
latitude: Option<f64>,
}
impl MatchPartialRecord {
pub fn coincident<T: Address + Geographic>(
partial: &PartialAddress,
address: &T,
) -> Option<MatchPartialRecord> {
let mut match_status = MatchStatus::Missing;
if let Some(value) = partial.address_number {
if value == address.number() {
match_status = MatchStatus::Matching;
}
}
if &partial.street_name_pre_directional != address.directional()
&& match_status == MatchStatus::Matching
{
match_status = MatchStatus::Missing;
}
if let Some(value) = &partial.street_name {
if value != address.street_name() && match_status == MatchStatus::Matching {
match_status = MatchStatus::Missing;
}
}
if let Some(value) = partial.street_name_post_type() {
if let &Some(street_type) = address.street_type() {
if value != street_type && match_status == MatchStatus::Matching {
match_status = MatchStatus::Missing;
}
}
}
if &partial.subaddress_identifier() != address.subaddress_id()
&& match_status == MatchStatus::Matching
{
match_status = MatchStatus::Divergent;
}
if address.subaddress_id().is_none()
&& &partial.building() != address.building()
&& match_status == MatchStatus::Matching
{
match_status = MatchStatus::Divergent;
}
if address.subaddress_id().is_none()
&& address.building().is_none()
&& &partial.floor() != address.floor()
&& match_status == MatchStatus::Matching
{
match_status = MatchStatus::Divergent;
}
if match_status != MatchStatus::Missing {
Some(MatchPartialRecord {
match_status,
address_label: partial.label(),
other_label: Some(address.label()),
longitude: Some(address.longitude()),
latitude: Some(address.latitude()),
})
} else {
None
}
}
pub fn compare<T: Address + Geographic>(
partial: &PartialAddress,
addresses: &[T],
) -> MatchPartialRecords {
let mut records = Vec::new();
for address in addresses {
let coincident = MatchPartialRecord::coincident(partial, address);
if let Some(record) = coincident {
records.push(record);
}
}
if records.is_empty() {
records.push(MatchPartialRecord {
match_status: MatchStatus::Missing,
address_label: partial.label(),
other_label: None,
longitude: None,
latitude: None,
})
}
let compared = MatchPartialRecords(records);
let matching = compared.clone().filter("matching");
if matching.is_empty() {
compared
} else {
matching
}
}
pub fn match_status(&self) -> MatchStatus {
self.match_status.to_owned()
}
pub fn address_label(&self) -> String {
self.address_label.to_owned()
}
pub fn other_label(&self) -> Option<String> {
self.other_label.clone()
}
pub fn longitude(&self) -> Option<f64> {
self.longitude
}
pub fn latitude(&self) -> Option<f64> {
self.latitude
}
}
#[derive(
Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, Deref, DerefMut, derive_new::new,
)]
pub struct MatchPartialRecords(Vec<MatchPartialRecord>);
impl MatchPartialRecords {
pub fn compare<T: Address + Geographic + Send + Sync>(
self_addresses: &PartialAddresses,
other_addresses: &[T],
) -> Self {
let style = indicatif::ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {'Comparing addresses.'}",
)
.unwrap();
let record = self_addresses
.par_iter()
.map(|address| MatchPartialRecord::compare(address, other_addresses))
.progress_with_style(style)
.collect::<Vec<MatchPartialRecords>>();
let mut records = Vec::new();
for mut item in record {
records.append(&mut item);
}
MatchPartialRecords(records)
}
pub fn filter(mut self, filter: &str) -> Self {
match filter {
"missing" => self.retain(|r| r.match_status == MatchStatus::Missing),
"divergent" => self.retain(|r| r.match_status == MatchStatus::Divergent),
"matching" => self.retain(|r| r.match_status == MatchStatus::Matching),
_ => info!("Invalid filter provided."),
}
self
}
}
impl IntoCsv<MatchPartialRecords> for MatchPartialRecords {
fn from_csv<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Io> {
let records = from_csv(path)?;
Ok(Self(records))
}
fn to_csv<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<(), AddressErrorKind> {
to_csv(&mut self.0, path.as_ref().into())
}
}