inital commit, have algorithm for spacing commits down.

This commit is contained in:
Gabe Venberg 2024-02-09 10:13:30 -06:00
commit 437a4bfa13
10 changed files with 1939 additions and 0 deletions

25
src/args.rs Normal file
View file

@ -0,0 +1,25 @@
use chrono::{NaiveDateTime, TimeZone};
use clap::Parser;
#[derive(Debug, Parser)]
#[command(author, version, about)]
pub(crate) struct Cli {
/// earliest time to place a commit, in format yyyy-mm-ddThh:mm:ss
pub(crate) start_time: NaiveDateTime,
/// latest time to place a commit, in format yyyy-mm-ddThh:mm:ss. Defaults to current time.
pub(crate) end_time: Option<NaiveDateTime>,
/// first commit to modify, will default to the first commit of the repo
#[arg(short, long)]
pub(crate) first_commit: Option<String>,
/// last commit to modify, will default to HEAD
#[arg(short, long)]
pub(crate) last_commit: Option<String>,
/// Timezone to place commits in, will default to local tz. Must be a Tz identifier as listed
/// on https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. Defaults to local tz
#[arg(short, long)]
pub(crate) timezone: Option<chrono_tz::Tz>
}

1
src/err.rs Normal file
View file

@ -0,0 +1 @@
use thiserror;

26
src/main.rs Normal file
View file

@ -0,0 +1,26 @@
mod args;
mod err;
mod time;
use chrono::prelude::*;
use chrono_tz::Tz;
use clap::Parser;
fn main() {
let cli = args::Cli::parse();
println!("{:#?}", cli);
let start_time = match cli.timezone {
Some(tz) => tz.from_local_datetime(&cli.start_time).unwrap(),
None => get_local_timezone()
.expect("Could not get local timezone")
.from_local_datetime(&cli.start_time)
.unwrap(),
};
}
fn get_local_timezone() -> Result<Tz, String> {
match iana_time_zone::get_timezone() {
Ok(tz_string) => tz_string.parse(),
Err(e) => Err(e.to_string()),
}
}

185
src/time.rs Normal file
View file

@ -0,0 +1,185 @@
use chrono::{DateTime, Duration};
use rand::{rngs::SmallRng, Rng, SeedableRng};
/// A range of time represented by a start datetime and an end datetime.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TimeRange<Tz: chrono::TimeZone> {
pub start: DateTime<Tz>,
pub end: DateTime<Tz>,
}
impl<Tz: chrono::TimeZone> TimeRange<Tz> {
/// whether a time instance lies within the time range
pub fn is_in_range<Otz: chrono::TimeZone>(&self, other: &DateTime<Otz>) -> bool {
(self.start <= *other) && (other <= &self.end)
}
/// the time between the start and end of the time range.
pub fn duration(&self) -> Duration {
self.end.clone() - self.start.clone()
}
/// Distribute a number of events evenly across a time range.
pub fn distribute_evenly(&self, number: u32) -> Vec<DateTime<Tz>> {
let time_between_events = self.duration() / ((number) as i32);
let mut events = Vec::new();
for i in 0..number {
events.push(self.start.clone() + time_between_events * (i as i32))
}
events
}
/// Distribute a number of events evenly across a time range with a specific amount of jitter.
pub fn distribute_with_jitter(&self, number: u32, max_jitter: &Duration) -> Vec<DateTime<Tz>> {
let mut events = self.distribute_evenly(number);
if !Duration::is_zero(max_jitter) {
let mut rng = SmallRng::from_entropy();
for event in &mut events {
//make sure we dont exceed the bounds of the range even with jitter.
let min_offset = (self.start.clone() - event.clone()).num_seconds();
let max_offset = (self.end.clone() - event.clone()).num_seconds();
*event += Duration::seconds(rng.gen_range(
min_offset.max(-max_jitter.num_seconds())
..max_offset.min(max_jitter.num_seconds()),
))
}
events.sort();
}
events
}
}
/// Distribute a number of events across a set of time ranges evenly,
/// applying a specific amount of jitter.
pub fn distribute_across_ranges_with_jitter<Tz: chrono::TimeZone>(
ranges: &[TimeRange<Tz>],
number: u32,
max_jitter: &Duration,
) -> Vec<DateTime<Tz>> {
// compress all the time ranges into a single range
let total_duration: Duration = ranges
.iter()
.fold(Duration::zero(), |a, r| a + r.duration());
let compressed_time = TimeRange {
start: ranges[0].start.clone(),
end: ranges[0].start.clone() + total_duration,
};
// distribute events
let events = compressed_time.distribute_with_jitter(number, max_jitter);
// uncompress the compressed range back into the original set of ranges
let mut expanded_events = Vec::new();
for event in events {
let mut time_after_start = event - compressed_time.start.clone();
for region in ranges {
if time_after_start >= region.duration() {
time_after_start -= region.duration();
} else {
expanded_events.push(region.start.clone() + time_after_start);
break;
}
}
}
expanded_events
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use itertools::Itertools;
use super::*;
#[test]
fn test_distribute_evenly() {
let timerange = TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
};
// 12 events should be spaced every 2 hours.
let events = timerange.distribute_evenly(12);
println!("{:#?}", events);
assert_eq!(events.len(), 12);
for (a, b) in events.iter().tuple_windows() {
assert_eq!((*b - a).num_hours(), 2)
}
for event in events {
assert!(timerange.is_in_range(&event))
}
}
#[test]
fn test_distribute_with_jitter() {
let timerange = TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
};
let events = timerange.distribute_evenly(12);
let random_events = timerange.distribute_with_jitter(12, &Duration::minutes(30));
println!("{:#?}", random_events);
for (standard, jittered) in events.iter().zip(random_events.iter()) {
assert!((*standard - jittered).num_minutes().abs() <= 30);
}
for event in random_events {
assert!(timerange.is_in_range(&event))
}
}
#[test]
fn test_with_extreme_jitter() {
let timerange = TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
};
let events = timerange.distribute_with_jitter(12, &Duration::hours(48));
println!("{:#?}", events);
for event in events {
assert!(timerange.is_in_range(&event))
}
}
#[test]
fn test_distribute_across_ranges_without_jitter() {
let ranges = vec![
TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
},
TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(),
},
];
//
let events = distribute_across_ranges_with_jitter(&ranges, 24, &Duration::zero());
println!("{:#?}", events);
assert_eq!(events.len(), 24);
for event in events {
assert!(ranges.iter().any(|r| r.is_in_range(&event)))
}
}
#[test]
fn test_distribute_across_ranges_with_jitter() {
let ranges = vec![
TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
},
TimeRange::<chrono::Utc> {
start: Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap(),
end: Utc.with_ymd_and_hms(2024, 1, 6, 0, 0, 0).unwrap(),
},
];
//
let events = distribute_across_ranges_with_jitter(&ranges, 24, &Duration::zero());
let random_events = distribute_across_ranges_with_jitter(&ranges, 24, &Duration::hours(1));
println!("{:#?}", events);
println!("{:#?}", random_events);
assert_eq!(events.len(), 24);
for event in events {
assert!(ranges.iter().any(|r| r.is_in_range(&event)))
}
}
}