#[cfg(not(feature = "chrono"))]
use crate::{
date::{DateTime, DateTimeTimezoneOffset},
error::{DateTimeSyntaxError, GenericSyntaxError},
};
use crate::{error::ParseNumberError, line::ParsedByteSlice};
use memchr::memchr;
#[cfg(not(feature = "chrono"))]
use memchr::memchr3;
use std::borrow::Cow;
pub trait AsStaticCow {
fn as_cow(&self) -> Cow<'static, str>;
}
pub fn split_on_new_line<'a>(bytes: &'a [u8]) -> ParsedByteSlice<'a, &'a [u8]> {
match memchr(b'\n', bytes) {
Some(n) if n > 0 && bytes[n - 1] == b'\r' => ParsedByteSlice {
parsed: &bytes[..(n - 1)],
remaining: Some(&bytes[(n + 1)..]),
},
Some(n) => ParsedByteSlice {
parsed: &bytes[..n],
remaining: Some(&bytes[(n + 1)..]),
},
None => ParsedByteSlice {
parsed: bytes,
remaining: None,
},
}
}
pub(crate) fn str_from(bytes: &[u8]) -> &str {
unsafe {
std::str::from_utf8_unchecked(bytes)
}
}
#[cfg(not(feature = "chrono"))]
pub fn parse_date_time_bytes<'a>(
input: &'a [u8],
) -> Result<ParsedByteSlice<'a, DateTime>, DateTimeSyntaxError> {
match input.get(4) {
Some(b'-') => (),
b => {
return Err(DateTimeSyntaxError::UnexpectedYearToMonthSeparator(
b.copied(),
));
}
};
let date_fullyear = parse_u32(&input[..4]).map_err(DateTimeSyntaxError::InvalidYear)?;
match input.get(7) {
Some(b'-') => (),
b => {
return Err(DateTimeSyntaxError::UnexpectedMonthToDaySeparator(
b.copied(),
));
}
};
let date_month = parse_u8(&input[5..7]).map_err(DateTimeSyntaxError::InvalidMonth)?;
match input.get(10) {
Some(b't') | Some(b'T') | Some(b' ') => (),
b => return Err(DateTimeSyntaxError::UnexpectedDayHourSeparator(b.copied())),
};
let date_mday = parse_u8(&input[8..10]).map_err(DateTimeSyntaxError::InvalidDay)?;
match input.get(13) {
Some(b':') => (),
b => {
return Err(DateTimeSyntaxError::UnexpectedHourMinuteSeparator(
b.copied(),
));
}
}
let time_hour = parse_u8(&input[11..13]).map_err(DateTimeSyntaxError::InvalidHour)?;
match input.get(16) {
Some(b':') => (),
b => {
return Err(DateTimeSyntaxError::UnexpectedMinuteSecondSeparator(
b.copied(),
));
}
};
let time_minute = parse_u8(&input[14..16]).map_err(DateTimeSyntaxError::InvalidMinute)?;
let time_offset_byte_index = match memchr3(b'Z', b'+', b'-', &input[16..]) {
Some(n) => n + 16,
None => match memchr(b'z', &input[16..]) {
Some(n) => n + 16,
None => return Err(GenericSyntaxError::UnexpectedEndOfLine)?,
},
};
let time_offset_byte = input[time_offset_byte_index];
let time_second = fast_float2::parse(&input[17..time_offset_byte_index])
.map_err(|_| DateTimeSyntaxError::InvalidSecond)?;
match time_offset_byte {
b'Z' | b'z' => {
let remaining = if input.get(time_offset_byte_index + 1).is_some() {
split_on_new_line(&input[(time_offset_byte_index + 1)..])
} else {
ParsedByteSlice {
parsed: b"" as &[u8],
remaining: None,
}
};
if !remaining.parsed.is_empty() {
return Err(DateTimeSyntaxError::UnexpectedCharactersAfterTimezone);
};
let remaining = remaining.remaining;
Ok(ParsedByteSlice {
parsed: DateTime {
date_fullyear,
date_month,
date_mday,
time_hour,
time_minute,
time_second,
timezone_offset: DateTimeTimezoneOffset {
time_hour: 0,
time_minute: 0,
},
},
remaining,
})
}
_ => {
let multiplier = if time_offset_byte == b'-' { -1i8 } else { 1i8 };
match input.get(time_offset_byte_index + 3) {
Some(b':') => (),
b => {
return Err(DateTimeSyntaxError::UnexpectedTimezoneHourMinuteSeparator(
b.copied(),
));
}
};
let timeoffset_hour =
parse_u8(&input[(time_offset_byte_index + 1)..(time_offset_byte_index + 3)])
.map_err(DateTimeSyntaxError::InvalidTimezoneHour)? as i8;
let timeoffset_hour = multiplier * timeoffset_hour;
match input.get(time_offset_byte_index + 4) {
Some(_) => (),
None => return Err(GenericSyntaxError::UnexpectedEndOfLine)?,
};
let ParsedByteSlice { parsed, remaining } =
split_on_new_line(&input[(time_offset_byte_index + 4)..]);
let timeoffset_minute =
parse_u8(parsed).map_err(DateTimeSyntaxError::InvalidTimezoneMinute)?;
Ok(ParsedByteSlice {
parsed: DateTime {
date_fullyear,
date_month,
date_mday,
time_hour,
time_minute,
time_second,
timezone_offset: DateTimeTimezoneOffset {
time_hour: timeoffset_hour,
time_minute: timeoffset_minute,
},
},
remaining,
})
}
}
}
macro_rules! parse_num_impl {
($fn_name:ident -> $ty:ident) => {
pub fn $fn_name(bytes: &[u8]) -> Result<$ty, ParseNumberError> {
if bytes.is_empty() {
return Err(ParseNumberError::Empty);
}
let mut n: $ty = 0;
for &byte in bytes {
let digit = match byte.checked_sub(b'0') {
None => return Err(ParseNumberError::InvalidDigit(byte)),
Some(digit) if digit > 9 => {
return Err(ParseNumberError::InvalidDigit(byte));
}
Some(digit) => {
debug_assert!((0..=9).contains(&digit));
$ty::from(digit)
}
};
n = n
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or_else(|| ParseNumberError::NumberTooBig)?;
}
Ok(n)
}
};
}
parse_num_impl!(parse_u64 -> u64);
#[cfg(not(feature = "chrono"))]
parse_num_impl!(parse_u32 -> u32);
#[cfg(not(feature = "chrono"))]
parse_num_impl!(parse_u8 -> u8);
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "chrono"))]
use crate::date_time;
use pretty_assertions::assert_eq;
#[cfg(not(feature = "chrono"))]
#[test]
fn date_time_parse_with_space_for_day_hour_separator_still_works() {
assert_eq!(
date_time!(2025-08-02 T 20:33:45.123 -05:00),
parse_date_time_bytes(b"2025-08-02 20:33:45.123-05:00")
.unwrap()
.parsed
);
}
#[test]
fn split_on_new_line_should_have_no_remaining_when_no_new_line() {
assert_eq!(
ParsedByteSlice {
parsed: b"test" as &[u8],
remaining: None,
},
split_on_new_line(b"test")
);
}
#[test]
fn split_on_new_line_should_remove_lf() {
assert_eq!(
ParsedByteSlice {
parsed: b"test" as &[u8],
remaining: Some(b"remaining"),
},
split_on_new_line(b"test\nremaining")
);
}
#[test]
fn split_on_new_line_should_remove_crlf() {
assert_eq!(
ParsedByteSlice {
parsed: b"test" as &[u8],
remaining: Some(b"remaining"),
},
split_on_new_line(b"test\r\nremaining")
);
}
#[test]
fn split_on_new_line_should_have_empty_remaining_if_lf_last_char() {
assert_eq!(
ParsedByteSlice {
parsed: b"test" as &[u8],
remaining: Some(b""),
},
split_on_new_line(b"test\n")
);
}
#[test]
fn split_on_new_line_should_have_empty_remaining_if_crlf_last_char() {
assert_eq!(
ParsedByteSlice {
parsed: b"test" as &[u8],
remaining: Some(b""),
},
split_on_new_line(b"test\r\n")
);
}
#[test]
fn split_on_new_line_when_lf_is_first_byte_should_not_panic() {
assert_eq!(
ParsedByteSlice {
parsed: b"" as &[u8],
remaining: Some(b"test"),
},
split_on_new_line(b"\ntest")
);
}
}