bach 0.1.2

Discrete-event simulation environment for async workflows
Documentation
use core::{fmt, ops};

mod bitset;
mod entry;
pub mod error;
pub mod scheduler;
mod stack;
mod timeout;
mod wheel;

pub use core::time::Duration;
pub use scheduler::Timer as Sleep;
pub use timeout::*;

pub fn sleep(duration: Duration) -> Sleep {
    measure!("sleep", duration);
    scheduler::scope::borrow_with(|handle| {
        let ticks = resolution::duration_to_ticks(duration);
        handle.delay(ticks)
    })
}

pub use self::sleep as delay;

pub fn sleep_until(target: Instant) -> Sleep {
    let now = Instant::now();
    let duration = target.0.saturating_sub(now.0);
    sleep(duration)
}

#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct Instant(Duration);

impl Instant {
    pub fn now() -> Self {
        scheduler::scope::borrow_with(|v| v.now())
    }

    pub fn try_now() -> Option<Self> {
        scheduler::scope::try_borrow_with(|v| v.as_ref().map(|v| v.now()))
    }

    pub fn elapsed(self) -> Duration {
        Self::now().0 - self.0
    }

    pub fn try_elapsed(self) -> Option<Duration> {
        let now = Self::try_now()?;
        Some(now.0 - self.0)
    }

    pub fn elapsed_since_start(self) -> Duration {
        self.0
    }

    pub fn has_elapsed(&self) -> bool {
        Self::now().ge(self)
    }

    pub fn saturating_duration_since(self, earlier: Instant) -> Duration {
        self.0.saturating_sub(earlier.0)
    }

    pub(crate) fn from_ticks(ticks: u64) -> Self {
        let duration = resolution::ticks_to_duration(ticks);
        Self(duration)
    }

    pub const fn zero() -> Self {
        Self(Duration::ZERO)
    }
}

impl ops::Add<Duration> for Instant {
    type Output = Instant;

    fn add(self, rhs: Duration) -> Self::Output {
        Self(self.0 + rhs)
    }
}

impl ops::AddAssign<Duration> for Instant {
    fn add_assign(&mut self, rhs: Duration) {
        self.0 += rhs;
    }
}

impl fmt::Debug for Instant {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("Instant")
            .field(&format_args!("{self:#}"))
            .finish()
    }
}

impl fmt::Display for Instant {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let duration = self.elapsed_since_start();
        let nanos = duration.subsec_nanos();
        let secs = duration.as_secs() % 60;
        let mins = duration.as_secs() / 60 % 60;
        let hours = duration.as_secs() / 60 / 60;
        if f.alternate() {
            let days = duration.as_secs() / 86400;

            // Calculate how many trailing zeros to remove without allocation
            let mut trimmed_nanos = nanos;
            let mut width = 9u32;
            while width > 0 && trimmed_nanos % 10 == 0 {
                trimmed_nanos /= 10;
                width -= 1;
            }

            match (days, hours, mins) {
                (0, 0, 0) if width == 0 => write!(f, "{secs}"),
                (0, 0, 0) => write!(f, "{secs}.{trimmed_nanos:0width$}", width = width as usize),
                (0, 0, _) if width == 0 => write!(f, "{mins}:{secs:02}"),
                (0, 0, _) => write!(
                    f,
                    "{mins}:{secs:02}.{trimmed_nanos:0width$}",
                    width = width as usize
                ),
                (0, _, _) if width == 0 => write!(f, "{hours}:{mins:02}:{secs:02}"),
                (0, _, _) => write!(
                    f,
                    "{hours}:{mins:02}:{secs:02}.{trimmed_nanos:0width$}",
                    width = width as usize
                ),
                (_, _, _) if width == 0 => write!(f, "{days}:{hours}:{mins:02}:{secs:02}"),
                (_, _, _) => write!(
                    f,
                    "{days}:{hours}:{mins:02}:{secs:02}.{trimmed_nanos:0width$}",
                    width = width as usize
                ),
            }
        } else {
            write!(f, "{hours}:{mins:02}:{secs:02}.{nanos:09}")
        }
    }
}

pub use resolution::{tick_duration, with_tick_duration};

pub(crate) mod resolution {
    use core::time::Duration;
    crate::scope::define!(scope, Duration);

    pub fn tick_duration() -> Duration {
        scope::try_borrow_with(|v| v.unwrap_or_else(|| Duration::from_nanos(1)))
    }

    pub use scope::with as with_tick_duration;

    pub fn ticks_to_duration(ticks: u64) -> Duration {
        let nanos_per_tick = tick_duration().as_nanos() as u64;

        let nanos = nanos_per_tick * ticks;
        Duration::from_nanos(nanos)
    }

    pub fn duration_to_ticks(duration: Duration) -> u64 {
        let nanos = duration.as_nanos();
        let nanos_per_tick = tick_duration().as_nanos();
        let ticks = nanos / nanos_per_tick;
        ticks as u64
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn instant_display_alternate_format() {
        // Test case from the issue: 1 second and 1000 nanos
        // With trailing zeros truncated, this should be clear and unambiguous
        let instant = Instant(Duration::new(1, 1000));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1.000001",
            "1 second + 1000 nanos should show as 1.000001 (trailing zeros truncated)"
        );

        // Test 0.5 seconds (500 million nanos)
        let instant = Instant(Duration::new(0, 500_000_000));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "0.5",
            "0.5 seconds should display with trailing zeros truncated"
        );

        // Test 1 second exactly (no nanos, so no decimal point)
        let instant = Instant(Duration::new(1, 0));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1",
            "1 second exactly should show without decimal point"
        );

        // Test just nanoseconds
        let instant = Instant(Duration::new(0, 1000));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "0.000001",
            "1000 nanos should display with trailing zeros truncated"
        );

        // Test with minutes and seconds (1 minute + 5 seconds)
        let instant = Instant(Duration::new(60 + 5, 123_456_789));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1:05.123456789",
            "65.123456789 seconds should format as minutes:seconds.nanos"
        );

        // Test with minutes and seconds (no nanos)
        let instant = Instant(Duration::new(60 + 5, 0));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1:05",
            "65 seconds exactly should not show decimal point"
        );

        // Test with hours (1 hour + 1 minute + 1 second)
        let instant = Instant(Duration::new(60 * 60 + 60 + 1, 999_999_999));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1:01:01.999999999",
            "3661.999999999 seconds should format as hours:minutes:seconds.nanos"
        );

        // Test with hours (no nanos)
        let instant = Instant(Duration::new(60 * 60 + 60 + 1, 0));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1:01:01",
            "3661 seconds exactly should not show decimal point"
        );

        // Test with days (1 day + 1 hour + 1 minute + 1 second)
        const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
        let instant = Instant(Duration::new(SECONDS_PER_DAY + 60 * 60 + 60 + 1, 1));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1:25:01:01.000000001",
            "1 day + 3661.000000001 seconds should include days"
        );

        // Test with days (no nanos)
        let instant = Instant(Duration::new(SECONDS_PER_DAY + 60 * 60 + 60 + 1, 0));
        let display = format!("{instant:#}");
        assert_eq!(
            display, "1:25:01:01",
            "1 day + 3661 seconds exactly should not show decimal point"
        );
    }

    #[test]
    fn instant_display_regular_format() {
        // Verify non-alternate format still works correctly
        let instant = Instant(Duration::new(1, 1000));
        let display = format!("{instant}");
        assert_eq!(
            display, "0:00:01.000001000",
            "Regular format should show hours:minutes:seconds.nanos"
        );

        let instant = Instant(Duration::new(3661, 123_456_789));
        let display = format!("{instant}");
        assert_eq!(
            display, "1:01:01.123456789",
            "Regular format with hours should work"
        );
    }
}