use std::hash::{Hash, Hasher};
use fundu_core::time::TimeUnitsLike;
use crate::TimeUnit::*;
use crate::{
Multiplier, TimeUnit, DEFAULT_ID_DAY, DEFAULT_ID_HOUR, DEFAULT_ID_MICRO_SECOND,
DEFAULT_ID_MILLI_SECOND, DEFAULT_ID_MINUTE, DEFAULT_ID_MONTH, DEFAULT_ID_NANO_SECOND,
DEFAULT_ID_SECOND, DEFAULT_ID_WEEK, DEFAULT_ID_YEAR,
};
pub const SYSTEMD_TIME_UNITS: [CustomTimeUnit<'static>; 10] = [
CustomTimeUnit::with_default(NanoSecond, &["ns", "nsec"]),
CustomTimeUnit::with_default(MicroSecond, &["us", "µs", "usec"]),
CustomTimeUnit::with_default(MilliSecond, &["ms", "msec"]),
CustomTimeUnit::with_default(Second, &["s", "sec", "second", "seconds"]),
CustomTimeUnit::with_default(Minute, &["m", "min", "minute", "minutes"]),
CustomTimeUnit::with_default(Hour, &["h", "hr", "hour", "hours"]),
CustomTimeUnit::with_default(Day, &["d", "day", "days"]),
CustomTimeUnit::with_default(Week, &["w", "week", "weeks"]),
CustomTimeUnit::with_default(Month, &["M", "month", "months"]),
CustomTimeUnit::with_default(Year, &["y", "year", "years"]),
];
pub const DEFAULT_TIME_UNITS: [CustomTimeUnit<'static>; 8] = [
CustomTimeUnit::with_default(NanoSecond, &[DEFAULT_ID_NANO_SECOND]),
CustomTimeUnit::with_default(MicroSecond, &[DEFAULT_ID_MICRO_SECOND]),
CustomTimeUnit::with_default(MilliSecond, &[DEFAULT_ID_MILLI_SECOND]),
CustomTimeUnit::with_default(Second, &[DEFAULT_ID_SECOND]),
CustomTimeUnit::with_default(Minute, &[DEFAULT_ID_MINUTE]),
CustomTimeUnit::with_default(Hour, &[DEFAULT_ID_HOUR]),
CustomTimeUnit::with_default(Day, &[DEFAULT_ID_DAY]),
CustomTimeUnit::with_default(Week, &[DEFAULT_ID_WEEK]),
];
pub const DEFAULT_ALL_TIME_UNITS: [CustomTimeUnit<'static>; 10] = [
CustomTimeUnit::with_default(NanoSecond, &[DEFAULT_ID_NANO_SECOND]),
CustomTimeUnit::with_default(MicroSecond, &[DEFAULT_ID_MICRO_SECOND]),
CustomTimeUnit::with_default(MilliSecond, &[DEFAULT_ID_MILLI_SECOND]),
CustomTimeUnit::with_default(Second, &[DEFAULT_ID_SECOND]),
CustomTimeUnit::with_default(Minute, &[DEFAULT_ID_MINUTE]),
CustomTimeUnit::with_default(Hour, &[DEFAULT_ID_HOUR]),
CustomTimeUnit::with_default(Day, &[DEFAULT_ID_DAY]),
CustomTimeUnit::with_default(Week, &[DEFAULT_ID_WEEK]),
CustomTimeUnit::with_default(Month, &[DEFAULT_ID_MONTH]),
CustomTimeUnit::with_default(Year, &[DEFAULT_ID_YEAR]),
];
pub(super) type IdentifiersLookupData<'a> = (LookupData, Vec<&'a str>);
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(super) struct LookupData {
min_length: usize,
max_length: usize,
time_unit: TimeUnit,
multiplier: Multiplier,
}
impl LookupData {
const fn new(time_unit: TimeUnit, multiplier: Multiplier) -> Self {
Self {
min_length: usize::MAX,
max_length: 0,
time_unit,
multiplier,
}
}
fn update(&mut self, identifier: &str) {
let len = identifier.len();
if self.min_length > len {
self.min_length = len;
}
if self.max_length < len {
self.max_length = len;
}
}
const fn check(&self, identifier: &str) -> bool {
let len = identifier.len();
self.min_length <= len && self.max_length >= len
}
}
#[derive(Debug, Eq, Clone, Copy)]
pub struct CustomTimeUnit<'a> {
pub(super) base_unit: TimeUnit,
pub(super) multiplier: Multiplier,
pub(super) identifiers: &'a [&'a str],
}
impl<'a> CustomTimeUnit<'a> {
pub const fn new(
base_unit: TimeUnit,
identifiers: &'a [&'a str],
multiplier: Option<Multiplier>,
) -> Self {
Self {
base_unit,
multiplier: match multiplier {
Some(m) => {
assert!(
base_unit.multiplier().checked_mul(m).is_some(),
"The time unit multiplier multiplied with the multiplier parameter may \
not overflow"
);
m
}
None => Multiplier(1, 0),
},
identifiers,
}
}
pub const fn with_default(base_unit: TimeUnit, identifiers: &'a [&'a str]) -> Self {
Self::new(base_unit, identifiers, None)
}
}
impl<'a> PartialEq for CustomTimeUnit<'a> {
fn eq(&self, other: &Self) -> bool {
self.base_unit == other.base_unit && self.multiplier == other.multiplier
}
}
impl<'a> Hash for CustomTimeUnit<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.base_unit.hash(state);
self.multiplier.hash(state);
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub(super) struct CustomTimeUnits<'a> {
min_length: usize,
max_length: usize,
time_units: Vec<IdentifiersLookupData<'a>>,
}
impl<'a> CustomTimeUnits<'a> {
pub(super) fn new() -> Self {
Self::with_capacity(0)
}
pub(super) fn with_time_units(units: &[CustomTimeUnit<'a>]) -> Self {
let mut time_units = Self::with_capacity(units.len());
for unit in units {
time_units.add_custom_time_unit(*unit);
}
time_units
}
pub(super) fn with_keywords(keywords: &[TimeKeyword<'a>]) -> Self {
let mut time_units = Self::with_capacity(keywords.len());
for keyword in keywords {
time_units.add_custom_time_unit(keyword.to_custom_time_unit());
}
time_units
}
pub(super) fn with_capacity(capacity: usize) -> Self {
Self {
min_length: usize::MAX,
max_length: 0,
time_units: Vec::with_capacity(capacity),
}
}
pub(super) fn add_custom_time_unit(&mut self, time_unit: CustomTimeUnit<'a>) {
if time_unit.identifiers.is_empty() {
return;
}
let CustomTimeUnit {
base_unit,
multiplier,
identifiers,
} = time_unit;
let (min_length, max_length) =
if let Some((data, ids)) = self.lookup_mut(base_unit, multiplier) {
for &identifier in identifiers.iter().filter(|&&id| !id.is_empty()) {
ids.push(identifier);
data.update(identifier);
}
(data.min_length, data.max_length)
} else {
let mut data = LookupData::new(base_unit, multiplier);
let mut ids = Vec::with_capacity(identifiers.len());
for &identifier in identifiers.iter().filter(|&&id| !id.is_empty()) {
ids.push(identifier);
data.update(identifier);
}
if ids.is_empty() {
return;
}
let lengths = (data.min_length, data.max_length);
self.time_units.push((data, ids));
lengths
};
self.update_lengths(min_length, max_length);
}
pub(super) fn lookup_mut(
&'_ mut self,
unit: TimeUnit,
multiplier: Multiplier,
) -> Option<&'_ mut (LookupData, Vec<&'a str>)> {
self.time_units
.iter_mut()
.find(|(data, _)| data.time_unit == unit && data.multiplier == multiplier)
}
#[allow(dead_code)]
pub(super) fn lookup(
&self,
unit: TimeUnit,
multiplier: Multiplier,
) -> Option<&(LookupData, Vec<&'a str>)> {
self.time_units
.iter()
.find(|(data, _)| data.time_unit == unit && data.multiplier == multiplier)
}
pub(super) fn find_id(&self, id: &str) -> Option<(TimeUnit, Multiplier)> {
self.time_units.iter().find_map(|(data, v)| {
(data.check(id) && v.contains(&id)).then_some((data.time_unit, data.multiplier))
})
}
pub(super) fn update_lengths(&mut self, min_length: usize, max_length: usize) {
if self.min_length > min_length {
self.min_length = min_length;
}
if self.max_length < max_length {
self.max_length = max_length;
}
}
}
impl<'a> TimeUnitsLike for CustomTimeUnits<'a> {
#[inline]
fn is_empty(&self) -> bool {
self.time_units.is_empty()
}
#[inline]
fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
let len = identifier.len();
if self.min_length > len || self.max_length < len {
return None;
}
self.find_id(identifier)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct TimeKeyword<'a> {
time_unit: CustomTimeUnit<'a>,
}
impl<'a> TimeKeyword<'a> {
pub const fn new(
base_unit: TimeUnit,
identifiers: &'a [&'a str],
multiplier: Option<Multiplier>,
) -> Self {
Self {
time_unit: CustomTimeUnit::new(base_unit, identifiers, multiplier),
}
}
pub(super) const fn to_custom_time_unit(self) -> CustomTimeUnit<'a> {
self.time_unit
}
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::DefaultHasher;
use rstest::rstest;
use super::*;
fn make_lookup_result(
min_length: usize,
max_length: usize,
time_unit: TimeUnit,
multiplier: Multiplier,
identifiers: Vec<&str>,
) -> (LookupData, Vec<&str>) {
(
LookupData {
min_length,
max_length,
time_unit,
multiplier,
},
identifiers,
)
}
#[test]
fn test_custom_time_units_init_new() {
let custom = CustomTimeUnits::new();
assert!(custom.time_units.is_empty());
assert!(custom.is_empty());
}
#[rstest]
#[case::nano_second(CustomTimeUnit::with_default(NanoSecond, &["some"]), 4, 4)]
#[case::nano_second_with_multiple_ids(
CustomTimeUnit::with_default(NanoSecond, &["some", "other", "деякі"]), 4, 10
)]
#[case::micro_second(
CustomTimeUnit::with_default(MicroSecond, &["some"]), 4, 4
)]
#[case::micro_second_with_multiple_ids(
CustomTimeUnit::with_default(MicroSecond, &["some", "other", "деякі"]), 4, 10
)]
#[case::milli_second(
CustomTimeUnit::with_default(MilliSecond, &["some"]), 4, 4
)]
#[case::milli_second_with_multiple_ids(
CustomTimeUnit::with_default(MilliSecond, &["some", "other", "деякі"]), 4, 10
)]
#[case::second(
CustomTimeUnit::with_default(Second, &["some"]), 4, 4
)]
#[case::second_with_multiple_ids(
CustomTimeUnit::with_default(Second, &["some", "other", "деякі"]), 4, 10
)]
#[case::minute(
CustomTimeUnit::with_default(Minute, &["some"]), 4, 4
)]
#[case::minute_with_multiple_ids(
CustomTimeUnit::with_default(Minute, &["some", "other", "деякі"]), 4, 10
)]
#[case::hour(
CustomTimeUnit::with_default(Hour, &["some"]), 4, 4
)]
#[case::hour_with_multiple_ids(
CustomTimeUnit::with_default(Hour, &["some", "other", "деякі"]), 4, 10
)]
#[case::day(
CustomTimeUnit::with_default(Day, &["some"]), 4, 4
)]
#[case::day_with_multiple_ids(
CustomTimeUnit::with_default(Day, &["some", "other", "деякі"]), 4, 10
)]
#[case::week(
CustomTimeUnit::with_default(Week, &["some"]), 4, 4
)]
#[case::week_with_multiple_ids(
CustomTimeUnit::with_default(Week, &["some", "other", "деякі"]), 4, 10
)]
#[case::month(
CustomTimeUnit::with_default(Month, &["some"]), 4, 4
)]
#[case::month_with_multiple_ids(
CustomTimeUnit::with_default(Month, &["some", "other", "деякі"]), 4, 10
)]
#[case::year(
CustomTimeUnit::with_default(Year, &["some"]), 4, 4
)]
#[case::year_with_multiple_ids(
CustomTimeUnit::with_default(Year, &["some", "other", "деякі"]), 4, 10
)]
fn test_custom_time_units_init_with_time_units(
#[case] time_unit: CustomTimeUnit,
#[case] min_length: usize,
#[case] max_length: usize,
) {
let custom = CustomTimeUnits::with_time_units(&[(time_unit)]);
assert!(!custom.is_empty());
assert_eq!(
custom.lookup(time_unit.base_unit, Multiplier::default()),
Some(&make_lookup_result(
min_length,
max_length,
time_unit.base_unit,
Multiplier::default(),
Vec::from(time_unit.identifiers)
))
);
}
#[test]
fn test_custom_time_units_init_with_time_units_when_multiple_equal_ids() {
let custom = CustomTimeUnits::with_time_units(&[(CustomTimeUnit::with_default(
NanoSecond,
&["same", "same"],
))]);
assert!(!custom.is_empty());
assert_eq!(
custom.lookup(NanoSecond, Multiplier::default()),
Some(&make_lookup_result(
4,
4,
NanoSecond,
Multiplier::default(),
vec!["same", "same"]
))
);
assert_eq!(
custom.get("same"),
Some((NanoSecond, Multiplier::default()))
);
}
#[test]
fn test_custom_time_units_when_add_custom_time_unit() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::with_default(MicroSecond, &["some", "ids"]));
assert!(
custom
.time_units
.iter()
.filter(|(data, _)| data.time_unit != MicroSecond)
.all(|(_, v)| v.is_empty())
);
assert_eq!(
custom.lookup(MicroSecond, Multiplier::default()).unwrap().1,
vec!["some", "ids"]
);
assert_eq!(
custom.get("some"),
Some((MicroSecond, Multiplier::default()))
);
assert_eq!(
custom.get("ids"),
Some((MicroSecond, Multiplier::default()))
);
assert_eq!(custom.get("does not exist"), None);
assert!(!custom.is_empty());
}
#[test]
fn test_custom_time_units_when_adding_time_unit_with_empty_slice_then_not_added() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::with_default(MicroSecond, &[]));
assert!(custom.is_empty());
assert_eq!(custom.lookup(MicroSecond, Multiplier::default()), None);
}
#[test]
fn test_custom_time_units_when_adding_time_unit_with_empty_id_then_not_added() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::with_default(MicroSecond, &[""]));
assert!(custom.is_empty());
assert_eq!(custom.lookup(MicroSecond, Multiplier::default()), None);
}
#[test]
fn test_custom_time_units_adding_custom_time_unit_with_empty_id_then_not_added() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::new(Second, &[""], Some(Multiplier(2, 0))));
assert!(custom.is_empty());
}
#[test]
fn test_custom_time_units_adding_custom_time_unit() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::new(
Second,
&["sec"],
Some(Multiplier(2, 0)),
));
assert!(!custom.is_empty());
assert_eq!(
custom.lookup(Second, Multiplier(2, 0)),
Some(&make_lookup_result(
3,
3,
Second,
Multiplier(2, 0),
vec!["sec"]
))
);
assert_eq!(custom.get("sec"), Some((Second, Multiplier(2, 0))));
}
#[test]
fn test_custom_time_units_adding_multiple_custom_time_unit() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::new(
Second,
&["sec"],
Some(Multiplier(1, 0)),
));
custom.add_custom_time_unit(CustomTimeUnit::new(
Second,
&["secs"],
Some(Multiplier(2, 0)),
));
assert_eq!(
custom.lookup(Second, Multiplier::default()),
Some(&make_lookup_result(
3,
3,
Second,
Multiplier::default(),
vec!["sec"]
))
);
assert_eq!(
custom.lookup(Second, Multiplier(2, 0)),
Some(&make_lookup_result(
4,
4,
Second,
Multiplier(2, 0),
vec!["secs"]
))
);
assert_eq!(custom.get("sec"), Some((Second, Multiplier(1, 0))));
assert_eq!(custom.get("secs"), Some((Second, Multiplier(2, 0))));
}
#[test]
fn test_custom_time_units_adding_custom_time_unit_when_normal_time_unit_already_exists() {
let mut custom =
CustomTimeUnits::with_time_units(&[CustomTimeUnit::with_default(Second, &["s"])]);
custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["ss"], Some(Multiplier(2, 0))));
assert_eq!(
custom.lookup(Second, Multiplier::default()),
Some(&make_lookup_result(
1,
1,
Second,
Multiplier::default(),
vec!["s"]
))
);
assert_eq!(
custom.lookup(Second, Multiplier(2, 0)),
Some(&make_lookup_result(
2,
2,
Second,
Multiplier(2, 0),
vec!["ss"]
))
);
assert_eq!(custom.get("s"), Some((Second, Multiplier(1, 0))));
assert_eq!(custom.get("ss"), Some((Second, Multiplier(2, 0))));
}
#[test]
fn test_custom_time_units_adding_custom_time_unit_when_normal_time_unit_with_same_id() {
let mut custom =
CustomTimeUnits::with_time_units(&[CustomTimeUnit::with_default(Second, &["s"])]);
custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2, 0))));
assert_eq!(
custom.lookup(Second, Multiplier::default()),
Some(&make_lookup_result(
1,
1,
Second,
Multiplier::default(),
vec!["s"]
))
);
assert_eq!(
custom.lookup(Second, Multiplier(2, 0)),
Some(&make_lookup_result(
1,
1,
Second,
Multiplier(2, 0),
vec!["s"]
))
);
assert_eq!(custom.get("s"), Some((Second, Multiplier(1, 0))));
}
#[test]
fn test_custom_time_units_adding_custom_time_unit_when_identifiers_is_empty() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::new(Second, &[], Some(Multiplier(2, 0))));
assert!(custom.is_empty());
assert_eq!(custom.lookup(Second, Multiplier(2, 0)), None);
}
#[test]
fn test_custom_time_units_adding_custom_time_unit_when_adding_same_unit_twice() {
let mut custom = CustomTimeUnits::new();
custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2, 0))));
custom.add_custom_time_unit(CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2, 0))));
assert_eq!(
custom.lookup(Second, Multiplier(2, 0)),
Some(&make_lookup_result(
1,
1,
Second,
Multiplier(2, 0),
vec!["s", "s"]
))
);
}
#[test]
#[should_panic = "The time unit multiplier multiplied with the multiplier parameter may not \
overflow"]
fn test_custom_time_unit_new_when_overflow_then_panic() {
CustomTimeUnit::new(Year, &["year"], Some(Multiplier(i64::MAX, 0)));
}
#[rstest]
#[case::none_and_default_multiplier_when_same_ids(
CustomTimeUnit::new(Second, &["s"], None),
CustomTimeUnit::new(Second, &["s"], Some(Multiplier::default()))
)]
#[case::none_and_default_multiplier_when_different_ids(
CustomTimeUnit::new(Second, &["s"], None),
CustomTimeUnit::new(Second, &["secs"], Some(Multiplier::default()))
)]
#[case::same_multipliers(
CustomTimeUnit::new(Second, &["s"], Some(Multiplier(2,0))),
CustomTimeUnit::new(Second, &["secs"], Some(Multiplier(2,0)))
)]
fn test_custom_time_unit_equality_when_equal(
#[case] time_unit: CustomTimeUnit,
#[case] other: CustomTimeUnit,
) {
assert_eq!(time_unit, other);
}
#[rstest]
#[case::different_time_units(
CustomTimeUnit::new(MilliSecond, &["ms"], Some(Multiplier(1,0))),
CustomTimeUnit::new(Second, &["ms"], Some(Multiplier(1,0)))
)]
#[case::different_multipliers(
CustomTimeUnit::new(MilliSecond, &["ms"], Some(Multiplier(1000,0))),
CustomTimeUnit::new(Second, &["ms"], Some(Multiplier(1, 0)))
)]
fn test_custom_time_unit_equality_when_not_equal(
#[case] time_unit: CustomTimeUnit,
#[case] other: CustomTimeUnit,
) {
assert_ne!(time_unit, other);
}
#[rstest]
#[case::second(
CustomTimeUnit::new(Second, &["some"], Some(Multiplier(1, 0))),
CustomTimeUnit::new(Second, &["other"], Some(Multiplier(1, 0)))
)]
#[case::day(
CustomTimeUnit::new(Day, &["some"], None),
CustomTimeUnit::with_default(Day, &["other"])
)]
#[case::day_with_multiplier(
CustomTimeUnit::new(Day, &["some"], Some(Multiplier(123, 4))),
CustomTimeUnit::new(Day, &["other"], Some(Multiplier(123, 4)))
)]
fn test_custom_time_unit_hash_when_equal(
#[case] time_unit: CustomTimeUnit,
#[case] other: CustomTimeUnit,
) {
assert_eq!(time_unit, other);
assert_eq!(other, time_unit);
let mut hasher = DefaultHasher::new();
time_unit.hash(&mut hasher);
let mut other_hasher = DefaultHasher::new();
other.hash(&mut other_hasher);
assert_eq!(hasher.finish(), other_hasher.finish());
}
}