use std::sync::Arc;
use arrow::array::{Array, ArrayRef, StringBuilder};
use crate::rng::Rng;
pub fn noise_dates(arr: &dyn arrow::array::Array, rng: &mut Rng) -> ArrayRef {
use arrow::array::AsArray;
let src = arr.as_string::<i32>();
let n = src.len();
let mut rng2 = rng.fork();
let ops: Vec<usize> = (0..n).map(|_| rng2.next_usize(4)).collect();
let mut builder = StringBuilder::with_capacity(n, n * 16);
for (i, &op) in ops.iter().enumerate().take(n) {
if src.is_null(i) {
builder.append_null();
continue;
}
let s = src.value(i);
if s.len() < 8 {
builder.append_value(s);
continue;
}
let result = match op {
0 => flip_format(s), 1 => fuzz_year(s, &mut rng2), 2 => normalize_dash(s), 3 => swap_dm(s), _ => s.to_string(),
};
builder.append_value(&result);
}
*rng = rng2;
Arc::new(builder.finish())
}
fn split_datetime(s: &str) -> (&str, Option<&str>) {
match s.split_once(' ') {
Some((date_part, time_part)) => (date_part, Some(time_part)),
None => (s, None),
}
}
fn rejoin_datetime(date_part: String, time_part: Option<&str>) -> String {
match time_part {
Some(t) => format!("{date_part} {t}"),
None => date_part,
}
}
fn flip_format(s: &str) -> String {
let (date_part, time_part) = split_datetime(s);
let parts: Vec<&str> = date_part.split(['-', '/']).collect();
let result = if parts.len() == 3 {
if parts[0].len() == 2 && parts[2].len() == 4 {
format!("{}/{}/{}", parts[2], parts[1], parts[0])
} else if parts[0].len() == 4 {
format!("{}-{}-{}", parts[2], parts[1], parts[0])
} else {
date_part.to_string()
}
} else {
date_part.to_string()
};
rejoin_datetime(result, time_part)
}
fn fuzz_year(s: &str, rng: &mut Rng) -> String {
let (date_part, time_part) = split_datetime(s);
let parts: Vec<&str> = date_part.split(['-', '/']).collect();
if parts.len() != 3 {
return s.to_string();
}
let year_idx = if parts[0].len() == 4 {
0
} else if parts[2].len() == 4 {
2
} else {
return s.to_string();
};
let year: i32 = match parts[year_idx].parse() {
Ok(y) => y,
Err(_) => return s.to_string(),
};
let offset: i32 = match rng.next_usize(4) {
0 => 10,
1 => -10,
2 => 1,
_ => -1,
};
let new_year = (year + offset).clamp(1930, 2025);
use std::fmt::Write;
let mut result = String::with_capacity(date_part.len() + 1);
for (j, p) in parts.iter().enumerate() {
if j > 0 {
result.push('-');
}
if j == year_idx {
write!(result, "{:04}", new_year).ok();
} else {
result.push_str(p);
}
}
rejoin_datetime(result, time_part)
}
fn normalize_dash(s: &str) -> String {
let (date_part, time_part) = split_datetime(s);
rejoin_datetime(date_part.replace('/', "-"), time_part)
}
fn swap_dm(s: &str) -> String {
let (date_part, time_part) = split_datetime(s);
let mut parts: Vec<&str> = date_part.split(['-', '/']).collect();
if parts.len() == 3 {
if parts[0].len() == 4 {
parts.swap(1, 2);
} else if parts[2].len() == 4 && parts[0].len() <= 2 && parts[1].len() <= 2 {
parts.swap(0, 1);
}
}
rejoin_datetime(parts.join("-"), time_part)
}
pub fn noise_dates_mix(arr: &dyn arrow::array::Array, rng: &mut Rng) -> ArrayRef {
use arrow::array::AsArray;
let src = arr.as_string::<i32>();
let n = src.len();
let mut rng2 = rng.fork();
let mut builder = StringBuilder::with_capacity(n, n * 16);
for i in 0..n {
let fmt = rng2.next_usize(4);
if src.is_null(i) {
builder.append_null();
continue;
}
let s = src.value(i);
let (date_part, time_part) = split_datetime(s);
let parts: Vec<&str> = date_part.split(['-', '/']).collect();
if parts.len() != 3 {
builder.append_value(s);
continue;
}
let day = parts[0];
let month = parts[1];
let year = parts[2];
let year_short = if year.len() == 4 {
year.get(2..).unwrap_or(year)
} else {
year
};
let result = match fmt {
0 => format!("{}/{}/{}", day, month, year),
1 => format!("{}/{}/{}", month, day, year),
2 => format!("{}/{}/{}", year, month, day),
3 => format!("{}/{}/{}", day, month, year_short),
_ => date_part.to_string(),
};
builder.append_value(rejoin_datetime(result, time_part));
}
*rng = rng2;
Arc::new(builder.finish())
}
pub fn apply_age_impossible(arr: &dyn arrow::array::Array, rng: &mut Rng) -> ArrayRef {
use arrow::array::AsArray;
let src = arr.as_string::<i32>();
let n = src.len();
let mut rng2 = rng.fork();
let strategies: Vec<usize> = (0..n).map(|_| rng2.next_usize(3)).collect();
let mut builder = StringBuilder::with_capacity(n, n * 16);
for (i, &strategy) in strategies.iter().enumerate().take(n) {
if src.is_null(i) {
builder.append_null();
continue;
}
let s = src.value(i);
let (date_part, time_part) = split_datetime(s);
let parts: Vec<&str> = date_part.split(['-', '/']).collect();
if parts.len() != 3 {
builder.append_value(s);
continue;
}
let year_idx = if parts[0].len() == 4 {
0
} else if parts[2].len() == 4 {
2
} else {
builder.append_value(s);
continue;
};
let year: i32 = match parts[year_idx].parse() {
Ok(y) => y,
Err(_) => {
builder.append_value(s);
continue;
}
};
let new_year = match strategy {
0 => year + rng2.next_usize(30) as i32 + 121, 1 => year - rng2.next_usize(31) as i32 - 20, _ => rng2.next_usize(101) as i32 + 1800, };
use std::fmt::Write;
let mut result = String::with_capacity(date_part.len() + 1);
for (j, p) in parts.iter().enumerate() {
if j > 0 {
result.push('-');
}
if j == year_idx {
write!(result, "{:04}", new_year).ok();
} else {
result.push_str(p);
}
}
builder.append_value(rejoin_datetime(result, time_part));
}
*rng = rng2;
Arc::new(builder.finish())
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Array, AsArray, StringArray};
fn test_rng() -> Rng {
Rng::new(42)
}
fn make_arr(vals: &[&str]) -> ArrayRef {
Arc::new(StringArray::from(vals.to_vec()))
}
#[test]
fn test_noise_dates() {
let arr = make_arr(&["15-03-2020", "01-01-1990", "2025-12-31"]);
let mut rng = test_rng();
let result = noise_dates(&*arr, &mut rng);
assert_eq!(result.len(), 3);
}
#[test]
fn test_noise_dates_mix() {
let arr = make_arr(&["15-03-2020", "01-01-1990"]);
let mut rng = test_rng();
let result = noise_dates_mix(&*arr, &mut rng);
assert_eq!(result.len(), 2);
}
#[test]
fn test_age_impossible() {
let arr = make_arr(&["15-03-1990", "01-01-2000"]);
let mut rng = test_rng();
let result = apply_age_impossible(&*arr, &mut rng);
assert_eq!(result.len(), 2);
}
#[test]
fn test_swap_dm_year_first_format() {
assert_eq!(swap_dm("2020-03-15"), "2020-15-03");
assert_eq!(swap_dm("15-03-2020"), "03-15-2020");
}
#[test]
fn test_deterministic() {
let arr = make_arr(&["15-03-2020", "01-01-1990"]);
let a = noise_dates(&*arr, &mut Rng::new(42));
let b = noise_dates(&*arr, &mut Rng::new(42));
let sa = a.as_string::<i32>();
let sb = b.as_string::<i32>();
assert_eq!(sa.value(0), sb.value(0));
}
}