use regex::Regex;
use std::cmp::{max, min};
use std::collections::BTreeMap;
use std::str::FromStr;
use std::time;
use url::Url;
use crate::{is_killswitch_triggered, trigger_killswitch, GooseError, CANCELED};
pub fn parse_timespan(time_str: &str) -> usize {
match usize::from_str(time_str) {
Ok(t) => {
trace!("{time_str} is integer: {t} seconds");
t
}
Err(_) => {
let re = Regex::new(r"((?P<hours>\d+?)h)?((?P<minutes>\d+?)m)?((?P<seconds>\d+?)s)?")
.unwrap();
let time_matches = re.captures(time_str).unwrap();
let hours = match time_matches.name("hours") {
Some(_) => usize::from_str(&time_matches["hours"]).unwrap(),
None => 0,
};
let minutes = match time_matches.name("minutes") {
Some(_) => usize::from_str(&time_matches["minutes"]).unwrap(),
None => 0,
};
let seconds = match time_matches.name("seconds") {
Some(_) => usize::from_str(&time_matches["seconds"]).unwrap(),
None => 0,
};
let total = hours * 60 * 60 + minutes * 60 + seconds;
trace!("{hours} hours {minutes} minutes {seconds} seconds: {total} seconds");
total
}
}
}
pub async fn sleep_minus_drift(
duration: std::time::Duration,
drift: tokio::time::Instant,
) -> tokio::time::Instant {
match duration.checked_sub(drift.elapsed()) {
Some(delay) if delay.as_nanos() > 0 => tokio::time::sleep(delay).await,
_ => info!("sleep_minus_drift: drift was greater than or equal to duration, not sleeping"),
};
tokio::time::Instant::now()
}
pub fn gcd(u: usize, v: usize) -> usize {
match ((u, v), (u & 1, v & 1)) {
((x, y), _) if x == y => x,
((x, y), (0, 1)) | ((y, x), (1, 0)) => gcd(x >> 1, y),
((x, y), (0, 0)) => gcd(x >> 1, y >> 1) << 1,
((x, y), (1, 1)) => {
let (x, y) = (min(x, y), max(x, y));
gcd((y - x) >> 1, x)
}
_ => unreachable!(),
}
}
pub fn standard_deviation(raw_average: f32, co_average: f32) -> f32 {
let mean = (raw_average + co_average) / 2.0;
let raw_difference = raw_average - mean;
let co_difference = co_average - mean;
let variance = raw_difference * raw_difference + co_difference * co_difference;
variance.sqrt()
}
pub fn median(
btree: &BTreeMap<usize, usize>,
total_elements: usize,
min: usize,
max: usize,
) -> usize {
let mut total_count: usize = 0;
let half_elements: usize = (total_elements as f64 / 2.0).round() as usize;
for (value, counter) in btree {
total_count += counter;
if total_count >= half_elements {
if *value > max {
return max;
} else if *value < min {
return min;
} else {
return *value;
}
}
}
0
}
pub fn truncate_string(str_to_truncate: &str, max_length: usize) -> String {
if str_to_truncate.char_indices().count() > max_length {
match str_to_truncate.char_indices().nth(max_length - 2) {
None => str_to_truncate.to_string(),
Some((idx, _)) => format!("{}..", &str_to_truncate[..idx]),
}
} else {
str_to_truncate.to_string()
}
}
pub fn timer_expired(started: time::Instant, run_time: usize) -> bool {
run_time > 0 && started.elapsed().as_secs() >= run_time as u64
}
pub fn ms_timer_expired(started: time::Instant, elapsed: usize) -> bool {
elapsed > 0 && started.elapsed().as_millis() >= elapsed as u128
}
pub fn get_hatch_rate(hatch_rate: Option<String>) -> f32 {
get_float_from_string(hatch_rate).unwrap_or(1.0)
}
pub fn get_float_from_string(string: Option<String>) -> Option<f32> {
match string {
Some(s) => match s.parse::<f32>() {
Ok(value) => Some(value),
Err(e) => {
warn!("failed to convert {s} to float: {e}");
None
}
},
None => None,
}
}
pub fn is_valid_host(host: &str) -> Result<bool, GooseError> {
Url::parse(host).map_err(|parse_error| GooseError::InvalidHost {
host: host.to_string(),
detail: "Invalid host.".to_string(),
parse_error,
})?;
Ok(true)
}
pub(crate) fn setup_ctrlc_handler() {
match ctrlc::set_handler(move || {
if is_killswitch_triggered() {
warn!("caught another ctrl-c, exiting immediately...");
std::process::exit(1);
} else {
warn!("caught ctrl-c, stopping...");
trigger_killswitch("SIGINT received");
}
}) {
Ok(_) => (),
Err(e) => {
let mut canceled = CANCELED.write().unwrap();
*canceled = false;
info!("reset ctrl-c handler: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timespan() {
assert_eq!(parse_timespan("0"), 0);
assert_eq!(parse_timespan("foo"), 0);
assert_eq!(parse_timespan("1"), 1);
assert_eq!(parse_timespan("1s"), 1);
assert_eq!(parse_timespan("1m"), 60);
assert_eq!(parse_timespan("61"), 61);
assert_eq!(parse_timespan("1m1s"), 61);
assert_eq!(parse_timespan("10m"), 600);
assert_eq!(parse_timespan("10m5s"), 605);
assert_eq!(parse_timespan("15mins"), 900);
assert_eq!(parse_timespan("60m"), 3600);
assert_eq!(parse_timespan("1h"), 3600);
assert_eq!(parse_timespan("1h15s"), 3615);
assert_eq!(parse_timespan("1h5m"), 3900);
assert_eq!(parse_timespan("1h5m13s"), 3913);
assert_eq!(parse_timespan("2h3min"), 7380);
assert_eq!(parse_timespan("3h3m"), 10980);
assert_eq!(parse_timespan("3h3m5s"), 10985);
assert_eq!(parse_timespan("5hours"), 18000);
assert_eq!(parse_timespan("450m"), 27000);
assert_eq!(parse_timespan("24h"), 86400);
assert_eq!(parse_timespan("88h88m88s"), 322168);
assert_eq!(parse_timespan("100hourblah"), 360000);
}
#[test]
fn greatest_common_divisor() {
assert_eq!(gcd(2, 4), 2);
assert_eq!(gcd(1, 4), 1);
assert_eq!(gcd(9, 102), 3);
assert_eq!(gcd(12345, 98765), 5);
assert_eq!(gcd(2, 99), 1);
assert_eq!(gcd(gcd(30, 90), 60), 30);
assert_eq!(gcd(gcd(25, 7425), gcd(15, 9025)), 5);
}
#[test]
fn median_test() {
let mut btree: BTreeMap<usize, usize> = BTreeMap::new();
btree.insert(1, 1);
btree.insert(2, 1);
btree.insert(3, 1);
assert_eq!(median(&btree, 3, 1, 3), 2);
assert_eq!(median(&btree, 3, 1, 1), 1);
assert_eq!(median(&btree, 3, 3, 3), 3);
btree.insert(1, 2);
assert_eq!(median(&btree, 3, 1, 3), 1);
btree.insert(4, 1);
btree.insert(5, 1);
assert_eq!(median(&btree, 6, 1, 5), 2);
btree.insert(6, 1);
btree.insert(7, 2);
btree.insert(8, 1);
btree.insert(9, 2);
assert_eq!(median(&btree, 12, 1, 9), 5);
let mut btree: BTreeMap<usize, usize> = BTreeMap::new();
btree.insert(2, 1);
btree.insert(5, 1);
btree.insert(25, 1);
assert_eq!(median(&btree, 3, 2, 25), 5);
btree.insert(5, 3);
assert_eq!(median(&btree, 4, 2, 25), 5);
btree.insert(25, 10);
assert_eq!(median(&btree, 14, 2, 25), 25);
btree.insert(100, 5);
assert_eq!(median(&btree, 19, 2, 100), 25);
btree.insert(100, 20);
assert_eq!(median(&btree, 29, 2, 100), 100);
let mut btree: BTreeMap<usize, usize> = BTreeMap::new();
btree.insert(100, 3);
btree.insert(210, 1);
btree.insert(240, 1);
assert_eq!(median(&btree, 5, 101, 243), 101);
btree.insert(240, 1);
assert_eq!(median(&btree, 9, 101, 239), 239);
}
#[test]
fn truncate() {
assert_eq!(
truncate_string("the quick brown fox", 25),
"the quick brown fox"
);
assert_eq!(truncate_string("the quick brown fox", 10), "the quic..");
assert_eq!(truncate_string("abcde", 5), "abcde");
assert_eq!(truncate_string("abcde", 4), "ab..");
assert_eq!(truncate_string("abcde", 3), "a..");
assert_eq!(truncate_string("abcde", 2), "..");
assert_eq!(truncate_string("これはテストだ", 10), "これはテストだ");
assert_eq!(truncate_string("これはテストだ", 3), "こ..");
assert_eq!(truncate_string("这是一个测试。", 10), "这是一个测试。");
assert_eq!(truncate_string("这是一个测试。", 3), "这..");
assert_eq!(
truncate_string("이것은 테스트입니다.", 15),
"이것은 테스트입니다."
);
assert_eq!(truncate_string("이것은 테스트입니다.", 3), "이..");
}
#[tokio::test]
async fn timer() {
let started = time::Instant::now();
assert!(!timer_expired(started, 60));
assert!(!timer_expired(started, 0));
let sleep_duration = time::Duration::from_secs(1);
tokio::time::sleep(sleep_duration).await;
assert!(timer_expired(started, 1));
}
#[test]
fn hatch_rate() {
assert!((get_hatch_rate(Some("1".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some("1.0".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some(".5".to_string())) - 0.5).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some("0.5".to_string())) - 0.5).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some(".12345".to_string())) - 0.12345).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some("12.345".to_string())) - 12.345).abs() < f32::EPSILON);
assert!((get_hatch_rate(None) - 1.0).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some("g".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some("2.1f".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((get_hatch_rate(Some("1.1.1".to_string())) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn valid_host() {
assert!(is_valid_host("http://example.com").is_ok());
assert!(is_valid_host("example.com").is_err());
assert!(is_valid_host("http://example.com/").is_ok());
assert!(is_valid_host("example.com/").is_err());
assert!(is_valid_host("https://www.example.com/and/with/path").is_ok());
assert!(is_valid_host("www.example.com/and/with/path").is_err());
assert!(is_valid_host("foo://example.com").is_ok());
assert!(is_valid_host("file:///path/to/file").is_ok());
assert!(is_valid_host("/path/to/file").is_err());
assert!(is_valid_host("http://").is_err());
assert!(is_valid_host("http://foo").is_ok());
assert!(is_valid_host("http:///example.com").is_ok());
assert!(is_valid_host("http:// example.com").is_err());
}
}