///|
/// Low-level writers shared by every `toString` implementation.
///
/// The output format is RFC 9557 (ISO 8601 with Temporal's annotation
/// extensions), so the same year, time and fraction writers are reused across
/// dates, times, instants and zoned date-times.
///|
/// Writes a non-negative integer padded to two digits.
fn write_padded_2(buf : StringBuilder, value : Int) -> Unit {
if value < 10 {
buf.write_string("0")
}
buf.write_string(value.to_string())
}
///|
/// Writes an ISO year: four digits when in `0 ..= 9999`, otherwise the signed
/// six-digit extended form.
fn write_year(buf : StringBuilder, year : Int) -> Unit {
if year >= 0 && year <= 9999 {
let s = year.to_string()
for _ in 0..<(4 - s.length()) {
buf.write_string("0")
}
buf.write_string(s)
} else {
buf.write_string(if year < 0 { "-" } else { "+" })
let s = year.abs().to_string()
for _ in 0..<(6 - s.length()) {
buf.write_string("0")
}
buf.write_string(s)
}
}
///|
/// Writes the fractional-second digits of `nanoseconds`, honouring the
/// requested precision.
///
/// With `Auto` precision, trailing zeros are trimmed; with a fixed digit count
/// the field is padded or truncated to exactly that many digits.
fn write_fraction(
buf : StringBuilder,
nanoseconds : Int,
precision : Precision,
) -> Unit {
let digits = nanoseconds.to_string()
let padded = "000000000"[:9 - digits.length()].to_owned() + digits
let width = match precision {
Digit(d) if d >= 0 && d <= 9 => d
// `Auto` keeps every digit up to the last non-zero one.
_ => {
let mut last = 0
for i in 0..<9 {
if padded[i] != '0' {
last = i + 1
}
}
last
}
}
buf.write_string(padded[:width].to_owned())
}
///|
/// Writes a wall-clock time, with or without `:` separators.
///
/// `Minute` precision stops after the minutes field; `Auto` omits the
/// fractional part when the sub-second value is zero.
fn write_time(
buf : StringBuilder,
hour : Int,
minute : Int,
second : Int,
nanosecond : Int,
precision : Precision,
include_sep : Bool,
) -> Unit {
write_padded_2(buf, hour)
if include_sep {
buf.write_string(":")
}
write_padded_2(buf, minute)
if precision is Minute {
return
}
if include_sep {
buf.write_string(":")
}
write_padded_2(buf, second)
if (nanosecond == 0 && precision is Auto) || precision is Digit(0) {
return
}
buf.write_string(".")
write_fraction(buf, nanosecond, precision)
}
///|
/// Writes an ISO date as `YYYY-MM-DD`.
fn write_date(buf : StringBuilder, year : Int, month : Int, day : Int) -> Unit {
write_year(buf, year)
buf.write_string("-")
write_padded_2(buf, month)
buf.write_string("-")
write_padded_2(buf, day)
}
///|
/// Writes the `[u-ca=...]` calendar annotation, if the options call for one.
fn write_calendar_annotation(
buf : StringBuilder,
calendar : Calendar,
show : DisplayCalendar,
) -> Unit {
match show {
Never => ()
// `Auto` prints the annotation only when it carries information.
Auto =>
if calendar != Calendar::ISO {
write_calendar(buf, calendar, false)
}
Always => write_calendar(buf, calendar, false)
Critical => write_calendar(buf, calendar, true)
}
}
///|
fn write_calendar(
buf : StringBuilder,
calendar : Calendar,
critical : Bool,
) -> Unit {
buf.write_string("[")
if critical {
buf.write_string("!")
}
buf.write_string("u-ca=")
buf.write_string(calendar.identifier())
buf.write_string("]")
}
///|
/// Renders the duration using the default options.
///
/// ```mbt check
/// test {
/// inspect(
/// @temporal.Duration::of(years=1, months=2, days=3).to_string(),
/// content="P1Y2M3D",
/// )
/// inspect(@temporal.duration_zero.to_string(), content="PT0S")
/// }
/// ```
pub fn Duration::to_string(self : Duration) -> String {
try! self.to_string_with_options(ToStringRoundingOptions::default())
}
///|
/// `TemporalDurationToString`: renders the duration, first rounding the
/// sub-second part as the options require.
///
/// ```mbt check
/// test {
/// let d = @temporal.Duration::of(seconds=1, milliseconds=500)
/// inspect(
/// d.to_string_with_options(
/// @temporal.ToStringRoundingOptions::new(precision=Digit(1)),
/// ),
/// content="PT1.5S",
/// )
/// }
/// ```
pub fn Duration::to_string_with_options(
self : Duration,
options : ToStringRoundingOptions,
) -> String raise TemporalError {
if options.smallest_unit is (Some(Hour) | Some(Minute)) {
raise RangeError(
"string rounding options cannot use an hour or minute smallestUnit",
)
}
let resolved = options.resolve()
if resolved.smallest_unit == Nanosecond &&
resolved.increment == rounding_increment_one {
return format_duration(self, resolved.precision)
}
let rounding_options = ResolvedRoundingOptions::from_to_string_options(
resolved,
)
let largest = self.default_largest_unit()
let internal = self.to_internal()
let time = internal.time.round(rounding_options)
let combined = InternalDurationRecord::combine(internal.date, time)
let rounded = Duration::from_internal(combined, largest.larger(Second))
format_duration(rounded, resolved.precision)
}
///|
/// Renders a duration in ISO 8601 form, such as `-P1Y2M3DT4H5M6.789S`.
///
/// The date part omits zero components. The time part always ends at seconds,
/// which keeps sub-second values attached to a seconds field, and a duration
/// with no components at all renders as `PT0S`.
fn format_duration(duration : Duration, precision : Precision) -> String {
let sign = duration.sign()
let duration = duration.abs()
let buf = StringBuilder::new()
if sign == Negative {
buf.write_string("-")
}
buf.write_string("P")
let has_date = duration.years != 0L ||
duration.months != 0L ||
duration.weeks != 0L ||
duration.days != 0L
if has_date {
write_unit_if_non_zero(buf, duration.years, "Y")
write_unit_if_non_zero(buf, duration.months, "M")
write_unit_if_non_zero(buf, duration.weeks, "W")
write_unit_if_non_zero(buf, duration.days, "D")
}
let hours = duration.hours
let minutes = duration.minutes
// Collapse seconds and below into one nanosecond count so that, say,
// 1500 milliseconds renders as `1.5S` rather than as its own field.
let sub_minute = TimeDuration::from_components(
0L,
0L,
duration.seconds,
duration.milliseconds,
duration.microseconds,
duration.nanoseconds,
)
let seconds = sub_minute.seconds().abs()
let subseconds = sub_minute.subseconds().abs()
// A bare `P` is not a valid duration, so a duration with no date part must
// print a seconds field even when it is zero.
let write_second = seconds != 0L ||
subseconds != 0 ||
(!has_date && hours == 0L && minutes == 0L) ||
precision is Digit(_)
if hours != 0L || minutes != 0L || write_second {
buf.write_string("T")
}
write_unit_if_non_zero(buf, hours, "H")
write_unit_if_non_zero(buf, minutes, "M")
if write_second {
buf.write_string(seconds.to_string())
if precision is Digit(0) || (precision is Auto && subseconds == 0) {
buf.write_string("S")
} else {
buf.write_string(".")
write_fraction(buf, subseconds, precision)
buf.write_string("S")
}
}
buf.to_string()
}
///|
/// Writes `value` followed by `suffix`, unless the value is zero.
fn write_unit_if_non_zero(
buf : StringBuilder,
value : Int64,
suffix : String,
) -> Unit {
if value != 0L {
buf.write_string(value.to_string())
buf.write_string(suffix)
}
}