///|
/// One exact calendar day. This type is used only as a conservative bound;
/// it must not be confused with the precision of the source EDTF value.
pub(all) struct CalendarDay {
year : Int
month : Int
day : Int
} derive(Eq, @debug.Debug)
///|
pub fn CalendarDay::to_string(self : CalendarDay) -> String {
exact_year_to_string(self.year) +
"-" +
pad4(self.month)[2:4].to_owned() +
"-" +
pad4(self.day)[2:4].to_owned()
}
///|
/// Inclusive lower and upper bounds of an unqualified date or closed
/// interval. An envelope can include days that the source expression does not
/// actually denote; callers must not treat it as a complete date expansion.
pub(all) struct DateEnvelope {
earliest : CalendarDay
latest : CalendarDay
} derive(Eq, @debug.Debug)
///|
pub(all) enum WindowRelation {
Outside
Within
PossibleOverlap
} derive(Eq, @debug.Debug)
///|
fn calendar_compare(a : CalendarDay, b : CalendarDay) -> Int {
if a.year != b.year {
return if a.year < b.year { -1 } else { 1 }
}
if a.month != b.month {
return if a.month < b.month { -1 } else { 1 }
}
if a.day != b.day {
return if a.day < b.day { -1 } else { 1 }
}
0
}
///|
fn is_qualified(date : EdtfDate) -> Bool {
date.year_qualifier.uncertain ||
date.year_qualifier.approximate ||
date.month_qualifier.uncertain ||
date.month_qualifier.approximate ||
date.day_qualifier.uncertain ||
date.day_qualifier.approximate
}
///|
fn year_bounds(year : YearSpec) -> (Int, Int) {
match year {
Exact(value) => (value, value)
Masked(prefix, x_count) => {
let mut factor = 1
for _ in 0.. CalendarDay raise EdtfError {
if is_qualified(date) {
// An uncertain or approximate label has no defensible numeric margin.
raise Unsupported("ENVELOPE_QUALIFIED", 0)
}
let (first_year, last_year) = year_bounds(date.year)
let year = if latest { last_year } else { first_year }
match date.month {
Unspecified | Masked => {
if date.day is Exact(_) {
raise Unsupported("ENVELOPE_INVALID_MODEL", 0)
}
{
year,
month: if latest {
12
} else {
1
},
day: if latest {
31
} else {
1
},
}
}
Season(_) => raise Unsupported("ENVELOPE_SEASON", 0)
Exact(month) => {
if date.year is Masked(_, _) || month < 1 || month > 12 {
raise Unsupported("ENVELOPE_INVALID_MODEL", 0)
}
let day = match date.day {
Unspecified | Masked => if latest { month_days(year, month) } else { 1 }
Exact(value) => {
if value < 1 || value > month_days(year, month) {
raise Unsupported("ENVELOPE_INVALID_MODEL", 0)
}
value
}
}
{ year, month, day, }
}
}
}
///|
/// Give a safe inclusive envelope for a parser-produced date or closed
/// interval. Masked digits widen the bound; missing components widen it to
/// their entire calendar period. Qualified dates, seasons, open/unknown
/// endpoints are refused rather than guessed. Sets and ranges receive only
/// their outer envelope, which can include gaps with no actual member.
pub fn date_envelope(value : EdtfValue) -> DateEnvelope raise EdtfError {
match value {
Date(date) => {
let earliest = date_edge(date, false)
let latest = date_edge(date, true)
{ earliest, latest, }
}
Interval(interval) =>
match (interval.start, interval.end) {
(Known(start), Known(end)) => {
let earliest = date_edge(start, false)
let latest = date_edge(end, true)
if calendar_compare(earliest, latest) > 0 {
raise Unsupported("ENVELOPE_REVERSED_INTERVAL", 0)
}
{ earliest, latest, }
}
_ => raise Unsupported("ENVELOPE_UNBOUNDED", 0)
}
DateTime(_) => raise Unsupported("ENVELOPE_DATETIME", 0)
Range(start, end) => {
let earliest = date_edge(start, false)
let latest = date_edge(end, true)
if calendar_compare(earliest, latest) > 0 {
raise Unsupported("ENVELOPE_REVERSED_RANGE", 0)
}
{ earliest, latest, }
}
Set(items) | AllSet(items) => {
if items.length() == 0 {
raise Unsupported("ENVELOPE_EMPTY_SET", 0)
}
let first = date_envelope(items[0])
let mut earliest = first.earliest
let mut latest = first.latest
for i = 1; i < items.length(); i = i + 1 {
let bounds = date_envelope(items[i])
if calendar_compare(bounds.earliest, earliest) < 0 {
earliest = bounds.earliest
}
if calendar_compare(bounds.latest, latest) > 0 {
latest = bounds.latest
}
}
{ earliest, latest, }
}
}
}
///|
fn exact_query_day(value : EdtfValue) -> CalendarDay raise EdtfError {
match value {
Date(date) => {
if is_qualified(date) {
raise Unsupported("WINDOW_NEEDS_EXACT_DAY", 0)
}
match (date.year, date.month, date.day) {
(Exact(year), Exact(month), Exact(day)) => { year, month, day, }
_ => raise Unsupported("WINDOW_NEEDS_EXACT_DAY", 0)
}
}
_ => raise Unsupported("WINDOW_NEEDS_EXACT_DAY", 0)
}
}
///|
/// Classify an EDTF value against an inclusive exact-day search window.
/// `PossibleOverlap` means only the conservative envelope intersects, not
/// that a particular day is known to occur. The query endpoints must each be
/// a complete, unqualified `YYYY-MM-DD` EDTF date.
pub fn window_relation(
value : EdtfValue,
query_start : String,
query_end : String,
) -> WindowRelation raise EdtfError {
let start = exact_query_day(parse(query_start))
let end = exact_query_day(parse(query_end))
if calendar_compare(start, end) > 0 {
raise Unsupported("WINDOW_REVERSED", 0)
}
let envelope = date_envelope(value)
relation_for_envelope(envelope, start, end)
}
///|
fn relation_for_envelope(
envelope : DateEnvelope,
start : CalendarDay,
end : CalendarDay,
) -> WindowRelation {
if calendar_compare(envelope.latest, start) < 0 ||
calendar_compare(envelope.earliest, end) > 0 {
Outside
} else if calendar_compare(envelope.earliest, start) >= 0 &&
calendar_compare(envelope.latest, end) <= 0 {
Within
} else {
PossibleOverlap
}
}