use crate::achievement::AchievementContext;
use crate::evaluator::{EvalResult, Evaluator};
use chrono::{LocalResult, NaiveTime, Timelike};
use std::fmt::Debug;
use std::ops::Add;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug)]
pub struct TimeEvaluator {
hour: Option<u32>,
minute: Option<u32>,
timezone: chrono_tz::Tz,
leeway: Duration,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("hour must be between 0 and 23")]
InvalidHour(u32),
#[error("minute must be between 0 and 59")]
InvalidMinute(u32),
}
type Result<T> = std::result::Result<T, Error>;
impl TimeEvaluator {
pub fn new(
hour: Option<u32>,
minute: Option<u32>,
timezone: chrono_tz::Tz,
leeway: Option<Duration>,
) -> Result<Self> {
if let Some(hour) = hour
&& hour > 23
{
return Err(Error::InvalidHour(hour));
}
if let Some(minute) = minute
&& minute > 59
{
return Err(Error::InvalidMinute(minute));
}
Ok(Self {
hour,
minute,
timezone,
leeway: leeway.unwrap_or_default(),
})
}
}
const MINUTE: Duration = Duration::from_secs(60);
const HOUR: Duration = Duration::from_secs(60 * 60);
impl<Player, Metadata, Trigger> Evaluator<Player, Metadata, Trigger> for TimeEvaluator {
fn evaluate(
&self,
ctx: &AchievementContext<Player, Metadata, Trigger>,
) -> impl Into<EvalResult> {
let now = ctx.current_bloop.recorded_at.with_timezone(&self.timezone);
let from = now.with_time(
NaiveTime::from_hms_opt(
self.hour.unwrap_or(now.hour()),
self.minute.unwrap_or(now.minute()),
0,
)
.unwrap(),
);
let (from, to) = match from {
LocalResult::Single(target_time) => (target_time, target_time),
LocalResult::Ambiguous(earliest, latest) => (earliest, latest),
LocalResult::None => return false,
};
let to = to.add(if self.minute.is_some() { MINUTE } else { HOUR });
now >= from && now < to + self.leeway
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bloop::Bloop;
use crate::evaluator::{EvalResult, Evaluator};
use crate::test_utils::{MockPlayer, TestCtxBuilder};
use chrono::{DateTime, TimeZone, Utc};
use chrono_tz::Europe::Berlin;
use std::time::Duration;
fn make_ctx_builder_for_time(dt: DateTime<Utc>) -> TestCtxBuilder<MockPlayer, (), ()> {
let (player, _) = MockPlayer::builder().build();
let bloop = Bloop::new(player.clone(), "client1", dt);
TestCtxBuilder::new(bloop)
}
#[test]
fn matches_exact_hour_minute() {
let time = Berlin
.with_ymd_and_hms(2024, 10, 1, 14, 30, 0)
.unwrap()
.with_timezone(&Utc);
let mut builder = make_ctx_builder_for_time(time);
let evaluator = TimeEvaluator::new(Some(14), Some(30), Berlin, None).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn within_leeway() {
let time = Berlin
.with_ymd_and_hms(2024, 10, 1, 14, 30, 30)
.unwrap()
.with_timezone(&Utc);
let mut builder = make_ctx_builder_for_time(time);
let evaluator =
TimeEvaluator::new(Some(14), Some(30), Berlin, Some(Duration::from_secs(60))).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn outside_leeway_fails() {
let time = Berlin
.with_ymd_and_hms(2024, 10, 1, 14, 32, 0)
.unwrap()
.with_timezone(&Utc);
let mut builder = make_ctx_builder_for_time(time);
let evaluator =
TimeEvaluator::new(Some(14), Some(30), Berlin, Some(Duration::from_secs(60))).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::NoAward
);
}
#[test]
fn with_only_hour() {
let time = Berlin
.with_ymd_and_hms(2024, 10, 1, 9, 15, 0)
.unwrap()
.with_timezone(&Utc);
let mut builder = make_ctx_builder_for_time(time);
let evaluator = TimeEvaluator::new(Some(9), None, Berlin, None).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn with_only_minute() {
let time = Berlin
.with_ymd_and_hms(2024, 10, 1, 22, 45, 0)
.unwrap()
.with_timezone(&Utc);
let mut builder = make_ctx_builder_for_time(time);
let evaluator = TimeEvaluator::new(None, Some(45), Berlin, None).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn dst_gap_returns_false() {
let time = Utc.with_ymd_and_hms(2024, 3, 31, 1, 30, 0).unwrap();
let mut builder = make_ctx_builder_for_time(time);
let evaluator = TimeEvaluator::new(Some(2), Some(30), Berlin, None).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::NoAward
);
}
#[test]
fn handles_ambiguous_earliest_time() {
let time = Utc.with_ymd_and_hms(2024, 10, 27, 0, 30, 0).unwrap();
let mut builder = make_ctx_builder_for_time(time);
let evaluator = TimeEvaluator::new(Some(2), Some(30), Berlin, None).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
#[test]
fn handles_ambiguous_latest_time() {
let time = Utc.with_ymd_and_hms(2024, 10, 27, 1, 30, 0).unwrap();
let mut builder = make_ctx_builder_for_time(time);
let evaluator = TimeEvaluator::new(Some(2), Some(30), Berlin, None).unwrap();
assert_eq!(
evaluator.evaluate(&builder.build()).into(),
EvalResult::AwardSelf
);
}
}