inital commit, have algorithm for spacing commits down.
This commit is contained in:
commit
437a4bfa13
10 changed files with 1939 additions and 0 deletions
25
src/args.rs
Normal file
25
src/args.rs
Normal 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
1
src/err.rs
Normal file
|
@ -0,0 +1 @@
|
|||
use thiserror;
|
26
src/main.rs
Normal file
26
src/main.rs
Normal 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
185
src/time.rs
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue