///|
pub(all) enum Granularity {
Annual
HalfYear
Quarterly
Monthly
Daily
} derive(Eq, @debug.Debug)
///|
pub extend Granularity with Eq::{equal, not_equal}
///|
pub extend Granularity with @debug.Debug::{to_repr}
///|
pub struct PeriodValue {
priv code : String
priv granularity : Granularity
priv start_day : Int32
priv end_day : Int32
} derive(Eq, @debug.Debug)
///|
pub extend PeriodValue with Eq::{equal, not_equal}
///|
pub extend PeriodValue with @debug.Debug::{to_repr}
///|
fn leap_year(year : Int32) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
fn month_days(year : Int32, month : Int32) -> Int32 {
match month {
2 => if leap_year(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
// 0001-01-01 is day zero; only the end sentinel may use year 10000.
fn civil_day(year : Int32, month : Int32, day : Int32) -> Int32 {
let y = year - 1
let mut ordinal = 365 * y + y / 4 - y / 100 + y / 400 + day - 1
for m = 1; m < month; m = m + 1 {
ordinal += month_days(year, m)
}
ordinal
}
///|
fn period_digits(
text : String,
start : Int32,
end : Int32,
) -> Int32 raise SdmxError {
let mut value = 0
for i = start; i < end; i = i + 1 {
let n = text[i].to_int()
if n < 48 || n > 57 {
raise Invalid(path="period", reason="expected decimal digits")
}
value = value * 10 + n - 48
}
value
}
///|
pub fn PeriodValue::parse(text : String) -> PeriodValue raise SdmxError {
let size = text.length()
if size != 4 && size != 7 && size != 10 {
raise Unsupported(
feature="period outside Gregorian Jan-1 year/half/quarter/month/day profile",
)
}
let year = period_digits(text, 0, 4)
if year == 0 {
raise Invalid(path="period.year", reason="year must be 0001..9999")
}
if size == 4 {
return {
code: text,
granularity: Annual,
start_day: civil_day(year, 1, 1),
end_day: civil_day(year + 1, 1, 1),
}
}
if text[4] != '-' {
raise Invalid(path="period", reason="expected '-' after year")
}
if size == 7 && (text[5] == 'Q' || text[5] == 'S') {
let part = period_digits(text, 6, 7)
let step = if text[5] == 'Q' { 3 } else { 6 }
let maximum = 12 / step
if part < 1 || part > maximum {
raise Invalid(path="period", reason="quarter/half out of range")
}
let month = (part - 1) * step + 1
let end_month = month + step
let end_day = if end_month > 12 {
civil_day(year + 1, 1, 1)
} else {
civil_day(year, end_month, 1)
}
return {
code: text,
granularity: if step == 3 {
Quarterly
} else {
HalfYear
},
start_day: civil_day(year, month, 1),
end_day,
}
}
let month = period_digits(text, 5, 7)
if month < 1 || month > 12 {
raise Invalid(path="period.month", reason="month out of range")
}
if size == 7 {
let end_day = if month == 12 {
civil_day(year + 1, 1, 1)
} else {
civil_day(year, month + 1, 1)
}
return {
code: text,
granularity: Monthly,
start_day: civil_day(year, month, 1),
end_day,
}
}
if text[7] != '-' {
raise Invalid(path="period", reason="expected '-' before day")
}
let day = period_digits(text, 8, 10)
if day < 1 || day > month_days(year, month) {
raise Invalid(path="period.day", reason="day out of range")
}
let start_day = civil_day(year, month, day)
{ code: text, granularity: Daily, start_day, end_day: start_day + 1, }
}
///|
pub fn PeriodValue::code(self : PeriodValue) -> String {
self.code
}
///|
pub fn PeriodValue::granularity(self : PeriodValue) -> Granularity {
self.granularity
}
///|
pub fn PeriodValue::day_bounds(self : PeriodValue) -> (Int32, Int32) {
(self.start_day, self.end_day)
}
///|
pub fn PeriodValue::overlaps(self : PeriodValue, other : PeriodValue) -> Bool {
self.start_day < other.end_day && other.start_day < self.end_day
}
///|
pub fn PeriodValue::contains_period(
self : PeriodValue,
other : PeriodValue,
) -> Bool {
self.start_day <= other.start_day && self.end_day >= other.end_day
}
///|
pub fn PeriodValue::compare_period(
self : PeriodValue,
other : PeriodValue,
) -> Int32 {
let first = self.start_day.compare(other.start_day)
if first != 0 {
first
} else {
self.end_day.compare(other.end_day)
}
}
///|
pub fn PeriodValue::frequency(self : PeriodValue) -> String {
match self.granularity {
Annual => "A"
HalfYear => "S"
Quarterly => "Q"
Monthly => "M"
Daily => "D"
}
}