use alloc::format;
use alloc::string::String;
use core::fmt;
use core::str::FromStr;
use crate::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Date {
pub year: u16,
pub month: u8,
pub day: u8,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Time {
pub hour: u8,
pub minute: u8,
pub second: u8,
pub nanosecond: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Offset {
Z,
Custom {
minutes: i16,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Datetime {
pub date: Option<Date>,
pub time: Option<Time>,
pub offset: Option<Offset>,
}
fn is_leap_year(year: u16) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
fn days_in_month(year: u16, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
impl Date {
pub fn validate(&self) -> Result<(), Error> {
if self.month < 1 || self.month > 12 {
return Err(Error::validate(format!(
"month out of range: {}",
self.month
)));
}
let max_day = days_in_month(self.year, self.month);
if self.day < 1 || self.day > max_day {
return Err(Error::validate(format!(
"day out of range: max {} for {}-{:02}, got {}",
max_day, self.year, self.month, self.day
)));
}
Ok(())
}
}
impl Time {
pub fn validate(&self) -> Result<(), Error> {
if self.hour > 23 {
return Err(Error::validate(format!("hour out of range: {}", self.hour)));
}
if self.minute > 59 {
return Err(Error::validate(format!(
"minute out of range: {}",
self.minute
)));
}
if self.second > 59 {
return Err(Error::validate(format!(
"second out of range: {}",
self.second
)));
}
if self.nanosecond > 999_999_999 {
return Err(Error::validate(format!(
"nanosecond out of range: {}",
self.nanosecond
)));
}
Ok(())
}
}
impl Offset {
pub fn validate(&self) -> Result<(), Error> {
match self {
Offset::Z => Ok(()),
Offset::Custom { minutes } => {
if *minutes < -1439 || *minutes > 1439 {
return Err(Error::validate(format!(
"offset out of range: {} minutes",
minutes
)));
}
Ok(())
}
}
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
if self.nanosecond > 0 {
let s = format!("{:09}", self.nanosecond);
let trimmed = s.trim_end_matches('0');
write!(f, ".{trimmed}")?;
}
Ok(())
}
}
impl fmt::Display for Offset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Offset::Z => write!(f, "Z"),
Offset::Custom { minutes } => {
let sign = if *minutes >= 0 { '+' } else { '-' };
let abs = minutes.unsigned_abs();
let h = abs / 60;
let m = abs % 60;
write!(f, "{sign}{h:02}:{m:02}")
}
}
}
}
impl fmt::Display for Datetime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.date, &self.time, &self.offset) {
(Some(d), Some(t), Some(o)) => write!(f, "{d}T{t}{o}"),
(Some(d), Some(t), None) => write!(f, "{d}T{t}"),
(Some(d), None, _) => write!(f, "{d}"),
(None, Some(t), _) => write!(f, "{t}"),
(None, None, _) => Ok(()),
}
}
}
fn parse_n_digits(s: &str, n: usize) -> Result<(u32, &str), Error> {
let bytes = s.as_bytes();
if bytes.len() < n {
return Err(Error::validate(format!(
"expected {n}-digit number but input is too short"
)));
}
if !bytes[..n].iter().all(|b| b.is_ascii_digit()) {
return Err(Error::validate(format!(
"expected {n}-digit number but found non-digit bytes"
)));
}
let digits = &s[..n];
let value = digits
.parse::<u32>()
.map_err(|e| Error::validate(format!("number conversion error: {e}")))?;
Ok((value, &s[n..]))
}
fn expect_byte(s: &str, expected: u8) -> Result<&str, Error> {
match s.as_bytes().first() {
Some(&b) if b == expected => Ok(&s[1..]),
Some(&b) => Err(Error::validate(format!(
"expected '{}' but found '{}'",
expected as char, b as char
))),
None => Err(Error::validate(format!(
"expected '{}' but input ended",
expected as char
))),
}
}
impl FromStr for Datetime {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_datetime_str(s)
}
}
pub(crate) fn parse_datetime_str(s: &str) -> Result<Datetime, Error> {
parse_datetime_str_with_version(s, crate::TomlVersion::V1_0)
}
pub(crate) fn parse_datetime_str_with_version(
s: &str,
version: crate::TomlVersion,
) -> Result<Datetime, Error> {
if s.len() >= 2
&& s.as_bytes()[0].is_ascii_digit()
&& s.as_bytes()[1].is_ascii_digit()
&& s.as_bytes().get(2) == Some(&b':')
{
if s.len() >= 5 && s.as_bytes()[4] == b'-' {
} else {
let (time, rest) = parse_time_part_with_version(s, version)?;
if !rest.is_empty() {
return Err(Error::validate(format!(
"unexpected characters after time: '{rest}'"
)));
}
return Ok(Datetime {
date: None,
time: Some(time),
offset: None,
});
}
}
let (date, rest) = parse_date_part(s)?;
if rest.is_empty() {
return Ok(Datetime {
date: Some(date),
time: None,
offset: None,
});
}
let rest = match rest.as_bytes().first() {
Some(&b'T') | Some(&b't') => &rest[1..],
Some(&b' ') => &rest[1..],
Some(&b) => {
return Err(Error::validate(format!(
"expected 'T' or space after date but found '{}'",
b as char
)));
}
None => unreachable!(),
};
let (time, rest) = parse_time_part_with_version(rest, version)?;
let (offset, rest) = if rest.is_empty() {
(None, rest)
} else {
match rest.as_bytes().first() {
Some(&b'Z') | Some(&b'z') => (Some(Offset::Z), &rest[1..]),
Some(&b'+') | Some(&b'-') => {
let (offset, remaining) = parse_offset_part(rest)?;
(Some(offset), remaining)
}
_ => (None, rest),
}
};
if !rest.is_empty() {
return Err(Error::validate(format!(
"unexpected characters after datetime: '{rest}'"
)));
}
Ok(Datetime {
date: Some(date),
time: Some(time),
offset,
})
}
fn parse_date_part(s: &str) -> Result<(Date, &str), Error> {
let (year, rest) = parse_n_digits(s, 4)?;
let rest = expect_byte(rest, b'-')?;
let (month, rest) = parse_n_digits(rest, 2)?;
let rest = expect_byte(rest, b'-')?;
let (day, rest) = parse_n_digits(rest, 2)?;
let date = Date {
year: year as u16,
month: month as u8,
day: day as u8,
};
date.validate()?;
Ok((date, rest))
}
fn parse_time_part_with_version(
s: &str,
version: crate::TomlVersion,
) -> Result<(Time, &str), Error> {
let (hour, rest) = parse_n_digits(s, 2)?;
let rest = expect_byte(rest, b':')?;
let (minute, rest) = parse_n_digits(rest, 2)?;
let (second, rest) = if version == crate::TomlVersion::V1_1 && !rest.starts_with(':') {
(0u32, rest)
} else {
let rest = expect_byte(rest, b':')?;
parse_n_digits(rest, 2)?
};
let (nanosecond, rest) = if rest.as_bytes().first() == Some(&b'.') {
let rest = &rest[1..];
let digit_count = rest.bytes().take_while(|b| b.is_ascii_digit()).count();
if digit_count == 0 {
return Err(Error::validate(
"fractional seconds require at least one digit",
));
}
let digits = &rest[..digit_count];
let nanosecond = if digit_count <= 9 {
let mut padded = String::from(digits);
while padded.len() < 9 {
padded.push('0');
}
padded
.parse::<u32>()
.map_err(|e| Error::validate(format!("nanosecond conversion error: {e}")))?
} else {
rest[..9]
.parse::<u32>()
.map_err(|e| Error::validate(format!("nanosecond conversion error: {e}")))?
};
(nanosecond, &rest[digit_count..])
} else {
(0, rest)
};
let time = Time {
hour: hour as u8,
minute: minute as u8,
second: second as u8,
nanosecond,
};
time.validate()?;
Ok((time, rest))
}
fn parse_offset_part(s: &str) -> Result<(Offset, &str), Error> {
let sign = *s
.as_bytes()
.first()
.ok_or_else(|| Error::validate("expected '+' or '-' for offset but input is empty"))?;
let rest = &s[1..];
let (hours, rest) = parse_n_digits(rest, 2)?;
let rest = expect_byte(rest, b':')?;
let (minutes, rest) = parse_n_digits(rest, 2)?;
if hours > 23 {
return Err(Error::validate(format!(
"offset hours out of range: {hours}"
)));
}
if minutes > 59 {
return Err(Error::validate(format!(
"offset minutes out of range: {minutes}"
)));
}
let total_minutes = (hours * 60 + minutes) as i16;
let total_minutes = if sign == b'-' {
-total_minutes
} else {
total_minutes
};
let offset = Offset::Custom {
minutes: total_minutes,
};
offset.validate()?;
Ok((offset, rest))
}