///|
/// A calendar-aware ISO 8601 duration. Years and months are retained as
/// calendar units; weeks, days and clock fields can also be reduced to seconds.
pub struct Duration {
years : Int
months : Int
weeks : Int
days : Int
hours : Int
minutes : Int
seconds : Int
negative : Bool
} derive(Eq, Debug)
///|
pub suberror DurationError {
EmptyDuration
MissingDesignator
MissingNumber(Int)
InvalidCharacter(Int, Int)
DuplicateUnit(String)
UnitOutOfOrder(String)
DateUnitInTimePart(String)
TimeUnitInDatePart(String)
FractionUnsupported
NonFixedCalendarUnit
} derive(Eq, Debug)
///|
pub fn Duration::zero() -> Duration {
{
years: 0,
months: 0,
weeks: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
negative: false,
}
}
///|
pub fn Duration::new(
years? : Int = 0,
months? : Int = 0,
weeks? : Int = 0,
days? : Int = 0,
hours? : Int = 0,
minutes? : Int = 0,
seconds? : Int = 0,
negative? : Bool = false,
) -> Duration {
{ years, months, weeks, days, hours, minutes, seconds, negative }
}
///|
pub fn Duration::is_zero(self : Duration) -> Bool {
self.years == 0 &&
self.months == 0 &&
self.weeks == 0 &&
self.days == 0 &&
self.hours == 0 &&
self.minutes == 0 &&
self.seconds == 0
}
///|
pub fn Duration::has_calendar_units(self : Duration) -> Bool {
self.years != 0 || self.months != 0
}
///|
pub fn Duration::sign(self : Duration) -> Int {
if self.negative && !self.is_zero() {
-1
} else {
1
}
}
///|
pub fn Duration::fixed_seconds(self : Duration) -> Int64 raise DurationError {
if self.has_calendar_units() {
raise NonFixedCalendarUnit
}
let magnitude = self.weeks.to_int64() * 604800L +
self.days.to_int64() * 86400L +
self.hours.to_int64() * 3600L +
self.minutes.to_int64() * 60L +
self.seconds.to_int64()
if self.negative {
-magnitude
} else {
magnitude
}
}
///|
fn duration_unit_name(code : Int, in_time : Bool) -> String {
match code {
'Y' => "years"
'M' => if in_time { "minutes" } else { "months" }
'W' => "weeks"
'D' => "days"
'H' => "hours"
'S' => "seconds"
_ => "unknown"
}
}
///|
/// Parse the integer subset of ISO 8601 durations, including leading minus.
/// Supported examples include P2W, P1Y2M3DT4H5M6S and -PT90S.
pub fn parse_duration(source : String) -> Duration raise DurationError {
let text = source.trim().to_upper()
if text.length() == 0 {
raise EmptyDuration
}
let mut cursor = 0
let mut negative = false
if text.code_unit_at(cursor) == '-' {
negative = true
cursor += 1
} else if text.code_unit_at(cursor) == '+' {
cursor += 1
}
if cursor >= text.length() || text.code_unit_at(cursor) != 'P' {
raise MissingDesignator
}
cursor += 1
let mut in_time = false
let mut saw_value = false
let mut pending = 0
let mut has_pending = false
let mut last_date_order = 0
let mut last_time_order = 0
let mut years = 0
let mut months = 0
let mut weeks = 0
let mut days = 0
let mut hours = 0
let mut minutes = 0
let mut seconds = 0
while cursor < text.length() {
let code = text.code_unit_at(cursor).to_int()
if code >= '0' && code <= '9' {
pending = pending * 10 + code - '0'
has_pending = true
cursor += 1
continue
}
if code == '.' || code == ',' {
raise FractionUnsupported
}
if code == 'T' {
if in_time || has_pending {
raise InvalidCharacter(cursor, code)
}
in_time = true
cursor += 1
continue
}
if !has_pending {
raise MissingNumber(cursor)
}
let name = duration_unit_name(code, in_time)
if name == "unknown" {
raise InvalidCharacter(cursor, code)
}
if in_time {
let order = match code {
'H' => 1
'M' => 2
'S' => 3
'Y' | 'W' | 'D' => raise DateUnitInTimePart(name)
_ => raise InvalidCharacter(cursor, code)
}
if order <= last_time_order {
if order == last_time_order {
raise DuplicateUnit(name)
}
raise UnitOutOfOrder(name)
}
last_time_order = order
match code {
'H' => hours = pending
'M' => minutes = pending
'S' => seconds = pending
_ => ()
}
} else {
let order = match code {
'Y' => 1
'M' => 2
'W' => 3
'D' => 4
'H' | 'S' => raise TimeUnitInDatePart(name)
_ => raise InvalidCharacter(cursor, code)
}
if order <= last_date_order {
if order == last_date_order {
raise DuplicateUnit(name)
}
raise UnitOutOfOrder(name)
}
last_date_order = order
match code {
'Y' => years = pending
'M' => months = pending
'W' => weeks = pending
'D' => days = pending
_ => ()
}
}
pending = 0
has_pending = false
saw_value = true
cursor += 1
}
if has_pending {
raise MissingDesignator
}
if !saw_value {
raise EmptyDuration
}
{ years, months, weeks, days, hours, minutes, seconds, negative }
}
///|
fn append_duration_part(
parts : Array[String],
value : Int,
suffix : String,
) -> Unit {
if value != 0 {
parts.push(value.to_string() + suffix)
}
}
///|
pub fn Duration::to_iso_string(self : Duration) -> String {
if self.is_zero() {
return "PT0S"
}
let date_parts : Array[String] = []
append_duration_part(date_parts, self.years, "Y")
append_duration_part(date_parts, self.months, "M")
append_duration_part(date_parts, self.weeks, "W")
append_duration_part(date_parts, self.days, "D")
let time_parts : Array[String] = []
append_duration_part(time_parts, self.hours, "H")
append_duration_part(time_parts, self.minutes, "M")
append_duration_part(time_parts, self.seconds, "S")
let prefix = if self.negative { "-P" } else { "P" }
prefix +
date_parts.join("") +
(if time_parts.length() > 0 { "T" + time_parts.join("") } else { "" })
}
///|
pub fn DateTime::add_duration(self : DateTime, duration : Duration) -> DateTime {
let factor = duration.sign()
let calendar_shifted = self
.add_years(factor * duration.years)
.add_months(factor * duration.months)
.add_days(factor * (duration.weeks * 7 + duration.days))
let clock_seconds = duration.hours.to_int64() * 3600L +
duration.minutes.to_int64() * 60L +
duration.seconds.to_int64()
calendar_shifted.add_seconds(factor.to_int64() * clock_seconds)
}
///|
pub fn duration_between(start : DateTime, finish : DateTime) -> Duration {
let delta = finish.to_epoch_second() - start.to_epoch_second()
let negative = delta < 0L
let mut remaining = if negative { -delta } else { delta }
let weeks = (remaining / 604800L).to_int()
remaining = remaining % 604800L
let days = (remaining / 86400L).to_int()
remaining = remaining % 86400L
let hours = (remaining / 3600L).to_int()
remaining = remaining % 3600L
let minutes = (remaining / 60L).to_int()
let seconds = (remaining % 60L).to_int()
{ years: 0, months: 0, weeks, days, hours, minutes, seconds, negative }
}
///|
/// Produce a compact English explanation suitable for reports and CLI output.
pub fn Duration::explain_en(self : Duration) -> String {
if self.is_zero() {
return "zero seconds"
}
let parts : Array[String] = []
let append = fn(value : Int, singular : String) {
if value > 0 {
parts.push(
value.to_string() + " " + singular + (if value == 1 { "" } else { "s" }),
)
}
}
append(self.years, "year")
append(self.months, "month")
append(self.weeks, "week")
append(self.days, "day")
append(self.hours, "hour")
append(self.minutes, "minute")
append(self.seconds, "second")
(if self.negative { "minus " } else { "" }) + parts.join(", ")
}
///|
pub fn Duration::explain_zh(self : Duration) -> String {
if self.is_zero() {
return "0 秒"
}
let parts : Array[String] = []
let append = fn(value : Int, unit : String) {
if value > 0 {
parts.push(value.to_string() + " " + unit)
}
}
append(self.years, "年")
append(self.months, "个月")
append(self.weeks, "周")
append(self.days, "天")
append(self.hours, "小时")
append(self.minutes, "分钟")
append(self.seconds, "秒")
(if self.negative { "负 " } else { "" }) + parts.join(" ")
}