///|
// Keep the existing scalar metadata while storing multi-value selections
// privately. No caller can mutate the internal array through a public field.
fn new_date_locator(
kind : LocatorKind,
interval : Int32,
selection : Array[Int32]?,
secondary : Int32,
) -> DateLocator {
{
kind,
interval,
selector: match selection {
Some([first, ..]) => first
_ => 0
},
secondary,
selector_enabled: selection is Some(_),
offset_minutes: 0,
timezone: None,
selection,
secondary_selection: None,
holidays: [],
rule: None,
automatic: None,
wrapper_locator: None,
microsecond_object: None,
}
}
///|
// Store holidays as exact Gregorian day keys. Times, offsets, duplicates and
// caller-side array mutations must not affect a constructed locator.
fn business_holiday_days(holidays : Array[DateTime]?) -> Array[Int32] {
let result : Array[Int32] = []
match holidays {
Some(values) =>
for value in values {
let key = days_from_civil(value.year, value.month, value.day)
if !result.contains(key) {
result.push(key)
}
}
None => ()
}
result.sort()
result
}
///|
fn positive_interval(value : Int32) -> Int32 raise DateError {
if value < 1 {
raise DateError::InvalidDate(message="locator interval must be positive")
}
value
}
///|
fn month_selector(value : Int32) -> Int32 raise DateError {
if value < 1 || value > 12 {
raise DateError::InvalidDate(message="month selector must be in 1..12")
}
value
}
///|
fn day_selector(value : Int32) -> Int32 raise DateError {
if value < 1 || value > 31 {
raise DateError::InvalidDate(message="day selector must be in 1..31")
}
value
}
///|
fn monthday_selector(value : Int32) -> Int32 raise DateError {
if value == 0 || value < -31 || value > 31 {
raise DateError::InvalidDate(
message="month day selector must be in -31..-1 or 1..31",
)
}
value
}
///|
fn yearday_selector(value : Int32) -> Int32 raise DateError {
if value == 0 || value < -366 || value > 366 {
raise DateError::InvalidDate(
message="year day selector must be in -366..-1 or 1..366",
)
}
value
}
///|
fn weekno_selector(value : Int32) -> Int32 raise DateError {
if value == 0 || value < -53 || value > 53 {
raise DateError::InvalidDate(
message="week number selector must be in -53..-1 or 1..53",
)
}
value
}
///|
fn easter_offset_selector(value : Int32) -> Int32 raise DateError {
if value < -366 || value > 366 {
raise DateError::InvalidDate(message="Easter offset must be in -366..366")
}
value
}
///|
fn hour_selector(value : Int32) -> Int32 raise DateError {
if value < 0 || value > 23 {
raise DateError::InvalidDate(message="hour selector must be in 0..23")
}
value
}
///|
fn minute_or_second_selector(value : Int32) -> Int32 raise DateError {
if value < 0 || value > 59 {
raise DateError::InvalidDate(message="time selector must be in 0..59")
}
value
}
///|
fn weekday_selector(value : Int32) -> Int32 raise DateError {
if value < 0 || value > 6 {
raise DateError::InvalidDate(
message="weekday selector must be Monday=0 through Sunday=6",
)
}
value
}
///|
// Copy and normalize on construction so caller mutations cannot change a
// locator or a shared axis. None means all fields; empty arrays are rejected.
fn locator_selection(
single : Int32?,
multiple : Array[Int32]?,
wildcard : Int32?,
default_value : Int32?,
validate : (Int32) -> Int32 raise DateError,
) -> Array[Int32]? raise DateError {
let values = match (single, multiple) {
(Some(_), Some(_)) =>
raise DateError::InvalidDate(
message="use either a scalar selector or a selector array",
)
(Some(value), None) =>
if wildcard == Some(value) {
return None
} else {
[value]
}
(None, Some(values)) => values
(None, None) =>
match default_value {
Some(value) => [value]
None => return None
}
}
if values.is_empty() {
raise DateError::InvalidDate(message="selector array must not be empty")
}
let normalized : Array[Int32] = []
for value in values {
let value = validate(value)
if !normalized.contains(value) {
normalized.push(value)
}
}
normalized.sort()
Some(normalized)
}
///|
fn DateLocator::matches(self : DateLocator, value : Int32) -> Bool {
match self.selection {
None => true
Some(values) => values.contains(value)
}
}
///|
fn DateLocator::matches_monthday(self : DateLocator, value : DateTime) -> Bool {
self.matches(value.day) ||
self.matches(value.day - days_in_month(value.year, value.month) - 1)
}
///|
fn DateLocator::matches_business_day(
self : DateLocator,
value : DateTime,
) -> Bool {
self.matches(value.weekday()) &&
!self.holidays.contains(days_from_civil(value.year, value.month, value.day))
}