use super::*;
pub(crate) fn as_date(v: &Value) -> Option<i64> {
match v {
Value::Property(PropertyValue::Date(d)) => Some(*d),
_ => None,
}
}
pub(crate) fn as_duration(v: &Value) -> Option<temporal::DurationParts> {
match v {
Value::Property(PropertyValue::Duration {
months,
days,
seconds,
nanos,
}) => Some((*months, *days, *seconds, *nanos)),
_ => None,
}
}
pub(crate) fn duration_value((months, days, seconds, nanos): temporal::DurationParts) -> Value {
Value::Property(PropertyValue::Duration {
months,
days,
seconds,
nanos,
})
}
pub(crate) fn as_local_time(v: &Value) -> Option<i64> {
match v {
Value::Property(PropertyValue::LocalTime(n)) => Some(*n),
_ => None,
}
}
pub(crate) fn as_time(v: &Value) -> Option<(i64, i32)> {
match v {
Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}) => Some((*nanos_of_day, *offset_seconds)),
_ => None,
}
}
pub(crate) fn as_local_date_time(v: &Value) -> Option<(i64, i32)> {
match v {
Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}) => Some((*epoch_seconds, *nanos)),
_ => None,
}
}
pub(crate) fn as_date_time(v: &Value) -> Option<(i64, i32, temporal::TzId)> {
match v {
Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone,
}) => Some((*epoch_seconds, *nanos, tz_from_graph(zone))),
_ => None,
}
}
pub(crate) fn tz_from_graph(zone: &GraphTzId) -> temporal::TzId {
match zone {
GraphTzId::Offset(o) => temporal::TzId::Offset(*o),
GraphTzId::Named(name) => temporal::TzId::Named(name.clone()),
}
}
pub(crate) fn tz_to_graph(zone: temporal::TzId) -> GraphTzId {
match zone {
temporal::TzId::Offset(o) => GraphTzId::Offset(o),
temporal::TzId::Named(name) => GraphTzId::Named(name),
}
}
pub(crate) fn apply_temporal_arith(
op: ArithOp,
a: &Value,
b: &Value,
) -> Result<Option<Value>, QueryError> {
let date_plus_duration =
|d: i64, dur: temporal::DurationParts, negate: bool| -> Result<Value, QueryError> {
let (months, days, seconds, nanos) = dur;
temporal::add_duration_to_date(d, months, days, seconds, nanos, negate)
.map(|d| Value::Property(PropertyValue::Date(d)))
.ok_or_else(|| {
QueryError::Type("date +/- duration produced an out-of-range date".into())
})
};
let local_time_plus_duration = |t: i64, dur: temporal::DurationParts, negate: bool| -> Value {
let (_, _, seconds, nanos) = dur;
Value::Property(PropertyValue::LocalTime(temporal::add_duration_to_time(
t, seconds, nanos, negate,
)))
};
let time_plus_duration =
|(t, offset): (i64, i32), dur: temporal::DurationParts, negate: bool| -> Value {
let (_, _, seconds, nanos) = dur;
Value::Property(PropertyValue::Time {
nanos_of_day: temporal::add_duration_to_time(t, seconds, nanos, negate),
offset_seconds: offset,
})
};
let local_date_time_plus_duration = |(epoch_seconds, existing_nanos): (i64, i32),
dur: temporal::DurationParts,
negate: bool|
-> Result<Value, QueryError> {
let (months, days, seconds, nanos) = dur;
temporal::add_duration_to_local_date_time(
epoch_seconds,
existing_nanos,
months,
days,
seconds,
nanos,
negate,
)
.map(|(epoch_seconds, nanos)| {
Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
})
})
.ok_or_else(|| {
QueryError::Type("local date-time +/- duration produced an out-of-range value".into())
})
};
let date_time_plus_duration =
|(epoch_seconds, existing_nanos, zone): (i64, i32, temporal::TzId),
dur: temporal::DurationParts,
negate: bool|
-> Result<Value, QueryError> {
let (months, days, seconds, nanos) = dur;
let offset_seconds = temporal::resolve_offset(&zone, epoch_seconds);
temporal::add_duration_to_local_date_time(
epoch_seconds + offset_seconds as i64,
existing_nanos,
months,
days,
seconds,
nanos,
negate,
)
.map(|(local_epoch_seconds, nanos)| {
Value::Property(PropertyValue::DateTime {
epoch_seconds: local_epoch_seconds - offset_seconds as i64,
nanos,
zone: tz_to_graph(zone),
})
})
.ok_or_else(|| {
QueryError::Type("date-time +/- duration produced an out-of-range value".into())
})
};
Ok(match op {
ArithOp::Add => {
if let (Some(d), Some(dur)) = (as_date(a), as_duration(b)) {
Some(date_plus_duration(d, dur, false)?)
} else if let (Some(dur), Some(d)) = (as_duration(a), as_date(b)) {
Some(date_plus_duration(d, dur, false)?)
} else if let (Some(t), Some(dur)) = (as_local_time(a), as_duration(b)) {
Some(local_time_plus_duration(t, dur, false))
} else if let (Some(dur), Some(t)) = (as_duration(a), as_local_time(b)) {
Some(local_time_plus_duration(t, dur, false))
} else if let (Some(t), Some(dur)) = (as_time(a), as_duration(b)) {
Some(time_plus_duration(t, dur, false))
} else if let (Some(dur), Some(t)) = (as_duration(a), as_time(b)) {
Some(time_plus_duration(t, dur, false))
} else if let (Some(dt), Some(dur)) = (as_local_date_time(a), as_duration(b)) {
Some(local_date_time_plus_duration(dt, dur, false)?)
} else if let (Some(dur), Some(dt)) = (as_duration(a), as_local_date_time(b)) {
Some(local_date_time_plus_duration(dt, dur, false)?)
} else if let (Some(dt), Some(dur)) = (as_date_time(a), as_duration(b)) {
Some(date_time_plus_duration(dt, dur, false)?)
} else if let (Some(dur), Some(dt)) = (as_duration(a), as_date_time(b)) {
Some(date_time_plus_duration(dt, dur, false)?)
} else if let (Some(x), Some(y)) = (as_duration(a), as_duration(b)) {
Some(duration_value(temporal::add_duration(x, y).ok_or_else(
|| QueryError::Type("duration addition overflow".into()),
)?))
} else {
None
}
}
ArithOp::Sub => {
if let (Some(d), Some(dur)) = (as_date(a), as_duration(b)) {
Some(date_plus_duration(d, dur, true)?)
} else if let (Some(t), Some(dur)) = (as_local_time(a), as_duration(b)) {
Some(local_time_plus_duration(t, dur, true))
} else if let (Some(t), Some(dur)) = (as_time(a), as_duration(b)) {
Some(time_plus_duration(t, dur, true))
} else if let (Some(dt), Some(dur)) = (as_local_date_time(a), as_duration(b)) {
Some(local_date_time_plus_duration(dt, dur, true)?)
} else if let (Some(dt), Some(dur)) = (as_date_time(a), as_duration(b)) {
Some(date_time_plus_duration(dt, dur, true)?)
} else if let (Some(x), Some(y)) = (as_duration(a), as_duration(b)) {
Some(duration_value(temporal::sub_duration(x, y).ok_or_else(
|| QueryError::Type("duration subtraction overflow".into()),
)?))
} else {
None
}
}
ArithOp::Mul => {
if let (Some(dur), Some(f)) = (as_duration(a), value_as_f64(b)) {
Some(duration_value(temporal::scale_duration(dur, f)))
} else if let (Some(f), Some(dur)) = (value_as_f64(a), as_duration(b)) {
Some(duration_value(temporal::scale_duration(dur, f)))
} else {
None
}
}
ArithOp::Div => {
if let (Some(dur), Some(f)) = (as_duration(a), value_as_f64(b)) {
if f == 0.0 {
return Err(QueryError::Type("division by zero".into()));
}
Some(duration_value(temporal::scale_duration(dur, 1.0 / f)))
} else {
None
}
}
ArithOp::Mod => None,
ArithOp::Pow => None,
})
}
pub(crate) fn now_or_null(args: &[Value], now_value: impl FnOnce() -> Value) -> Value {
if matches!(args.first(), Some(Value::Null)) {
Value::Null
} else {
now_value()
}
}
pub(crate) fn date_builtin(
args: &[Value],
now: temporal::NowSnapshot,
) -> Result<Value, QueryError> {
if args.len() > 1 {
return Err(QueryError::Semantic(format!(
"date() expects zero or one argument, got {}",
args.len()
)));
}
let Some(arg) = args.first() else {
return Ok(Value::Property(PropertyValue::Date(now.epoch_day)));
};
if matches!(arg, Value::Null) {
return Ok(Value::Null);
}
if let Value::Property(PropertyValue::Date(d)) = arg {
return Ok(Value::Property(PropertyValue::Date(*d)));
}
if matches!(
arg,
Value::Property(PropertyValue::LocalDateTime { .. } | PropertyValue::DateTime { .. })
) {
let epoch_day = extract_date_base_epoch_day("date() argument", arg)?;
return Ok(Value::Property(PropertyValue::Date(epoch_day)));
}
if let Some(s) = as_arith_str(arg) {
let d = temporal::parse_date(s).ok_or_else(|| {
QueryError::Type(format!(
"'{s}' isn't a date string MarsDB can parse -- only the calendar forms YYYY-MM-DD/YYYYMMDD/\
YYYY-MM/YYYYMM/YYYY, week-date forms YYYY-Www[-D]/YYYYWww[D], and ordinal-date forms \
YYYY-DDD/YYYYDDD are supported"
))
})?;
return Ok(Value::Property(PropertyValue::Date(d)));
}
if let Value::Map(m) = arg {
return Ok(Value::Property(PropertyValue::Date(date_from_map(m)?)));
}
Err(QueryError::Type(format!(
"date() doesn't support this argument: {arg:?}"
)))
}
pub(crate) fn extract_date_base_epoch_day(key: &str, v: &Value) -> Result<i64, QueryError> {
match v {
Value::Property(PropertyValue::Date(d)) => Ok(*d),
Value::Property(PropertyValue::LocalDateTime { epoch_seconds, .. }) => {
Ok(temporal::split_epoch_seconds(*epoch_seconds).0)
}
Value::Property(PropertyValue::DateTime {
epoch_seconds,
zone,
..
}) => {
let offset_seconds = temporal::resolve_offset(&tz_from_graph(zone), *epoch_seconds);
Ok(temporal::split_epoch_seconds(epoch_seconds + offset_seconds as i64).0)
}
other => Err(QueryError::Type(format!(
"'{key}' must be a Date, LocalDateTime, or DateTime, got {other:?}"
))),
}
}
pub(crate) type ClockBase = (i64, i64, i64, i64, Option<(temporal::TzId, i32)>);
pub(crate) fn extract_time_base(key: &str, v: &Value) -> Result<ClockBase, QueryError> {
let hms_nanos = |nanos_of_day: i64| {
(
temporal::local_time_component(nanos_of_day, "hour").unwrap(),
temporal::local_time_component(nanos_of_day, "minute").unwrap(),
temporal::local_time_component(nanos_of_day, "second").unwrap(),
temporal::local_time_component(nanos_of_day, "nanosecond").unwrap(),
)
};
match v {
Value::Property(PropertyValue::LocalTime(n)) => {
let (h, m, s, ns) = hms_nanos(*n);
Ok((h, m, s, ns, None))
}
Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}) => {
let (h, m, s, ns) = hms_nanos(*nanos_of_day);
Ok((
h,
m,
s,
ns,
Some((temporal::TzId::Offset(*offset_seconds), *offset_seconds)),
))
}
Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}) => {
let (_, nanos_of_day) = temporal::split_epoch_seconds(*epoch_seconds);
let (h, m, s, _) = hms_nanos(nanos_of_day);
Ok((h, m, s, *nanos as i64, None))
}
Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone,
}) => {
let tz = tz_from_graph(zone);
let offset_seconds = temporal::resolve_offset(&tz, *epoch_seconds);
let local = epoch_seconds + offset_seconds as i64;
let (_, nanos_of_day) = temporal::split_epoch_seconds(local);
let (h, m, s, _) = hms_nanos(nanos_of_day);
Ok((h, m, s, *nanos as i64, Some((tz, offset_seconds))))
}
other => Err(QueryError::Type(format!(
"'{key}' must be a LocalTime, Time, LocalDateTime, or DateTime, got {other:?}"
))),
}
}
pub(crate) const DATE_ALLOWED_KEYS: &[&str] = &[
"year",
"month",
"day",
"week",
"dayOfWeek",
"ordinalDay",
"quarter",
"dayOfQuarter",
"date",
];
pub(crate) fn date_from_map(m: &BTreeMap<String, Value>) -> Result<i64, QueryError> {
let (year, month, day) = calendar_fields_from_map("date", m, DATE_ALLOWED_KEYS)?;
temporal::epoch_day_from_ymd(year, month, day).ok_or_else(|| {
QueryError::Type(format!(
"{year:04}-{month:02}-{day:02} isn't a valid calendar date"
))
})
}
pub(crate) fn calendar_fields_from_map(
caller: &str,
m: &BTreeMap<String, Value>,
allowed: &[&str],
) -> Result<(i64, u32, u32), QueryError> {
if let Some(bad) = m.keys().find(|k| !allowed.contains(&k.as_str())) {
return Err(QueryError::Type(format!(
"{caller}({{...}}) key '{bad}' isn't a recognized field"
)));
}
let int_field = |key: &str, value: &Value| {
value_as_i64(value).ok_or_else(|| {
QueryError::Type(format!("{caller}({{...}})'s '{key}' must be an integer"))
})
};
let base_epoch_day = m
.get("date")
.map(|v| ("date", v))
.or_else(|| m.get("datetime").map(|v| ("datetime", v)))
.map(|(k, v)| extract_date_base_epoch_day(k, v))
.transpose()?;
let epoch_day_from_component =
|prop: &str| base_epoch_day.map(|ed| temporal::date_component(ed, prop).unwrap());
if m.contains_key("week") || m.contains_key("dayOfWeek") {
let week_year = match m.get("year") {
Some(v) => int_field("year", v)?,
None => epoch_day_from_component("weekYear").ok_or_else(|| {
QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
})?,
};
let week = match m.get("week") {
Some(v) => u32::try_from(int_field("week", v)?).map_err(|_| {
QueryError::Type(format!("{caller}({{...}})'s 'week' is out of range"))
})?,
None => u32::try_from(epoch_day_from_component("week").ok_or_else(|| {
QueryError::Type(format!("{caller}({{...}}) requires a 'week' key"))
})?)
.unwrap(),
};
let day_of_week = match m.get("dayOfWeek") {
Some(v) => int_field("dayOfWeek", v)?,
None => epoch_day_from_component("dayOfWeek").unwrap_or(1),
};
let epoch_day = temporal::epoch_day_from_week_fields(week_year, week, day_of_week)
.ok_or_else(|| {
QueryError::Type(format!(
"{caller}({{...}}) has an out-of-range week-date field"
))
})?;
return Ok((
temporal::date_component(epoch_day, "year").unwrap(),
temporal::date_component(epoch_day, "month").unwrap() as u32,
temporal::date_component(epoch_day, "day").unwrap() as u32,
));
}
if m.contains_key("ordinalDay") {
let year = match m.get("year") {
Some(v) => int_field("year", v)?,
None => epoch_day_from_component("year").ok_or_else(|| {
QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
})?,
};
let ordinal_raw = int_field("ordinalDay", m.get("ordinalDay").unwrap())?;
let ordinal_day = u32::try_from(ordinal_raw).map_err(|_| {
QueryError::Type(format!("{caller}({{...}})'s 'ordinalDay' is out of range"))
})?;
let epoch_day =
temporal::epoch_day_from_ordinal_fields(year, ordinal_day).ok_or_else(|| {
QueryError::Type(format!(
"{caller}({{...}}) has an out-of-range ordinalDay field"
))
})?;
return Ok((
year,
temporal::date_component(epoch_day, "month").unwrap() as u32,
temporal::date_component(epoch_day, "day").unwrap() as u32,
));
}
if m.contains_key("quarter") || m.contains_key("dayOfQuarter") {
let year = match m.get("year") {
Some(v) => int_field("year", v)?,
None => epoch_day_from_component("year").ok_or_else(|| {
QueryError::Type(format!("{caller}({{...}}) requires a 'year' key"))
})?,
};
let quarter = match m.get("quarter") {
Some(v) => u32::try_from(int_field("quarter", v)?).map_err(|_| {
QueryError::Type(format!("{caller}({{...}})'s 'quarter' is out of range"))
})?,
None => u32::try_from(epoch_day_from_component("quarter").ok_or_else(|| {
QueryError::Type(format!("{caller}({{...}}) requires a 'quarter' key"))
})?)
.unwrap(),
};
let day_of_quarter = match m.get("dayOfQuarter") {
Some(v) => int_field("dayOfQuarter", v)?,
None => epoch_day_from_component("dayOfQuarter").unwrap_or(1),
};
let epoch_day = temporal::epoch_day_from_quarter_fields(year, quarter, day_of_quarter)
.ok_or_else(|| {
QueryError::Type(format!(
"{caller}({{...}}) has an out-of-range quarter-date field"
))
})?;
return Ok((
year,
temporal::date_component(epoch_day, "month").unwrap() as u32,
temporal::date_component(epoch_day, "day").unwrap() as u32,
));
}
let year_raw = match m.get("year") {
Some(v) => int_field("year", v)?,
None => epoch_day_from_component("year")
.ok_or_else(|| QueryError::Type(format!("{caller}({{...}}) requires a 'year' key")))?,
};
let year = year_raw;
let month_raw = match m.get("month") {
Some(v) => int_field("month", v)?,
None => epoch_day_from_component("month").unwrap_or(1),
};
let month = u32::try_from(month_raw).map_err(|_| {
QueryError::Type(format!(
"{caller}({{...}})'s 'month' is out of range: {month_raw}"
))
})?;
let day_raw = match m.get("day") {
Some(v) => int_field("day", v)?,
None => epoch_day_from_component("day").unwrap_or(1),
};
let day = u32::try_from(day_raw).map_err(|_| {
QueryError::Type(format!(
"{caller}({{...}})'s 'day' is out of range: {day_raw}"
))
})?;
Ok((year, month, day))
}
pub(crate) fn duration_builtin(args: &[Value]) -> Result<Value, QueryError> {
if args.len() != 1 {
return Err(QueryError::Semantic(format!(
"duration() expects exactly one argument, got {}",
args.len()
)));
}
let arg = &args[0];
if matches!(arg, Value::Null) {
return Ok(Value::Null);
}
let (months, days, seconds, nanos) = if let Some(s) = as_arith_str(arg) {
temporal::parse_duration(s).ok_or_else(|| {
QueryError::Type(format!(
"'{s}' isn't a duration string MarsDB can parse -- only ISO-8601 'PnYnMnWnDTnHnMnS' text is \
supported, not the alternate combined date-time duration syntax"
))
})?
} else if let Value::Map(m) = arg {
temporal::normalize_duration(duration_fields_from_map(m)?)
} else {
return Err(QueryError::Type(format!(
"duration() doesn't support this argument: {arg:?}"
)));
};
Ok(Value::Property(PropertyValue::Duration {
months,
days,
seconds,
nanos,
}))
}
pub(crate) fn duration_fields_from_map(
m: &BTreeMap<String, Value>,
) -> Result<temporal::DurationFields, QueryError> {
const ALLOWED: &[&str] = &[
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds",
"nanoseconds",
];
if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
return Err(QueryError::Type(format!(
"duration({{...}}) key '{bad}' isn't a recognized duration unit"
)));
}
let field = |key: &str| -> Result<f64, QueryError> {
match m.get(key) {
None => Ok(0.0),
Some(v) => value_as_f64(v).ok_or_else(|| {
QueryError::Type(format!("duration({{...}})'s '{key}' must be a number"))
}),
}
};
Ok(temporal::DurationFields {
years: field("years")?,
months: field("months")?,
weeks: field("weeks")?,
days: field("days")?,
hours: field("hours")?,
minutes: field("minutes")?,
seconds: field("seconds")?,
milliseconds: field("milliseconds")?,
microseconds: field("microseconds")?,
nanoseconds: field("nanoseconds")?,
})
}
pub(crate) fn sub_second_nanos_from_map(
base_fraction_ns: i64,
m: &BTreeMap<String, Value>,
) -> Result<i64, QueryError> {
let base_ms = base_fraction_ns / 1_000_000;
let base_us = (base_fraction_ns / 1_000) % 1000;
let base_ns = base_fraction_ns % 1000;
let ms = int_field(m, "millisecond", base_ms)?;
let us = int_field(m, "microsecond", base_us)?;
let ns = int_field(m, "nanosecond", base_ns)?;
Ok(ms * 1_000_000 + us * 1_000 + ns)
}
pub(crate) fn int_field(
m: &BTreeMap<String, Value>,
key: &str,
default: i64,
) -> Result<i64, QueryError> {
match m.get(key) {
None => Ok(default),
Some(v) => {
value_as_i64(v).ok_or_else(|| QueryError::Type(format!("'{key}' must be an integer")))
}
}
}
pub(crate) fn clock_fields_from_map(
m: &BTreeMap<String, Value>,
epoch_day: Option<i64>,
) -> Result<ClockBase, QueryError> {
let (base_h, base_m, base_s, base_ns, base_zone) = if let Some(v) = m.get("time") {
extract_time_base("time", v)?
} else if let Some(v) = m.get("datetime") {
extract_time_base("datetime", v)?
} else {
(0, 0, 0, 0, None)
};
let has_explicit_timezone = m.contains_key("timezone");
let effective_zone = match m.get("timezone") {
Some(v) => Some(timezone_value_to_tzid(v)?),
None => base_zone.as_ref().map(|(tz, _)| tz.clone()),
};
let base_nanos_of_day =
base_h * 3_600_000_000_000 + base_m * 60_000_000_000 + base_s * 1_000_000_000 + base_ns;
let (base_h, base_m, base_s, base_ns, effective_offset) = if has_explicit_timezone {
let from_offset = match base_zone.as_ref() {
Some((temporal::TzId::Offset(o), _)) => Some(*o),
Some((zone @ temporal::TzId::Named(_), resolved)) => Some(match epoch_day {
Some(ed) => temporal::resolve_offset(
zone,
temporal::combine_epoch_day_and_nanos_of_day(ed, base_nanos_of_day),
),
None => *resolved,
}),
None => None,
};
let to_offset = match (from_offset, effective_zone.as_ref(), epoch_day) {
(Some(_), Some(temporal::TzId::Offset(to)), _) => Some(*to),
(Some(from), Some(zone @ temporal::TzId::Named(_)), Some(ed)) => {
let approx_epoch_seconds =
temporal::combine_epoch_day_and_nanos_of_day(ed, base_nanos_of_day)
- from as i64;
Some(temporal::resolve_offset(zone, approx_epoch_seconds))
}
_ => None,
};
match (from_offset, to_offset) {
(Some(from), Some(to)) if from != to => {
let shifted = (base_nanos_of_day + (to - from) as i64 * 1_000_000_000)
.rem_euclid(86_400_000_000_000);
(
shifted / 3_600_000_000_000,
(shifted / 60_000_000_000) % 60,
(shifted / 1_000_000_000) % 60,
shifted % 1_000_000_000,
to_offset.unwrap_or(0),
)
}
_ => (base_h, base_m, base_s, base_ns, to_offset.unwrap_or(0)),
}
} else {
(
base_h,
base_m,
base_s,
base_ns,
base_zone.as_ref().map_or(0, |(_, o)| *o),
)
};
Ok((
int_field(m, "hour", base_h)?,
int_field(m, "minute", base_m)?,
int_field(m, "second", base_s)?,
sub_second_nanos_from_map(base_ns, m)?,
effective_zone.map(|z| (z, effective_offset)),
))
}
pub(crate) fn local_time_builtin(
args: &[Value],
now: temporal::NowSnapshot,
) -> Result<Value, QueryError> {
if args.len() > 1 {
return Err(QueryError::Semantic(format!(
"localtime() expects zero or one argument, got {}",
args.len()
)));
}
let Some(arg) = args.first() else {
return Ok(Value::Property(PropertyValue::LocalTime(now.nanos_of_day)));
};
if matches!(arg, Value::Null) {
return Ok(Value::Null);
}
if let Value::Property(PropertyValue::LocalTime(t)) = arg {
return Ok(Value::Property(PropertyValue::LocalTime(*t)));
}
if matches!(
arg,
Value::Property(
PropertyValue::Time { .. }
| PropertyValue::LocalDateTime { .. }
| PropertyValue::DateTime { .. }
)
) {
let (hour, minute, second, nanos, _) = extract_time_base("localtime() argument", arg)?;
let t = temporal::local_time_nanos_from_fields(hour, minute, second, nanos).ok_or_else(
|| QueryError::Type("localtime() argument has an out-of-range field".into()),
)?;
return Ok(Value::Property(PropertyValue::LocalTime(t)));
}
if let Some(s) = as_arith_str(arg) {
let t = temporal::parse_local_time(s).ok_or_else(|| {
QueryError::Type(format!("'{s}' isn't a local time string MarsDB can parse"))
})?;
return Ok(Value::Property(PropertyValue::LocalTime(t)));
}
if let Value::Map(m) = arg {
const ALLOWED: &[&str] = &[
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
"time",
];
if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
return Err(QueryError::Type(format!(
"localtime({{...}}) key '{bad}' isn't a recognized field"
)));
}
let (hour, minute, second, nanos, _) = clock_fields_from_map(m, None)?;
let t = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
.ok_or_else(|| QueryError::Type("localtime({...}) has an out-of-range field".into()))?;
return Ok(Value::Property(PropertyValue::LocalTime(t)));
}
Err(QueryError::Type(format!(
"localtime() doesn't support this argument: {arg:?}"
)))
}
pub(crate) fn time_builtin(
args: &[Value],
now: temporal::NowSnapshot,
) -> Result<Value, QueryError> {
if args.len() > 1 {
return Err(QueryError::Semantic(format!(
"time() expects zero or one argument, got {}",
args.len()
)));
}
let Some(arg) = args.first() else {
return Ok(Value::Property(PropertyValue::Time {
nanos_of_day: now.nanos_of_day,
offset_seconds: 0,
}));
};
if matches!(arg, Value::Null) {
return Ok(Value::Null);
}
if let Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}) = arg
{
return Ok(Value::Property(PropertyValue::Time {
nanos_of_day: *nanos_of_day,
offset_seconds: *offset_seconds,
}));
}
if matches!(
arg,
Value::Property(
PropertyValue::LocalTime(_)
| PropertyValue::LocalDateTime { .. }
| PropertyValue::DateTime { .. }
)
) {
let (hour, minute, second, nanos, zone) = extract_time_base("time() argument", arg)?;
let nanos_of_day = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
.ok_or_else(|| QueryError::Type("time() argument has an out-of-range field".into()))?;
return Ok(Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds: zone.map_or(0, |(_, o)| o),
}));
}
if let Some(s) = as_arith_str(arg) {
if s.contains('[') {
return Err(QueryError::Type(
"time('...'): named timezones (e.g. '[Europe/Stockholm]') aren't supported, only a fixed UTC \
offset like '+01:00'"
.into(),
));
}
let (nanos_of_day, offset_seconds) = temporal::parse_time(s).ok_or_else(|| {
QueryError::Type(format!("'{s}' isn't a time string MarsDB can parse"))
})?;
return Ok(Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}));
}
if let Value::Map(m) = arg {
const ALLOWED: &[&str] = &[
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
"timezone",
"time",
];
if let Some(bad) = m.keys().find(|k| !ALLOWED.contains(&k.as_str())) {
return Err(QueryError::Type(format!(
"time({{...}}) key '{bad}' isn't a recognized field"
)));
}
let (hour, minute, second, nanos, zone) = clock_fields_from_map(m, None)?;
let offset_seconds = match zone {
None => 0,
Some((_, o)) if !m.contains_key("timezone") => o,
Some((temporal::TzId::Offset(o), _)) => o,
Some((temporal::TzId::Named(name), _)) => {
return Err(QueryError::Type(format!(
"'timezone': '{name}' looks like a named timezone (e.g. 'Europe/Stockholm') -- TIME has \
no calendar date to resolve a named zone's DST-dependent offset against, only a fixed \
UTC offset like '+01:00' is supported"
)));
}
};
let nanos_of_day = temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
.ok_or_else(|| QueryError::Type("time({...}) has an out-of-range field".into()))?;
return Ok(Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}));
}
Err(QueryError::Type(format!(
"time() doesn't support this argument: {arg:?}"
)))
}
pub(crate) fn timezone_value_to_tzid(v: &Value) -> Result<temporal::TzId, QueryError> {
let s = as_arith_str(v).ok_or_else(|| {
QueryError::Type(
"'timezone' must be a string offset or IANA zone name, e.g. '+01:00' or \
'Europe/Stockholm'"
.into(),
)
})?;
if let Some(offset) = temporal::parse_offset_seconds(s) {
return Ok(temporal::TzId::Offset(offset));
}
if temporal::parse_timezone_name(s).is_some() {
return Ok(temporal::TzId::Named(s.to_string()));
}
Err(QueryError::Type(format!(
"'timezone': '{s}' isn't a valid UTC offset or a recognized IANA zone name"
)))
}
pub(crate) fn local_date_time_builtin(
args: &[Value],
now: temporal::NowSnapshot,
) -> Result<Value, QueryError> {
if args.len() > 1 {
return Err(QueryError::Semantic(format!(
"localdatetime() expects zero or one argument, got {}",
args.len()
)));
}
let Some(arg) = args.first() else {
return Ok(Value::Property(PropertyValue::LocalDateTime {
epoch_seconds: now.epoch_seconds,
nanos: now.nanos,
}));
};
if matches!(arg, Value::Null) {
return Ok(Value::Null);
}
if let Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}) = arg
{
return Ok(Value::Property(PropertyValue::LocalDateTime {
epoch_seconds: *epoch_seconds,
nanos: *nanos,
}));
}
if matches!(arg, Value::Property(PropertyValue::DateTime { .. })) {
let epoch_day = extract_date_base_epoch_day("localdatetime() argument", arg)?;
let year = temporal::date_component(epoch_day, "year").unwrap();
let month = temporal::date_component(epoch_day, "month").unwrap() as u32;
let day = temporal::date_component(epoch_day, "day").unwrap() as u32;
let (hour, minute, second, nanos, _) = extract_time_base("localdatetime() argument", arg)?;
let (epoch_seconds, nanos) =
temporal::local_date_time_from_fields(temporal::CalendarDateTime {
year,
month,
day,
hour,
minute,
second,
nanos,
})
.ok_or_else(|| {
QueryError::Type("localdatetime() argument has an out-of-range field".into())
})?;
return Ok(Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}));
}
if let Some(s) = as_arith_str(arg) {
let (epoch_seconds, nanos) = temporal::parse_local_date_time(s).ok_or_else(|| {
QueryError::Type(format!(
"'{s}' isn't a local date-time string MarsDB can parse"
))
})?;
return Ok(Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}));
}
if let Value::Map(m) = arg {
let (year, month, day) =
calendar_fields_from_map("localdatetime", m, DATE_TIME_ALLOWED_KEYS)?;
let (hour, minute, second, nanos, _) = clock_fields_from_map(m, None)?;
let (epoch_seconds, nanos) =
temporal::local_date_time_from_fields(temporal::CalendarDateTime {
year,
month,
day,
hour,
minute,
second,
nanos,
})
.ok_or_else(|| {
QueryError::Type("localdatetime({...}) has an out-of-range field".into())
})?;
return Ok(Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}));
}
Err(QueryError::Type(format!(
"localdatetime() doesn't support this argument: {arg:?}"
)))
}
pub(crate) const DATE_TIME_ALLOWED_KEYS: &[&str] = &[
"year",
"month",
"day",
"week",
"dayOfWeek",
"ordinalDay",
"quarter",
"dayOfQuarter",
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
"timezone",
"date",
"time",
"datetime",
];
pub(crate) fn date_time_builtin(
args: &[Value],
now: temporal::NowSnapshot,
) -> Result<Value, QueryError> {
if args.len() > 1 {
return Err(QueryError::Semantic(format!(
"datetime() expects zero or one argument, got {}",
args.len()
)));
}
let Some(arg) = args.first() else {
return Ok(Value::Property(PropertyValue::DateTime {
epoch_seconds: now.epoch_seconds,
nanos: now.nanos,
zone: GraphTzId::Offset(0),
}));
};
if matches!(arg, Value::Null) {
return Ok(Value::Null);
}
if let Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone,
}) = arg
{
return Ok(Value::Property(PropertyValue::DateTime {
epoch_seconds: *epoch_seconds,
nanos: *nanos,
zone: zone.clone(),
}));
}
if let Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}) = arg
{
return Ok(Value::Property(PropertyValue::DateTime {
epoch_seconds: *epoch_seconds,
nanos: *nanos,
zone: GraphTzId::Offset(0),
}));
}
if let Some(s) = as_arith_str(arg) {
let (epoch_seconds, nanos, zone) = temporal::parse_date_time(s).ok_or_else(|| {
QueryError::Type(format!("'{s}' isn't a date-time string MarsDB can parse"))
})?;
return Ok(Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone: tz_to_graph(zone),
}));
}
if let Value::Map(m) = arg {
let (year, month, day) = calendar_fields_from_map("datetime", m, DATE_TIME_ALLOWED_KEYS)?;
let epoch_day = temporal::epoch_day_from_ymd(year, month, day);
let (hour, minute, second, nanos, zone) = clock_fields_from_map(m, epoch_day)?;
let zone = zone.map_or(temporal::TzId::Offset(0), |(z, _)| z);
let (epoch_seconds, nanos) = temporal::date_time_from_fields(
temporal::CalendarDateTime {
year,
month,
day,
hour,
minute,
second,
nanos,
},
&zone,
)
.ok_or_else(|| QueryError::Type("datetime({...}) has an out-of-range field".into()))?;
return Ok(Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone: tz_to_graph(zone),
}));
}
Err(QueryError::Type(format!(
"datetime() doesn't support this argument: {arg:?}"
)))
}
pub(crate) fn between_operand(name: &str, v: &Value) -> Result<BetweenOperand, QueryError> {
match v {
Value::Property(PropertyValue::Date(d)) => Ok((Some(*d), None, None)),
Value::Property(PropertyValue::LocalTime(n)) => Ok((None, Some(*n), None)),
Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}) => Ok((
None,
Some(*nanos_of_day),
Some(temporal::TzId::Offset(*offset_seconds)),
)),
Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}) => {
let (d, n) = temporal::split_epoch_seconds(*epoch_seconds);
Ok((Some(d), Some(n + *nanos as i64), None))
}
Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone,
}) => {
let tz = tz_from_graph(zone);
let offset_seconds = temporal::resolve_offset(&tz, *epoch_seconds);
let local = epoch_seconds + offset_seconds as i64;
let (d, n) = temporal::split_epoch_seconds(local);
Ok((Some(d), Some(n + *nanos as i64), Some(tz)))
}
other => Err(QueryError::Type(format!(
"{name}() needs a Date, LocalTime, Time, LocalDateTime, or DateTime, got {other:?}"
))),
}
}
pub(crate) type BetweenOperand = (Option<i64>, Option<i64>, Option<temporal::TzId>);
pub(crate) type BetweenFn = fn(
Option<i64>,
Option<i64>,
Option<&temporal::TzId>,
Option<i64>,
Option<i64>,
Option<&temporal::TzId>,
) -> temporal::DurationParts;
pub(crate) fn duration_between_builtin(
name: &str,
args: &[Value],
f: BetweenFn,
) -> Result<Value, QueryError> {
if args.len() != 2 {
return Err(QueryError::Semantic(format!(
"{name}() expects exactly two arguments, got {}",
args.len()
)));
}
if matches!(args[0], Value::Null) || matches!(args[1], Value::Null) {
return Ok(Value::Null);
}
let (a_date, a_time, a_zone) = between_operand(name, &args[0])?;
let (b_date, b_time, b_zone) = between_operand(name, &args[1])?;
Ok(duration_value(f(
a_date,
a_time,
a_zone.as_ref(),
b_date,
b_time,
b_zone.as_ref(),
)))
}
pub(crate) type TruncateArgs<'a> = (&'a str, &'a Value, Option<&'a BTreeMap<String, Value>>);
pub(crate) fn parse_truncate_args<'a>(
name: &str,
args: &'a [Value],
) -> Result<TruncateArgs<'a>, QueryError> {
if args.len() < 2 || args.len() > 3 {
return Err(QueryError::Semantic(format!(
"{name}() expects 2 or 3 arguments, got {}",
args.len()
)));
}
let unit = as_arith_str(&args[0]).ok_or_else(|| {
QueryError::Type(format!("{name}()'s first argument must be a unit string"))
})?;
let map = match args.get(2) {
None | Some(Value::Null) => None,
Some(Value::Map(m)) => Some(m),
Some(other) => {
return Err(QueryError::Type(format!(
"{name}()'s third argument must be a map, got {other:?}"
)))
}
};
Ok((unit, &args[1], map))
}
pub(crate) fn apply_date_overrides(
base_epoch_day: i64,
map: Option<&BTreeMap<String, Value>>,
) -> Result<i64, QueryError> {
let base_y = temporal::date_component(base_epoch_day, "year").unwrap();
let base_m = temporal::date_component(base_epoch_day, "month").unwrap();
let base_d = temporal::date_component(base_epoch_day, "day").unwrap();
let Some(m) = map else {
return Ok(base_epoch_day);
};
let year = int_field(m, "year", base_y)?;
let month_raw = int_field(m, "month", base_m)?;
let month = u32::try_from(month_raw)
.map_err(|_| QueryError::Type(format!("'month' is out of range: {month_raw}")))?;
let day_raw = int_field(m, "day", base_d)?;
let day = u32::try_from(day_raw)
.map_err(|_| QueryError::Type(format!("'day' is out of range: {day_raw}")))?;
let result = temporal::epoch_day_from_ymd(year, month, day).ok_or_else(|| {
QueryError::Type(format!(
"{year:04}-{month:02}-{day:02} isn't a valid calendar date"
))
})?;
match m.get("dayOfWeek") {
None => Ok(result),
Some(v) => {
let dow = value_as_i64(v)
.ok_or_else(|| QueryError::Type("'dayOfWeek' must be an integer".into()))?;
temporal::set_iso_weekday(result, dow).ok_or_else(|| {
QueryError::Type(format!(
"'dayOfWeek' must be 1..7 (Monday..Sunday), got {dow}"
))
})
}
}
}
pub(crate) fn apply_time_overrides(
base_nanos_of_day: i64,
map: Option<&BTreeMap<String, Value>>,
) -> Result<i64, QueryError> {
let base_h = temporal::local_time_component(base_nanos_of_day, "hour").unwrap();
let base_min = temporal::local_time_component(base_nanos_of_day, "minute").unwrap();
let base_s = temporal::local_time_component(base_nanos_of_day, "second").unwrap();
let base_ns = temporal::local_time_component(base_nanos_of_day, "nanosecond").unwrap();
let Some(m) = map else {
return Ok(base_nanos_of_day);
};
let nanos = sub_second_nanos_from_map(base_ns, m)?;
let hour = int_field(m, "hour", base_h)?;
let minute = int_field(m, "minute", base_min)?;
let second = int_field(m, "second", base_s)?;
temporal::local_time_nanos_from_fields(hour, minute, second, nanos)
.ok_or_else(|| QueryError::Type("truncate(...)'s map has an out-of-range field".into()))
}
pub(crate) fn validate_truncate_map_keys(
name: &str,
map: Option<&BTreeMap<String, Value>>,
allowed: &[&str],
) -> Result<(), QueryError> {
let Some(m) = map else { return Ok(()) };
if let Some(bad) = m.keys().find(|k| !allowed.contains(&k.as_str())) {
return Err(QueryError::Type(format!(
"{name}(...)'s map has an unrecognized field '{bad}'"
)));
}
Ok(())
}
pub(crate) fn date_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
let (unit, other, map) = parse_truncate_args("date.truncate", args)?;
validate_truncate_map_keys("date.truncate", map, &["year", "month", "day", "dayOfWeek"])?;
if matches!(other, Value::Null) {
return Ok(Value::Null);
}
let (base_date, _, _) = between_operand("date.truncate", other)?;
let base_date = base_date.ok_or_else(|| {
QueryError::Type(
"date.truncate() needs a value with a calendar date (Date, LocalDateTime, or DateTime)"
.into(),
)
})?;
let truncated = temporal::truncate_date_unit(base_date, unit).ok_or_else(|| {
QueryError::Type(format!(
"date.truncate(): '{unit}' isn't a recognized date unit"
))
})?;
Ok(Value::Property(PropertyValue::Date(apply_date_overrides(
truncated, map,
)?)))
}
pub(crate) const TIME_TRUNCATE_MAP_KEYS: &[&str] = &[
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
];
pub(crate) fn local_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
let (unit, other, map) = parse_truncate_args("localtime.truncate", args)?;
validate_truncate_map_keys("localtime.truncate", map, TIME_TRUNCATE_MAP_KEYS)?;
if matches!(other, Value::Null) {
return Ok(Value::Null);
}
let (_, base_time, _) = between_operand("localtime.truncate", other)?;
let base_time = base_time.ok_or_else(|| {
QueryError::Type(
"localtime.truncate() needs a value with a time-of-day (LocalTime, Time, \
LocalDateTime, or DateTime)"
.into(),
)
})?;
let truncated = temporal::truncate_time_unit(base_time, unit).ok_or_else(|| {
QueryError::Type(format!(
"localtime.truncate(): '{unit}' isn't a recognized time unit"
))
})?;
Ok(Value::Property(PropertyValue::LocalTime(
apply_time_overrides(truncated, map)?,
)))
}
pub(crate) fn time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
let (unit, other, map) = parse_truncate_args("time.truncate", args)?;
validate_truncate_map_keys(
"time.truncate",
map,
&[
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
"timezone",
],
)?;
if matches!(other, Value::Null) {
return Ok(Value::Null);
}
let (_, base_time, base_offset) = between_operand("time.truncate", other)?;
let base_time = base_time.ok_or_else(|| {
QueryError::Type(
"time.truncate() needs a value with a time-of-day (LocalTime, Time, LocalDateTime, \
or DateTime)"
.into(),
)
})?;
let truncated = temporal::truncate_time_unit(base_time, unit).ok_or_else(|| {
QueryError::Type(format!(
"time.truncate(): '{unit}' isn't a recognized time unit"
))
})?;
let nanos_of_day = apply_time_overrides(truncated, map)?;
let offset_seconds = match map.and_then(|m| m.get("timezone")) {
Some(v) => match timezone_value_to_tzid(v)? {
temporal::TzId::Offset(o) => o,
temporal::TzId::Named(name) => {
return Err(QueryError::Type(format!(
"'timezone': '{name}' looks like a named timezone (e.g. 'Europe/Stockholm') -- TIME has \
no calendar date to resolve a named zone's DST-dependent offset against, only a fixed \
UTC offset like '+01:00' is supported"
)));
}
},
None => match base_offset {
Some(temporal::TzId::Offset(o)) => o,
_ => 0,
},
};
Ok(Value::Property(PropertyValue::Time {
nanos_of_day,
offset_seconds,
}))
}
pub(crate) fn truncate_date_time(base_date: i64, base_time: i64, unit: &str) -> Option<(i64, i64)> {
if let Some(d) = temporal::truncate_date_unit(base_date, unit) {
Some((d, 0))
} else {
temporal::truncate_time_unit(base_time, unit).map(|t| (base_date, t))
}
}
pub(crate) fn local_date_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
let (unit, other, map) = parse_truncate_args("localdatetime.truncate", args)?;
validate_truncate_map_keys(
"localdatetime.truncate",
map,
&[
"year",
"month",
"day",
"dayOfWeek",
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
],
)?;
if matches!(other, Value::Null) {
return Ok(Value::Null);
}
let (base_date, base_time, _) = between_operand("localdatetime.truncate", other)?;
let base_date = base_date.ok_or_else(|| {
QueryError::Type(
"localdatetime.truncate() needs a value with a calendar date (Date, LocalDateTime, \
or DateTime)"
.into(),
)
})?;
let (trunc_date, trunc_time) = truncate_date_time(base_date, base_time.unwrap_or(0), unit)
.ok_or_else(|| {
QueryError::Type(format!(
"localdatetime.truncate(): '{unit}' isn't a recognized unit"
))
})?;
let final_date = apply_date_overrides(trunc_date, map)?;
let final_time = apply_time_overrides(trunc_time, map)?;
let (epoch_seconds, nanos) = temporal::combine_date_and_time(final_date, final_time);
Ok(Value::Property(PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
}))
}
pub(crate) fn date_time_truncate_builtin(args: &[Value]) -> Result<Value, QueryError> {
let (unit, other, map) = parse_truncate_args("datetime.truncate", args)?;
validate_truncate_map_keys(
"datetime.truncate",
map,
&[
"year",
"month",
"day",
"dayOfWeek",
"hour",
"minute",
"second",
"millisecond",
"microsecond",
"nanosecond",
"timezone",
],
)?;
if matches!(other, Value::Null) {
return Ok(Value::Null);
}
let (base_date, base_time, base_offset) = between_operand("datetime.truncate", other)?;
let base_date = base_date.ok_or_else(|| {
QueryError::Type(
"datetime.truncate() needs a value with a calendar date (Date, LocalDateTime, or \
DateTime)"
.into(),
)
})?;
let (trunc_date, trunc_time) = truncate_date_time(base_date, base_time.unwrap_or(0), unit)
.ok_or_else(|| {
QueryError::Type(format!(
"datetime.truncate(): '{unit}' isn't a recognized unit"
))
})?;
let final_date = apply_date_overrides(trunc_date, map)?;
let final_time = apply_time_overrides(trunc_time, map)?;
let zone = match map.and_then(|m| m.get("timezone")) {
Some(v) => timezone_value_to_tzid(v)?,
None => base_offset.unwrap_or(temporal::TzId::Offset(0)),
};
let calendar = temporal::CalendarDateTime {
year: temporal::date_component(final_date, "year").unwrap(),
month: temporal::date_component(final_date, "month").unwrap() as u32,
day: temporal::date_component(final_date, "day").unwrap() as u32,
hour: temporal::local_time_component(final_time, "hour").unwrap(),
minute: temporal::local_time_component(final_time, "minute").unwrap(),
second: temporal::local_time_component(final_time, "second").unwrap(),
nanos: temporal::local_time_component(final_time, "nanosecond").unwrap(),
};
let (epoch_seconds, nanos) =
temporal::date_time_from_fields(calendar, &zone).ok_or_else(|| {
QueryError::Type("datetime.truncate() produced an out-of-range value".into())
})?;
Ok(Value::Property(PropertyValue::DateTime {
epoch_seconds,
nanos,
zone: tz_to_graph(zone),
}))
}
pub(crate) fn is_temporal_property_value(pv: &PropertyValue) -> bool {
matches!(
pv,
PropertyValue::Date(_)
| PropertyValue::Duration { .. }
| PropertyValue::LocalTime(_)
| PropertyValue::Time { .. }
| PropertyValue::LocalDateTime { .. }
| PropertyValue::DateTime { .. }
)
}
pub(crate) fn temporal_component(pv: &PropertyValue, prop: &str) -> Option<PropertyValue> {
match pv {
PropertyValue::Date(d) => temporal::date_component(*d, prop).map(PropertyValue::Int),
PropertyValue::Duration {
months,
days,
seconds,
nanos,
} => temporal::duration_component(*months, *days, *seconds, *nanos, prop)
.map(PropertyValue::Int),
PropertyValue::LocalTime(nanos_of_day) => {
temporal::local_time_component(*nanos_of_day, prop).map(PropertyValue::Int)
}
PropertyValue::Time {
nanos_of_day,
offset_seconds,
} => time_component(*nanos_of_day, *offset_seconds, prop),
PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
} => date_time_component(*epoch_seconds, *nanos, None, prop),
PropertyValue::DateTime {
epoch_seconds,
nanos,
zone,
} => date_time_component(*epoch_seconds, *nanos, Some(&tz_from_graph(zone)), prop),
_ => None,
}
}
pub(crate) fn time_component(
nanos_of_day: i64,
offset_seconds: i32,
prop: &str,
) -> Option<PropertyValue> {
match prop {
"timezone" | "offset" => Some(PropertyValue::String(temporal::format_offset(
offset_seconds,
))),
"offsetSeconds" => Some(PropertyValue::Int(offset_seconds as i64)),
"offsetMinutes" => Some(PropertyValue::Int(offset_seconds as i64 / 60)),
_ => temporal::local_time_component(nanos_of_day, prop).map(PropertyValue::Int),
}
}
pub(crate) fn date_time_component(
epoch_seconds: i64,
nanos: i32,
zone: Option<&temporal::TzId>,
prop: &str,
) -> Option<PropertyValue> {
if let Some(zone) = zone {
let offset_seconds = temporal::resolve_offset(zone, epoch_seconds);
match prop {
"timezone" => {
let text = match zone {
temporal::TzId::Named(name) => name.clone(),
temporal::TzId::Offset(_) => temporal::format_offset(offset_seconds),
};
return Some(PropertyValue::String(text));
}
"offset" => {
return Some(PropertyValue::String(temporal::format_offset(
offset_seconds,
)))
}
"offsetSeconds" => return Some(PropertyValue::Int(offset_seconds as i64)),
"offsetMinutes" => return Some(PropertyValue::Int(offset_seconds as i64 / 60)),
"epochSeconds" => return Some(PropertyValue::Int(epoch_seconds)),
"epochMillis" => {
return Some(PropertyValue::Int(
temporal::epoch_seconds_and_millis(epoch_seconds, nanos).1,
))
}
_ => {}
}
}
let offset_seconds = zone.map_or(0, |z| temporal::resolve_offset(z, epoch_seconds));
let local_epoch_seconds = epoch_seconds + offset_seconds as i64;
temporal::date_time_calendar_component(local_epoch_seconds, prop)
.or_else(|| temporal::date_time_clock_component(local_epoch_seconds, nanos, prop))
.map(PropertyValue::Int)
}