use crate::{
DecimalValue, ParseBooleanError, ParseDecimalError, ParseDoubleError, ParseError,
ParseFloatError, ParseIntegerError, Type, Value,
derived::{Byte, Int, Integer, Long, Short},
primitive::{Boolean, Decimal, Double, Float},
};
#[cfg(feature = "alloc")]
use core::convert::Infallible;
#[cfg(feature = "jiff")]
use crate::{
ParseDateTimeError, ParseDurationError, ParseTemporalError,
primitive::{Date, DateTime, Duration, Time},
};
#[cfg_attr(
feature = "jiff",
doc = "Temporal failures return [`ParseError::InvalidTemporal`], retaining the requested datatype and original Jiff error."
)]
pub fn parse(input: impl AsRef<str>, datatype: impl Into<Type>) -> Result<Value, ParseError> {
use crate::{DecimalType as D, PrimitiveType as P, Type::*};
match datatype.into() {
datatype @ (Decimal(D::Decimal) | Primitive(P::Decimal)) => {
parse_decimal(input).map_err(|source| ParseError::InvalidDecimal { datatype, source })
},
Decimal(D::Integer) => parse_integer(input).map_err(|source| ParseError::InvalidInteger {
datatype: D::Integer,
source,
}),
Decimal(D::Long) => parse_long(input).map_err(|source| ParseError::InvalidInteger {
datatype: D::Long,
source,
}),
Decimal(D::Int) => parse_int(input).map_err(|source| ParseError::InvalidInteger {
datatype: D::Int,
source,
}),
Decimal(D::Short) => parse_short(input).map_err(|source| ParseError::InvalidInteger {
datatype: D::Short,
source,
}),
Decimal(D::Byte) => parse_byte(input).map_err(|source| ParseError::InvalidInteger {
datatype: D::Byte,
source,
}),
#[cfg(feature = "alloc")]
Primitive(P::String) => parse_string(input).map_err(|_| ParseError::InvalidLiteral),
Primitive(P::Boolean) => parse_boolean(input),
Primitive(P::GYearMonth) => {
crate::parse_g_year_month(input).map_err(|source| ParseError::InvalidCalendar {
datatype: P::GYearMonth,
source,
})
},
Primitive(P::GYear) => {
crate::parse_g_year(input).map_err(|source| ParseError::InvalidCalendar {
datatype: P::GYear,
source,
})
},
Primitive(P::GMonthDay) => {
crate::parse_g_month_day(input).map_err(|source| ParseError::InvalidCalendar {
datatype: P::GMonthDay,
source,
})
},
Primitive(P::GDay) => {
crate::parse_g_day(input).map_err(|source| ParseError::InvalidCalendar {
datatype: P::GDay,
source,
})
},
Primitive(P::GMonth) => {
crate::parse_g_month(input).map_err(|source| ParseError::InvalidCalendar {
datatype: P::GMonth,
source,
})
},
Primitive(P::Float) => parse_float(input).map_err(|source| ParseError::InvalidFloat {
datatype: P::Float,
source,
}),
Primitive(P::Double) => parse_double(input).map_err(|source| ParseError::InvalidFloat {
datatype: P::Double,
source,
}),
#[cfg(feature = "jiff")]
Primitive(P::Duration) => {
parse_duration(input).map_err(|source| ParseError::InvalidTemporal {
datatype: P::Duration,
source: ParseTemporalError(source),
})
},
#[cfg(feature = "jiff")]
Primitive(P::DateTime) => {
parse_datetime(input).map_err(|source| ParseError::InvalidTemporal {
datatype: P::DateTime,
source: ParseTemporalError(source),
})
},
#[cfg(feature = "jiff")]
Primitive(P::Time) => parse_time(input).map_err(|source| ParseError::InvalidTemporal {
datatype: P::Time,
source: ParseTemporalError(source),
}),
#[cfg(feature = "jiff")]
Primitive(P::Date) => parse_date(input).map_err(|source| ParseError::InvalidTemporal {
datatype: P::Date,
source: ParseTemporalError(source),
}),
datatype => Err(ParseError::UnsupportedDatatype(datatype)),
}
}
pub fn parse_decimal(input: impl AsRef<str>) -> Result<Value, ParseDecimalError> {
let input = input.as_ref();
let unsigned = input.strip_prefix(['+', '-']).unwrap_or(input);
if !is_decimal_significand(unsigned) {
return Err(ParseDecimalError::InvalidLexical);
}
input
.parse::<Decimal>()
.map(Value::from)
.map_err(Into::into)
}
pub fn parse_integer(input: impl AsRef<str>) -> Result<Value, ParseIntegerError> {
input
.as_ref()
.parse::<Integer>()
.map(DecimalValue::from)
.map(Value::from)
}
pub fn parse_long(input: impl AsRef<str>) -> Result<Value, ParseIntegerError> {
input
.as_ref()
.parse::<Long>()
.map(DecimalValue::from)
.map(Value::from)
}
pub fn parse_int(input: impl AsRef<str>) -> Result<Value, ParseIntegerError> {
input
.as_ref()
.parse::<Int>()
.map(DecimalValue::from)
.map(Value::from)
}
pub fn parse_short(input: impl AsRef<str>) -> Result<Value, ParseIntegerError> {
input
.as_ref()
.parse::<Short>()
.map(DecimalValue::from)
.map(Value::from)
}
pub fn parse_byte(input: impl AsRef<str>) -> Result<Value, ParseIntegerError> {
input
.as_ref()
.parse::<Byte>()
.map(DecimalValue::from)
.map(Value::from)
}
#[cfg(feature = "alloc")]
pub fn parse_string(input: impl AsRef<str>) -> Result<Value, Infallible> {
use crate::primitive::String;
input.as_ref().parse::<String>().map(Value::from)
}
#[cfg(not(feature = "alloc"))]
pub fn parse_string(_input: impl AsRef<str>) -> Result<Value, ParseError> {
Err(ParseError::UnsupportedDatatype(crate::STRING))
}
pub fn parse_boolean(input: impl AsRef<str>) -> Result<Value, ParseBooleanError> {
input
.as_ref()
.parse::<Boolean>()
.map(Value::from)
.map_err(|_| ParseError::InvalidBoolean)
}
pub fn parse_float(input: impl AsRef<str>) -> Result<Value, ParseFloatError> {
let input = input.as_ref();
if !is_xsd_floating_point(input) {
return Err(ParseFloatError::InvalidLexical);
}
input.parse::<Float>().map(Value::from).map_err(Into::into)
}
fn is_xsd_floating_point(input: &str) -> bool {
if matches!(input, "INF" | "+INF" | "-INF" | "NaN") {
return true;
}
let unsigned = input.strip_prefix(['+', '-']).unwrap_or(input);
let significand = if let Some((significand, exponent)) = unsigned.split_once(['e', 'E']) {
let exponent = exponent.strip_prefix(['+', '-']).unwrap_or(exponent);
if exponent.is_empty() || !exponent.bytes().all(|byte| byte.is_ascii_digit()) {
return false;
}
significand
} else {
unsigned
};
is_decimal_significand(significand)
}
fn is_decimal_significand(significand: &str) -> bool {
if let Some((integer, fraction)) = significand.split_once('.') {
!(integer.is_empty() && fraction.is_empty())
&& integer.bytes().all(|byte| byte.is_ascii_digit())
&& fraction.bytes().all(|byte| byte.is_ascii_digit())
} else {
!significand.is_empty() && significand.bytes().all(|byte| byte.is_ascii_digit())
}
}
pub fn parse_double(input: impl AsRef<str>) -> Result<Value, ParseDoubleError> {
let input = input.as_ref();
if !is_xsd_floating_point(input) {
return Err(ParseFloatError::InvalidLexical);
}
input.parse::<Double>().map(Value::from).map_err(Into::into)
}
#[cfg(feature = "jiff")]
pub fn parse_duration(input: impl AsRef<str>) -> Result<Value, ParseDurationError> {
let input = input.as_ref();
if input.starts_with('+') {
return Err(jiff::Error::from_args(format_args!(
"xsd:duration must not have a leading plus sign"
)));
}
let unsigned = input.strip_prefix('-').unwrap_or(input);
if input.contains(',') {
return Err(jiff::Error::from_args(format_args!(
"xsd:duration fractions require a period separator"
)));
}
if !unsigned.starts_with('P') || input.bytes().any(|byte| byte.is_ascii_lowercase()) {
return Err(jiff::Error::from_args(format_args!(
"xsd:duration requires uppercase duration designators"
)));
}
if let Some((whole, suffix)) = input.split_once('.') {
let valid_fraction = suffix.strip_suffix('S').is_some_and(|fraction| {
!fraction.is_empty() && fraction.bytes().all(|byte| byte.is_ascii_digit())
});
if !whole.as_bytes().last().is_some_and(u8::is_ascii_digit) || !valid_fraction {
return Err(jiff::Error::from_args(format_args!(
"xsd:duration permits fractions only on seconds, with digits before and after the period"
)));
}
if suffix.len() - 1 > 9 {
return Err(jiff::Error::from_args(format_args!(
"xsd:duration fractional seconds exceed the supported nanosecond precision (9 digits)"
)));
}
}
input.parse::<Duration>().map(Value::from)
}
#[cfg(feature = "jiff")]
pub fn parse_datetime(input: impl AsRef<str>) -> Result<Value, ParseDateTimeError> {
let input = input.as_ref();
if let Some((date, time)) = input.split_once('T')
&& time.starts_with("24:")
&& let Value::Primitive(crate::PrimitiveValue::Date(date)) = parse_date(date)?
{
if date.timezone().is_some() {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime timezone must follow the time component"
)));
}
let Value::Primitive(crate::PrimitiveValue::Time(time)) = parse_time(time)? else {
unreachable!("parse_time returns a Time value");
};
let next = date.civil().tomorrow()?;
return DateTime::new(next.year(), next.month(), next.day(), 0, 0, 0, 0)
.map(|datetime| datetime.with_timezone(time.timezone()).into());
}
if let Some((_, clock)) = input.split_once('T') {
validate_nanosecond_precision(clock)?;
}
let negative_year = input.starts_with('-') && input.as_bytes().get(5) == Some(&b'-');
let without_utc = input.strip_suffix('Z').unwrap_or(input);
let civil_input = if negative_year {
&without_utc[1..]
} else {
without_utc
};
let mut datetime = DateTime::from(civil_input.parse::<jiff::civil::DateTime>()?);
if negative_year {
if datetime.year() == 0 {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime years must not be negative zero"
)));
}
datetime = DateTime::new(
-datetime.year(),
datetime.month(),
datetime.day(),
datetime.hour(),
datetime.minute(),
datetime.second(),
datetime.subsec_nanosecond(),
)?;
}
let separator = input
.bytes()
.position(|byte| matches!(byte, b'T' | b't' | b' '));
let Some(separator) = separator.filter(|&index| input.as_bytes()[index] == b'T') else {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime literals require an uppercase T separator"
)));
};
let date = input[..separator]
.strip_prefix(['+', '-'])
.unwrap_or(&input[..separator]);
if !date
.split_once('-')
.is_some_and(|(_, rest)| matches!(rest.as_bytes(), [_, _, b'-', _, _]))
{
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime literals require hyphen-separated year, month, and day"
)));
}
if input.starts_with('+') {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime years must not have a leading plus sign"
)));
}
if date
.split_once('-')
.is_some_and(|(year, _)| year.len() > 4 && year.starts_with('0'))
{
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime years longer than four digits must not begin with zero"
)));
}
if !matches!(
&input.as_bytes()[separator + 1..],
[_, _, b':', _, _, b':', _, _, ..]
) {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime literals require hours, minutes, and seconds (hh:mm:ss)"
)));
}
if input.as_bytes().get(separator + 9) == Some(&b',') {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime fractional seconds require a period separator"
)));
}
if &input.as_bytes()[separator + 7..separator + 9] == b"60" {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime seconds must be less than 60"
)));
}
if jiff::fmt::temporal::DateTimeParser::new()
.parse_pieces(civil_input)?
.to_numeric_offset()
.is_some_and(|offset| offset.seconds().unsigned_abs() > 14 * 60 * 60)
{
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime timezone offsets must be between -14:00 and +14:00"
)));
}
if input.as_bytes().contains(&b'[') {
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime literals must not contain bracketed annotations"
)));
}
let suffix = &input.as_bytes()[separator + 9..];
if let Some(start) = suffix.iter().position(|byte| matches!(byte, b'+' | b'-'))
&& !matches!(&suffix[start..], [b'+' | b'-', _, _, b':', _, _])
{
return Err(jiff::Error::from_args(format_args!(
"xsd:dateTime numeric timezone offsets require +hh:mm or -hh:mm"
)));
}
let timezone = if let Some(start) = suffix.iter().position(|byte| matches!(byte, b'+' | b'-')) {
Some(
input[separator + 9 + start..]
.parse::<crate::TimezoneOffset>()
.map_err(|error| {
jiff::Error::from_args(format_args!("invalid xsd:dateTime timezone: {error}"))
})?,
)
} else if input.ends_with('Z') {
Some(crate::TimezoneOffset::UTC)
} else {
None
};
Ok(Value::from(datetime.with_timezone(timezone)))
}
#[cfg(feature = "jiff")]
pub fn parse_time(input: impl AsRef<str>) -> Result<Value, ParseDateTimeError> {
let input = input.as_ref();
if !matches!(input.as_bytes(), [_, _, b':', _, _, b':', _, _, ..]) {
return Err(jiff::Error::from_args(format_args!(
"xsd:time literals require hours, minutes, and seconds (hh:mm:ss)"
)));
}
if input.as_bytes().contains(&b'[') {
return Err(jiff::Error::from_args(format_args!(
"xsd:time literals must not contain bracketed annotations"
)));
}
let timezone_start = input.as_bytes().get(8..).and_then(|suffix| {
suffix
.iter()
.position(|byte| matches!(byte, b'+' | b'-' | b'Z' | b'z'))
});
let (clock, timezone) = if let Some(start) = timezone_start {
let (clock, suffix) = input.split_at(start + 8);
if let [b'+' | b'-', h1, h2, rest @ ..] = suffix.as_bytes()
&& h1.is_ascii_digit()
&& h2.is_ascii_digit()
{
let hours = (h1 - b'0') * 10 + h2 - b'0';
if hours > 14 || (hours == 14 && rest.iter().any(|byte| matches!(byte, b'1'..=b'9'))) {
return Err(jiff::Error::from_args(format_args!(
"xsd:time timezone offsets must be between -14:00 and +14:00"
)));
}
}
let offset = suffix
.parse::<crate::TimezoneOffset>()
.map_err(|error| match error {
crate::TimezoneOffsetError::OutOfRange => jiff::Error::from_args(format_args!(
"xsd:time timezone offsets must be between -14:00 and +14:00"
)),
_ => jiff::Error::from_args(format_args!(
"xsd:time numeric timezone offsets require +hh:mm or -hh:mm"
)),
})?;
(clock, Some(offset))
} else {
(input, None)
};
if is_timezone_free_end_of_day(clock) {
return Time::new(0, 0, 0, 0).map(|time| time.with_timezone(timezone).into());
}
validate_nanosecond_precision(clock)?;
let time = Time::from(clock.parse::<jiff::civil::Time>()?).with_timezone(timezone);
if input.as_bytes().get(8) == Some(&b',') {
return Err(jiff::Error::from_args(format_args!(
"xsd:time fractional seconds require a period separator"
)));
}
if &input.as_bytes()[6..8] == b"60" {
return Err(jiff::Error::from_args(format_args!(
"xsd:time seconds must be less than 60"
)));
}
Ok(Value::from(time))
}
#[cfg(feature = "jiff")]
fn validate_nanosecond_precision(clock: &str) -> Result<(), jiff::Error> {
if let Some(fraction) = clock.as_bytes().get(8..).and_then(|s| s.strip_prefix(b".")) {
let digits = fraction
.iter()
.take_while(|byte| byte.is_ascii_digit())
.count();
if digits > 9 {
return Err(jiff::Error::from_args(format_args!(
"XSD fractional seconds exceed the supported nanosecond precision (9 digits)"
)));
}
}
Ok(())
}
#[cfg(feature = "jiff")]
fn is_timezone_free_end_of_day(input: &str) -> bool {
input.strip_prefix("24:00:00").is_some_and(|suffix| {
suffix.is_empty()
|| suffix.strip_prefix('.').is_some_and(|fraction| {
!fraction.is_empty() && fraction.bytes().all(|byte| byte == b'0')
})
})
}
#[cfg(feature = "jiff")]
pub fn parse_date(input: impl AsRef<str>) -> Result<Value, ParseDateTimeError> {
let input = input.as_ref();
if input.contains('[') {
return Err(jiff::Error::from_args(format_args!(
"xsd:date literals must not contain bracketed annotations"
)));
}
if input.contains(['T', 't', ' ']) {
return Err(jiff::Error::from_args(format_args!(
"xsd:date literals must not contain a time component"
)));
}
let unsigned = input.strip_prefix(['+', '-']).unwrap_or(input);
let Some((year_digits, fields)) = unsigned.split_once('-') else {
return Err(jiff::Error::from_args(format_args!(
"xsd:date literals require hyphen-separated year, month, and day"
)));
};
if !matches!(fields.as_bytes(), [_, _, b'-', _, _, ..]) {
return Err(jiff::Error::from_args(format_args!(
"xsd:date literals require hyphen-separated year, month, and day"
)));
}
if input.starts_with('+') {
return Err(jiff::Error::from_args(format_args!(
"xsd:date years must not have a leading plus sign"
)));
}
if year_digits.len() > 4 && year_digits.starts_with('0') {
return Err(jiff::Error::from_args(format_args!(
"xsd:date years longer than four digits must not begin with zero"
)));
}
let (year, rest) = crate::parse_calendar::year_prefix(input)
.map_err(|error| jiff::Error::from_args(format_args!("invalid xsd:date year: {error}")))?;
let year = i16::try_from(year).map_err(|_| {
jiff::Error::from_args(format_args!("xsd:date year is outside -9999..=9999"))
})?;
let [b'-', m1, m2, b'-', d1, d2, ..] = rest.as_bytes() else {
return Err(jiff::Error::from_args(format_args!(
"xsd:date requires hyphen-separated year, month, and day"
)));
};
if ![m1, m2, d1, d2].iter().all(|byte| byte.is_ascii_digit()) {
return Err(jiff::Error::from_args(format_args!(
"xsd:date month and day require two ASCII digits"
)));
}
let month = ((m1 - b'0') * 10 + m2 - b'0') as i8;
let day = ((d1 - b'0') * 10 + d2 - b'0') as i8;
let timezone = if rest.len() == 6 {
None
} else {
Some(
rest[6..]
.parse::<crate::TimezoneOffset>()
.map_err(|error| {
jiff::Error::from_args(format_args!("invalid xsd:date timezone: {error}"))
})?,
)
};
Date::new(year, month, day).map(|date| date.with_timezone(timezone).into())
}