use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChangeImpact {
Low,
Medium,
High,
Replacement,
}
impl ChangeImpact {
pub fn score(&self) -> f64 {
match self {
Self::Low => 0.25,
Self::Medium => 0.50,
Self::High => 0.75,
Self::Replacement => 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalVersion {
pub version_id: String,
pub issued_date: Option<NaiveDate>,
pub early_adoption_from: Option<NaiveDate>,
pub effective_from: NaiveDate,
pub superseded_at: Option<NaiveDate>,
pub jurisdiction_overrides: HashMap<String, NaiveDate>,
pub change_summary: Vec<String>,
pub impact: ChangeImpact,
}
impl TemporalVersion {
pub fn new(
version_id: impl Into<String>,
effective_from: NaiveDate,
impact: ChangeImpact,
) -> Self {
Self {
version_id: version_id.into(),
issued_date: None,
early_adoption_from: None,
effective_from,
superseded_at: None,
jurisdiction_overrides: HashMap::new(),
change_summary: Vec::new(),
impact,
}
}
pub fn with_issued_date(mut self, date: NaiveDate) -> Self {
self.issued_date = Some(date);
self
}
pub fn with_early_adoption(mut self, date: NaiveDate) -> Self {
self.early_adoption_from = Some(date);
self
}
pub fn superseded_at(mut self, date: NaiveDate) -> Self {
self.superseded_at = Some(date);
self
}
pub fn with_jurisdiction_override(mut self, country: &str, date: NaiveDate) -> Self {
self.jurisdiction_overrides
.insert(country.to_string(), date);
self
}
pub fn with_change(mut self, summary: impl Into<String>) -> Self {
self.change_summary.push(summary.into());
self
}
pub fn is_active_at(&self, date: NaiveDate) -> bool {
date >= self.effective_from && self.superseded_at.is_none_or(|sup| date < sup)
}
pub fn is_active_at_in(&self, date: NaiveDate, country: &str) -> bool {
let effective = self
.jurisdiction_overrides
.get(country)
.copied()
.unwrap_or(self.effective_from);
date >= effective && self.superseded_at.is_none_or(|sup| date < sup)
}
pub fn effective_date_for(&self, country: &str) -> NaiveDate {
self.jurisdiction_overrides
.get(country)
.copied()
.unwrap_or(self.effective_from)
}
pub fn days_active_at(&self, date: NaiveDate) -> Option<i64> {
if self.is_active_at(date) {
Some((date - self.effective_from).num_days())
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedStandard {
pub id: super::StandardId,
pub version: TemporalVersion,
pub local_designation: Option<String>,
pub applicable_entities: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn date(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).expect("valid date")
}
#[test]
fn test_version_active_at() {
let v = TemporalVersion::new("2019", date(2019, 1, 1), ChangeImpact::High);
assert!(!v.is_active_at(date(2018, 12, 31)));
assert!(v.is_active_at(date(2019, 1, 1)));
assert!(v.is_active_at(date(2025, 6, 30)));
}
#[test]
fn test_version_superseded() {
let v = TemporalVersion::new("2019", date(2019, 1, 1), ChangeImpact::High)
.superseded_at(date(2023, 1, 1));
assert!(v.is_active_at(date(2022, 12, 31)));
assert!(!v.is_active_at(date(2023, 1, 1)));
}
#[test]
fn test_jurisdiction_override() {
let v = TemporalVersion::new("2019", date(2019, 1, 1), ChangeImpact::High)
.with_jurisdiction_override("IN", date(2020, 4, 1));
assert!(v.is_active_at_in(date(2019, 6, 1), "US"));
assert!(!v.is_active_at_in(date(2019, 6, 1), "IN"));
assert!(v.is_active_at_in(date(2020, 6, 1), "IN"));
}
#[test]
fn test_change_impact_score() {
assert!((ChangeImpact::Low.score() - 0.25).abs() < f64::EPSILON);
assert!((ChangeImpact::Replacement.score() - 1.0).abs() < f64::EPSILON);
}
}