blob: f1632528319e7f3f77c03ca0315b6ae5900d8bf3 [file] [log] [blame] [edit]
//! Date and time types.
use bitflags::bitflags;
use core::fmt::{self, Display, Formatter};
/// Date and time representation.
#[derive(Debug, Default, Copy, Clone, Eq)]
#[repr(C)]
pub struct Time {
/// Year. Valid range: `1900..=9999`.
pub year: u16,
/// Month. Valid range: `1..=12`.
pub month: u8,
/// Day of the month. Valid range: `1..=31`.
pub day: u8,
/// Hour. Valid range: `0..=23`.
pub hour: u8,
/// Minute. Valid range: `0..=59`.
pub minute: u8,
/// Second. Valid range: `0..=59`.
pub second: u8,
/// Unused padding.
pub pad1: u8,
/// Nanosececond. Valid range: `0..=999_999_999`.
pub nanosecond: u32,
/// Offset in minutes from UTC. Valid range: `-1440..=1440`, or
/// [`Time::UNSPECIFIED_TIMEZONE`].
pub time_zone: i16,
/// Daylight savings time information.
pub daylight: Daylight,
/// Unused padding.
pub pad2: u8,
}
impl Time {
/// Indicates the time should be interpreted as local time.
pub const UNSPECIFIED_TIMEZONE: i16 = 0x07ff;
/// Create an invalid `Time` with all fields set to zero.
#[must_use]
pub const fn invalid() -> Self {
Self {
year: 0,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
pad1: 0,
nanosecond: 0,
time_zone: 0,
daylight: Daylight::empty(),
pad2: 0,
}
}
/// True if all fields are within valid ranges, false otherwise.
#[must_use]
pub fn is_valid(&self) -> bool {
(1900..=9999).contains(&self.year)
&& (1..=12).contains(&self.month)
&& (1..=31).contains(&self.day)
&& self.hour <= 23
&& self.minute <= 59
&& self.second <= 59
&& self.nanosecond <= 999_999_999
&& ((-1440..=1440).contains(&self.time_zone)
|| self.time_zone == Self::UNSPECIFIED_TIMEZONE)
}
}
impl Display for Time {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:04}-{:02}-{:02} ", self.year, self.month, self.day)?;
write!(
f,
"{:02}:{:02}:{:02}.{:09}",
self.hour, self.minute, self.second, self.nanosecond
)?;
if self.time_zone == Self::UNSPECIFIED_TIMEZONE {
write!(f, " (local)")?;
} else {
let offset_in_hours = self.time_zone as f32 / 60.0;
let integer_part = offset_in_hours as i16;
// We can't use "offset_in_hours.fract()" because it is part of `std`.
let fraction_part = offset_in_hours - (integer_part as f32);
// most time zones
if fraction_part == 0.0 {
write!(f, "UTC+{offset_in_hours}")?;
}
// time zones with 30min offset (and perhaps other special time zones)
else {
write!(f, "UTC+{offset_in_hours:.1}")?;
}
}
Ok(())
}
}
/// The padding fields of `Time` are ignored for comparison.
impl PartialEq for Time {
fn eq(&self, other: &Self) -> bool {
self.year == other.year
&& self.month == other.month
&& self.day == other.day
&& self.hour == other.hour
&& self.minute == other.minute
&& self.second == other.second
&& self.nanosecond == other.nanosecond
&& self.time_zone == other.time_zone
&& self.daylight == other.daylight
}
}
bitflags! {
/// A bitmask containing daylight savings time information.
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Daylight: u8 {
/// Time is affected by daylight savings time.
const ADJUST_DAYLIGHT = 0x01;
/// Time has been adjusted for daylight savings time.
const IN_DAYLIGHT = 0x02;
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
use alloc::string::ToString;
#[test]
fn test_time_display() {
let mut time = Time {
year: 2023,
month: 5,
day: 18,
hour: 11,
minute: 29,
second: 57,
nanosecond: 123_456_789,
time_zone: Time::UNSPECIFIED_TIMEZONE,
daylight: Daylight::empty(),
pad1: 0,
pad2: 0,
};
assert_eq!(time.to_string(), "2023-05-18 11:29:57.123456789 (local)");
time.time_zone = 120;
assert_eq!(time.to_string(), "2023-05-18 11:29:57.123456789UTC+2");
time.time_zone = 150;
assert_eq!(time.to_string(), "2023-05-18 11:29:57.123456789UTC+2.5");
}
}