greg 0.2.5

Simple Unobtrusive Date & Time library
Documentation
use std::fmt;
use std::str::FromStr;

use crate::Span;
use super::{
	Time,
	ParseError
};

impl Time {
	/// The beginning of the day, `00:00:00`
	pub const MIDNIGHT: Self = Self {h: 0, m: 0, s: 0};

	/// Construct from hours, minutes and seconds and assert [`Self::is_valid`]
	///
	///```compile_fail
	/// use greg::calendar::Time;
	/// const INVALID: Time = Time::hms_checked(24, 0, 0);
	///```
	pub const fn hms_checked(h: u8, m: u8, s: u8) -> Self {
		let new = Self {h, m, s};
		if new.is_valid() {return new};

		const MSG: &[u8] = b"invalid Time: ";
		let mut msg = MSG;
		let mut buf = [0u8; MSG.len() + "___:___:___".len()];
		let mut i = 0;
		while let [b, rest @ ..] = msg {
			buf[i] = *b;
			msg = rest;
			i += 1;
		}

		const fn push_n<const N: usize>(n: u8, mut buf: [u8; N], mut i: usize)
			-> ([u8; N], usize)
		{
			if n >= 100 {
				buf[i] = (n / 100) + b'0';
				i += 1;
			}
			buf[i] = ((n % 100) / 10) + b'0';
			i += 1;
			buf[i] = (n % 10) + b'0';
			i += 1;
			(buf, i)
		}

		let (mut buf, mut i) = push_n(h, buf, i);
		buf[i] = b':';
		i += 1;
		let (mut buf, mut i) = push_n(m, buf, i);
		buf[i] = b':';
		i += 1;
		let (buf, i) = push_n(s, buf, i);
		let buf = buf.split_at(i).0;
		let Ok(msg) = std::str::from_utf8(buf) else {
			panic!("invalid Time and failed to format it");
		};

		panic!("{}", msg)
	}
	/// Checks whether fields are in the valid range
	///
	/// Does not allow for a leap second, `self.s` must be in `0..=59` to be valid.
	pub const fn is_valid(self) -> bool {
		(self.h < 24)
		& (self.m < 60)
		& (self.s < 60)
	}
	/// Break count of total seconds down into hours, minutes and seconds
	///
	/// Will produce an invalid [`Time`] if `seconds >= 86_400`.
	pub(crate) const fn from_seconds(seconds: u32) -> Self {
		let h = (seconds / (60 * 60)) as u8;
		let m = (seconds / 60 % 60) as u8;
		let s = (seconds % 60) as u8;
		Self {h, m, s}
	}
	/// Seconds since midnight
	pub const fn as_seconds(self) -> u32 {
		self.h as u32 * 60 * 60
		+ self.m as u32 * 60
		+ self.s as u32
	}
	/// Duration since midnight
	pub const fn as_span(self) -> Span {
		Span::from_seconds(self.as_seconds() as u64)
	}
}

impl fmt::Display for Time {
	/// Formats the [`Time`] as `hh:mm:ss` (i.e. `12:34:56`)
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		let Self {h, m, s} = self;
		write!(f, "{h:02}:{m:02}:{s:02}")
	}
}
impl fmt::Debug for Time {
	/// Same as [`Display`](fmt::Display): `hh:mm:ss` (i.e. `12:34:56`)
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		fmt::Display::fmt(self, f)
	}
}

impl Time {
	/// Try to parse Time as `hh:mm:ss` or `hh:mm`
	///
	/// Does not accept strings with too much or too little 0-padding.
	///
	///```
	/// use greg::calendar::Time;
	/// assert_eq!(Time::try_parse("00:00:00"), Ok(Time::hms_checked(0, 0, 0)));
	/// assert_eq!(Time::try_parse("10:15"), Ok(Time::hms_checked(10, 15, 0)));
	/// assert_eq!(
	///     Time::try_parse("12:34:56"),
	///     Ok(Time::hms_checked(12, 34, 56))
	/// );
	/// // also implemented via FromStr
	/// assert_eq!(
	///     "23:59:59".parse::<Time>(),
	///     Ok(Time::hms_checked(23, 59, 59))
	/// );
	///
	/// use greg::calendar::ParseError;
	///
	/// assert_eq!(Time::try_parse("2021-01-01"), Err(ParseError::Format));
	/// assert_eq!(Time::try_parse("24:00:00"), Err(ParseError::Invalid));
	/// assert_eq!(Time::try_parse("23:1:0"), Err(ParseError::Format));
	/// // no leap seconds
	/// assert!("23:59:60".parse::<Time>().is_err());
	/// // no sub-second precision
	/// assert!("12:34:56.789".parse::<Time>().is_err());
	/// assert!("6pm".parse::<Time>().is_err());
	///```
	pub const fn try_parse(from: &str) -> Result<Self, ParseError> {
		if from.is_empty() {return Err(ParseError::Empty)}
		let bytes = from.as_bytes();
		let [h1 @ b'0'..=b'9', h2 @ b'0'..=b'9', b':', rest @ ..] = bytes else {
			return Err(ParseError::Format);
		};
		let [m1 @ b'0'..=b'9', m2 @ b'0'..=b'9', rest @ ..] = rest else {
			return Err(ParseError::Format);
		};
		const fn digits(a: u8, b: u8) -> u8 {(a - b'0') * 10 + (b - b'0')}
		let h = digits(*h1, *h2);
		let m = digits(*m1, *m2);
		let s = match rest {
			[b':', s1 @ b'0'..=b'9', s2 @ b'0'..=b'9'] => digits(*s1, *s2),
			[] => 0,
			_ => return Err(ParseError::Format)
		};
		let time = Self {h, m, s};
		if time.is_valid() {Ok(time)} else {Err(ParseError::Invalid)}
	}
	/// Try to parse Time as `hh:mm:ss` or `hh:mm` and panic if invalid
	///
	///```
	/// use greg::calendar::Time;
	/// const NOON_ISH: Time = Time::parse("12:34:56");
	///
	/// assert_eq!(Time::parse("00:00:00"), Time::MIDNIGHT);
	/// assert_eq!(Time::parse("10:15"), Time::hms_checked(10, 15, 0));
	///```
	///
	/// This is mainly useful in `const` contexts, since the panic gets caught at compile-time.
	///
	///```compile_fail
	/// use greg::calendar::Time;
	/// const MORNING: Time = Time::parse("06;30"); // typo: ';' instead of ':'
	///```
	#[must_use]
	pub const fn parse(from: &str) -> Self {
		match Self::try_parse(from) {
			Ok(time) => time,
			Err(err) => panic!("{}", err.as_str())
		}
	}
}

impl FromStr for Time {
	type Err = ParseError;
	/// Parse `hh:mm:ss` or `hh:mm` time
	///
	/// Does not accept strings with too much or too little 0-padding.
	///
	///```
	/// use greg::calendar::Time;
	/// assert_eq!("00:00:00".parse(), Ok(Time::hms_checked(0, 0, 0)));
	/// assert_eq!("10:15".parse(), Ok(Time::hms_checked(10, 15, 0)));
	/// assert_eq!("12:34:56".parse(), Ok(Time::hms_checked(12, 34, 56)));
	/// assert_eq!("23:59:59".parse(), Ok(Time::hms_checked(23, 59, 59)));
	///
	/// assert!("2021-01-01".parse::<Time>().is_err());
	/// assert!("24:00:00".parse::<Time>().is_err());
	/// assert!("23:1:0".parse::<Time>().is_err());
	/// // no leap seconds
	/// assert!("23:59:60".parse::<Time>().is_err());
	/// // no sub-second precision
	/// assert!("12:34:56.789".parse::<Time>().is_err());
	/// assert!("6pm".parse::<Time>().is_err());
	///```
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		Self::try_parse(s)
	}
}

#[test]
#[should_panic(expected = "100:100:100")]
fn hms_checked() {
	let _ = Time::hms_checked(100, 100, 100);
}