use chrono::{DateTime, Datelike, TimeZone, Timelike};
use rand_distr::num_traits::Pow;
use super::{AnoError, TimeUnit, datetime_to_rfc3339};
pub struct NumberAggregator {
power_of_ten_exponent: i32,
}
impl NumberAggregator {
pub fn new(power_of_ten_exponent: i32) -> Result<Self, AnoError> {
if power_of_ten_exponent > f64::MAX_10_EXP {
return Err(AnoError::AnonymizationError(format!(
"Exponent must be lower than {}, given {power_of_ten_exponent}.",
f64::MAX_10_EXP,
)));
}
if power_of_ten_exponent < -(f64::MAX_10_EXP) {
return Err(AnoError::AnonymizationError(format!(
"Exponent must be greater than {}, given {power_of_ten_exponent}.",
-(f64::MAX_10_EXP),
)));
}
Ok(Self {
power_of_ten_exponent,
})
}
#[must_use]
pub fn apply_on_float(&self, data: f64) -> String {
if self.power_of_ten_exponent < 0 {
let precision = usize::try_from(-self.power_of_ten_exponent).unwrap_or(0);
return format!("{data:.precision$}");
}
let r = 10_f64.pow(self.power_of_ten_exponent);
format!("{}", (data / r).round() * r)
}
#[must_use]
pub fn apply_on_int(&self, data: i64) -> String {
let r = 10_f64.pow(self.power_of_ten_exponent);
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let as_float = data as f64;
format!("{:.0}", (as_float / r).round() * r)
}
}
pub struct DateAggregator {
time_unit: TimeUnit,
}
impl DateAggregator {
#[must_use]
pub const fn new(time_unit: TimeUnit) -> Self {
Self { time_unit }
}
pub fn apply_on_date(&self, date_str: &str) -> Result<String, AnoError> {
let date = DateTime::parse_from_rfc3339(date_str).map_err(|e| {
AnoError::AnonymizationError(format!(
"invalid RFC3339 date '{date_str}': {e} (expected format: 2023-04-07T12:34:56+02:00)"
))
})?;
let tz = date.timezone();
let (y, mo, d, h, mi, s) = match self.time_unit {
TimeUnit::Second => (
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute(),
date.second(),
),
TimeUnit::Minute => (
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute(),
0,
),
TimeUnit::Hour => (date.year(), date.month(), date.day(), date.hour(), 0, 0),
TimeUnit::Day => (date.year(), date.month(), date.day(), 0, 0, 0),
TimeUnit::Month => (date.year(), date.month(), 1, 0, 0, 0),
TimeUnit::Year => (date.year(), 1, 1, 0, 0, 0),
};
datetime_to_rfc3339(tz.with_ymd_and_hms(y, mo, d, h, mi, s), date_str)
}
}
pub struct NumberScaler {
mean: f64,
std_deviation: f64,
scale: f64,
translate: f64,
}
impl NumberScaler {
pub fn new(
mean: f64,
std_deviation: f64,
scale: f64,
translate: f64,
) -> Result<Self, AnoError> {
if std_deviation == 0.0 {
return Err(AnoError::AnonymizationError(
"Standard deviation must be non-zero to avoid division by zero.".to_owned(),
));
}
Ok(Self {
mean,
std_deviation,
scale,
translate,
})
}
#[must_use]
pub fn apply_on_float(&self, data: f64) -> f64 {
let normalized_data = (data - self.mean) / self.std_deviation;
normalized_data.mul_add(self.scale, self.translate)
}
#[must_use]
pub fn apply_on_int(&self, data: i64) -> i64 {
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::as_conversions
)]
let result = self.apply_on_float(data as f64).round() as i64;
result
}
}