use std::fmt;
use std::time::{Duration, Instant};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Budget {
pub deadline: Option<Instant>,
pub time_limit: Option<Duration>,
pub max_pivots: Option<usize>,
}
impl Budget {
pub const fn unlimited() -> Self {
Budget {
deadline: None,
time_limit: None,
max_pivots: None,
}
}
pub const fn deadline(at: Instant) -> Self {
Budget {
deadline: Some(at),
time_limit: None,
max_pivots: None,
}
}
pub const fn max_pivots(n: usize) -> Self {
Budget {
deadline: None,
time_limit: None,
max_pivots: Some(n),
}
}
pub fn within(duration: Duration) -> Self {
Budget {
deadline: Instant::now().checked_add(duration),
time_limit: None,
max_pivots: None,
}
}
pub const fn time_limit(duration: Duration) -> Self {
Budget {
deadline: None,
time_limit: Some(duration),
max_pivots: None,
}
}
#[must_use]
pub fn with_deadline(mut self, at: Instant) -> Self {
self.deadline = Some(at);
self
}
#[must_use]
pub fn with_time_limit(mut self, limit: Duration) -> Self {
self.time_limit = Some(limit);
self
}
#[must_use]
pub fn with_max_pivots(mut self, n: usize) -> Self {
self.max_pivots = Some(n);
self
}
#[must_use]
pub fn with_max_steps(self, n: usize) -> Self {
self.with_max_pivots(n)
}
pub const fn is_unlimited(&self) -> bool {
self.deadline.is_none() && self.time_limit.is_none() && self.max_pivots.is_none()
}
pub fn deadline_from_now(&self) -> Option<Instant> {
deadline_from(self.deadline, self.time_limit)
}
#[must_use]
pub fn start(&self) -> Self {
Budget {
deadline: self.deadline_from_now(),
time_limit: None,
max_pivots: self.max_pivots,
}
}
pub fn deadline_passed(&self) -> bool {
deadline_passed(self.deadline)
}
pub fn exhausted(&self, spent: usize) -> Option<BudgetHit> {
if self.deadline_passed() {
return Some(BudgetHit::Deadline);
}
if self.max_pivots.is_some_and(|cap| spent >= cap) {
return Some(BudgetHit::MaxPivots);
}
None
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BudgetHit {
Deadline,
MaxPivots,
}
impl fmt::Display for BudgetHit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
BudgetHit::Deadline => "deadline",
BudgetHit::MaxPivots => "max_pivots",
})
}
}
pub fn deadline_from(deadline: Option<Instant>, time_limit: Option<Duration>) -> Option<Instant> {
let from_limit = time_limit.and_then(|t| Instant::now().checked_add(t));
match (deadline, from_limit) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pub fn deadline_passed(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|d| Instant::now() >= d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builders_and_defaults() {
assert!(Budget::default().is_unlimited());
assert_eq!(Budget::default(), Budget::unlimited());
let b = Budget::max_pivots(3).with_time_limit(Duration::from_secs(1));
assert_eq!(b.max_pivots, Some(3));
assert_eq!(b.time_limit, Some(Duration::from_secs(1)));
assert!(b.deadline.is_none() && !b.is_unlimited());
assert_eq!(Budget::max_pivots(3), Budget::default().with_max_steps(3));
}
#[test]
fn deadlines() {
let past = Instant::now() - Duration::from_secs(1);
assert!(Budget::deadline(past).deadline_passed());
assert_eq!(
Budget::deadline(past).exhausted(0),
Some(BudgetHit::Deadline)
);
assert!(!Budget::default().deadline_passed());
assert!(Budget::within(Duration::ZERO).deadline_passed());
let b = Budget::time_limit(Duration::ZERO);
assert!(!b.deadline_passed());
let started = b.start();
assert!(started.time_limit.is_none() && started.deadline_passed());
let far = Instant::now() + Duration::from_secs(3600);
let d = deadline_from(Some(far), Some(Duration::ZERO)).expect("some deadline");
assert!(d < far);
assert_eq!(deadline_from(Some(far), None), Some(far));
assert!(deadline_from(None, None).is_none());
assert!(!deadline_passed(None));
assert!(deadline_from(None, Some(Duration::MAX)).is_none());
}
#[test]
fn step_cap() {
let b = Budget::max_pivots(2);
assert_eq!(b.exhausted(0), None);
assert_eq!(b.exhausted(1), None);
assert_eq!(b.exhausted(2), Some(BudgetHit::MaxPivots));
assert_eq!(BudgetHit::MaxPivots.to_string(), "max_pivots");
assert_eq!(BudgetHit::Deadline.to_string(), "deadline");
}
}