use lazy_static::lazy_static;
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, reset_killswitch, trigger_killswitch, GooseError};
pub fn parse_timespan(time_str: &str) -> Option<usize> {
timespan_seconds(time_str).ok()
}
#[derive(Debug)]
pub(crate) enum SpanError {
TooLong,
ForeignDigit,
Invalid,
}
lazy_static! {
static ref TIMESPAN: Regex =
Regex::new(r"(?i)^((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\d+)s)?$").unwrap();
}
pub(crate) fn timespan_seconds(time_str: &str) -> Result<usize, SpanError> {
let cause = |e: &std::num::ParseIntError| match e.kind() {
std::num::IntErrorKind::PosOverflow => SpanError::TooLong,
_ => SpanError::ForeignDigit,
};
let seconds = match usize::from_str(time_str) {
Ok(t) => t,
Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => {
return Err(SpanError::TooLong)
}
Err(_) => {
let time_matches = TIMESPAN.captures(time_str).ok_or(SpanError::Invalid)?;
let part = |name: &str, unit: usize| match time_matches.name(name) {
Some(n) => usize::from_str(n.as_str())
.map_err(|e| cause(&e))?
.checked_mul(unit)
.ok_or(SpanError::TooLong),
None => Ok(0),
};
part("hours", 3600)?
.checked_add(part("minutes", 60)?)
.ok_or(SpanError::TooLong)?
.checked_add(part("seconds", 1)?)
.ok_or(SpanError::TooLong)?
}
};
trace!("{time_str} is {seconds} seconds");
seconds
.checked_mul(1_000)
.map(|_| seconds)
.ok_or(SpanError::TooLong)
}
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 parse_rate(rate: Option<String>) -> f32 {
get_float_from_string(rate).unwrap_or(1.0)
}
const MIN_RATE: f32 = 1.0 / 86_400.0;
pub(crate) fn below_one_user_a_day(rate: f32) -> bool {
rate < MIN_RATE
}
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...");
crate::logger::finish_goose_log();
std::process::exit(1);
} else {
warn!("caught ctrl-c, stopping...");
trigger_killswitch("SIGINT received");
}
}) {
Ok(_) => (),
Err(e) => {
reset_killswitch();
info!("reset ctrl-c handler: {e}");
}
}
}
pub(crate) fn warning_block(tag: &str, text: &str) {
let rule = "=".repeat(78);
eprintln!("{rule}\n{text}\n{rule}");
warn!("{tag}: {}", text.replace('\n', " "));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timespan() {
assert_eq!(parse_timespan("0"), Some(0));
assert_eq!(parse_timespan("foo"), None);
assert_eq!(parse_timespan("1"), Some(1));
assert_eq!(parse_timespan("1s"), Some(1));
assert_eq!(parse_timespan("1m"), Some(60));
assert_eq!(parse_timespan("61"), Some(61));
assert_eq!(parse_timespan("1m1s"), Some(61));
assert_eq!(parse_timespan("10m"), Some(600));
assert_eq!(parse_timespan("10m5s"), Some(605));
assert_eq!(parse_timespan("15mins"), None);
assert_eq!(parse_timespan("60m"), Some(3600));
assert_eq!(parse_timespan("1h"), Some(3600));
assert_eq!(parse_timespan("1h15s"), Some(3615));
assert_eq!(parse_timespan("1h5m"), Some(3900));
assert_eq!(parse_timespan("1h5m13s"), Some(3913));
assert_eq!(parse_timespan("2h3min"), None);
assert_eq!(parse_timespan("3h3m"), Some(10980));
assert_eq!(parse_timespan("3h3m5s"), Some(10985));
assert_eq!(parse_timespan("5hours"), None);
assert_eq!(parse_timespan("450m"), Some(27000));
assert_eq!(parse_timespan("24h"), Some(86400));
assert_eq!(parse_timespan("88h88m88s"), Some(322168));
assert_eq!(parse_timespan("100hourblah"), None);
assert!(matches!(
timespan_seconds("١h"),
Err(SpanError::ForeignDigit)
));
assert_eq!(parse_timespan("١h"), None);
assert_eq!(parse_timespan("1h١m"), None);
assert_eq!(parse_timespan("١"), None);
assert_eq!(parse_timespan("x١"), None);
assert_eq!(parse_timespan("1h2m٣"), None);
assert_eq!(parse_timespan("2h²"), None);
assert_eq!(parse_timespan("5124095576030431h100s"), None);
assert_eq!(parse_timespan("99999999999999999999"), None);
let longest = usize::MAX / 1_000;
assert_eq!(parse_timespan(&longest.to_string()), Some(longest));
assert_eq!(parse_timespan(&(longest + 1).to_string()), None);
}
#[test]
fn timespan_is_refused_unless_it_all_reads_as_one() {
for refused in ["5 junk", "5s junk", "junk", " 5s", "5s ", "1m1h"] {
assert!(
matches!(timespan_seconds(refused), Err(SpanError::Invalid)),
"{:?}",
refused
);
assert_eq!(parse_timespan(refused), None, "{:?}", refused);
}
for (span, seconds) in [
("5", 5),
("5s", 5),
("1h2m3s", 3_723),
("2m", 120),
("1H2M3S", 3_723),
] {
assert_eq!(parse_timespan(span), Some(seconds), "{:?}", span);
}
}
#[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 test_parse_rate() {
assert!((parse_rate(Some("1".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((parse_rate(Some("1.0".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((parse_rate(Some(".5".to_string())) - 0.5).abs() < f32::EPSILON);
assert!((parse_rate(Some("0.5".to_string())) - 0.5).abs() < f32::EPSILON);
assert!((parse_rate(Some(".12345".to_string())) - 0.12345).abs() < f32::EPSILON);
assert!((parse_rate(Some("12.345".to_string())) - 12.345).abs() < f32::EPSILON);
assert!((parse_rate(None) - 1.0).abs() < f32::EPSILON);
assert!((parse_rate(Some("g".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((parse_rate(Some("2.1f".to_string())) - 1.0).abs() < f32::EPSILON);
assert!((parse_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());
}
}