Skip to content

Commit 7bd7190

Browse files
committed
Add initial implementation of to_string methods for Duration, PlainMonthDay, and PlainYearMonth
1 parent 46dcd52 commit 7bd7190

File tree

6 files changed

+389
-12
lines changed

6 files changed

+389
-12
lines changed

src/components/duration.rs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ use crate::{
44
components::{timezone::TzProvider, PlainDateTime, PlainTime},
55
iso::{IsoDateTime, IsoTime},
66
options::{
7-
ArithmeticOverflow, RelativeTo, ResolvedRoundingOptions, RoundingOptions, TemporalUnit,
7+
ArithmeticOverflow, RelativeTo, ResolvedRoundingOptions, RoundingIncrement,
8+
RoundingOptions, TemporalUnit, ToStringRoundingOptions,
89
},
10+
parsers::{FormattableDuration, Precision},
911
primitive::FiniteF64,
1012
temporal_assert, Sign, TemporalError, TemporalResult,
1113
};
1214
use alloc::format;
15+
use alloc::string::String;
1316
use alloc::vec;
1417
use alloc::vec::Vec;
1518
use core::str::FromStr;
16-
use ixdtf::parsers::{records::TimeDurationRecord, IsoDurationParser};
19+
use ixdtf::parsers::{
20+
records::{DateDurationRecord, DurationParseRecord, Sign as IxdtfSign, TimeDurationRecord},
21+
IsoDurationParser,
22+
};
1723
use normalized::NormalizedDurationRecord;
1824
use num_traits::AsPrimitive;
1925

@@ -26,7 +32,6 @@ mod date;
2632
pub(crate) mod normalized;
2733
mod time;
2834

29-
#[cfg(feature = "experimental")]
3035
#[cfg(test)]
3136
mod tests;
3237

@@ -83,6 +88,16 @@ pub struct Duration {
8388
time: TimeDuration,
8489
}
8590

91+
impl core::fmt::Display for Duration {
92+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93+
f.write_str(
94+
&self
95+
.to_temporal_string(ToStringRoundingOptions::default())
96+
.expect("Duration must return a valid string with default options."),
97+
)
98+
}
99+
}
100+
86101
// NOTE(nekevss): Structure of the below is going to be a little convoluted,
87102
// but intended to section everything based on the below
88103
//
@@ -619,6 +634,97 @@ impl Duration {
619634
}
620635
}
621636
}
637+
638+
pub fn to_temporal_string(&self, options: ToStringRoundingOptions) -> TemporalResult<String> {
639+
if options.smallest_unit == Some(TemporalUnit::Hour)
640+
|| options.smallest_unit == Some(TemporalUnit::Minute)
641+
{
642+
return Err(TemporalError::range().with_message(
643+
"string rounding options cannot have hour or minute smallest unit.",
644+
));
645+
}
646+
647+
let resolved_options = options.resolve()?;
648+
if resolved_options.smallest_unit == TemporalUnit::Nanosecond
649+
&& resolved_options.increment == RoundingIncrement::ONE
650+
{
651+
let duration = duration_to_formattable(self, resolved_options.precision)?;
652+
return Ok(duration.to_string());
653+
}
654+
655+
let rounding_options = ResolvedRoundingOptions::from_to_string_options(&resolved_options);
656+
657+
// 11. Let largestUnit be DefaultTemporalLargestUnit(duration).
658+
let largest = self.default_largest_unit();
659+
// 12. Let internalDuration be ToInternalDurationRecord(duration).
660+
let norm = NormalizedDurationRecord::new(
661+
self.date,
662+
NormalizedTimeDuration::from_time_duration(&self.time),
663+
)?;
664+
// 13. Let timeDuration be ? RoundTimeDuration(internalDuration.[[Time]], precision.[[Increment]], precision.[[Unit]], roundingMode).
665+
let (rounded, _) = norm
666+
.normalized_time_duration()
667+
.round(FiniteF64::default(), rounding_options)?;
668+
// 14. Set internalDuration to CombineDateAndTimeDuration(internalDuration.[[Date]], timeDuration).
669+
let norm = NormalizedDurationRecord::new(norm.date(), rounded.normalized_time_duration())?;
670+
// 15. Let roundedLargestUnit be LargerOfTwoTemporalUnits(largestUnit, second).
671+
let rounded_largest = largest.max(TemporalUnit::Second);
672+
// 16. Let roundedDuration be ? TemporalDurationFromInternal(internalDuration, roundedLargestUnit).
673+
let rounded = Self::from_normalized(norm, rounded_largest)?;
674+
675+
// 17. Return TemporalDurationToString(roundedDuration, precision.[[Precision]]).
676+
Ok(duration_to_formattable(&rounded, resolved_options.precision)?.to_string())
677+
}
678+
}
679+
680+
pub fn duration_to_formattable(
681+
duration: &Duration,
682+
precision: Precision,
683+
) -> TemporalResult<FormattableDuration> {
684+
let sign = duration.sign();
685+
let sign = if sign == Sign::Negative {
686+
IxdtfSign::Negative
687+
} else {
688+
IxdtfSign::Positive
689+
};
690+
let date = duration.years().0 + duration.months().0 + duration.weeks().0 + duration.days().0;
691+
let date = if date != 0.0 {
692+
Some(DateDurationRecord {
693+
years: duration.years().0 as u32,
694+
months: duration.months().0 as u32,
695+
weeks: duration.weeks().0 as u32,
696+
days: duration.days().0 as u32,
697+
})
698+
} else {
699+
None
700+
};
701+
702+
let hours = duration.hours().abs();
703+
let minutes = duration.minutes().abs();
704+
705+
let time = NormalizedTimeDuration::from_time_duration(&TimeDuration::new_unchecked(
706+
FiniteF64::default(),
707+
FiniteF64::default(),
708+
duration.seconds(),
709+
duration.milliseconds(),
710+
duration.microseconds(),
711+
duration.nanoseconds(),
712+
));
713+
714+
let seconds = time.seconds().unsigned_abs() as u32;
715+
let subseconds = time.subseconds().unsigned_abs();
716+
717+
let time = Some(TimeDurationRecord::Seconds {
718+
hours: hours.0 as u32,
719+
minutes: minutes.0 as u32,
720+
seconds,
721+
fraction: subseconds,
722+
});
723+
724+
Ok(FormattableDuration {
725+
precision,
726+
duration: DurationParseRecord { sign, date, time },
727+
})
622728
}
623729

624730
#[cfg(feature = "experimental")]

src/components/duration/tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#[cfg(feature = "experimental")]
12
use crate::{
23
components::{calendar::Calendar, PlainDate},
34
options::{RoundingIncrement, TemporalRoundingMode},
@@ -6,6 +7,7 @@ use crate::{
67

78
use super::*;
89

10+
#[cfg(feature = "experimental")]
911
fn get_round_result(
1012
test_duration: &Duration,
1113
relative_to: RelativeTo,
@@ -21,6 +23,7 @@ fn get_round_result(
2123
}
2224

2325
// roundingmode-floor.js
26+
#[cfg(feature = "experimental")]
2427
#[test]
2528
fn basic_positive_floor_rounding_v2() {
2629
let test_duration = Duration::new(
@@ -89,6 +92,7 @@ fn basic_positive_floor_rounding_v2() {
8992
}
9093

9194
#[test]
95+
#[cfg(feature = "experimental")]
9296
fn basic_negative_floor_rounding_v2() {
9397
// Test setup
9498
let test_duration = Duration::new(
@@ -158,6 +162,7 @@ fn basic_negative_floor_rounding_v2() {
158162
}
159163

160164
// roundingmode-ceil.js
165+
#[cfg(feature = "experimental")]
161166
#[test]
162167
fn basic_positive_ceil_rounding() {
163168
let test_duration = Duration::new(
@@ -225,6 +230,7 @@ fn basic_positive_ceil_rounding() {
225230
assert_eq!(&result, &[5, 7, 0, 27, 16, 30, 20, 123, 987, 500],);
226231
}
227232

233+
#[cfg(feature = "experimental")]
228234
#[test]
229235
fn basic_negative_ceil_rounding() {
230236
let test_duration = Duration::new(
@@ -293,6 +299,7 @@ fn basic_negative_ceil_rounding() {
293299
}
294300

295301
// roundingmode-expand.js
302+
#[cfg(feature = "experimental")]
296303
#[test]
297304
fn basic_positive_expand_rounding() {
298305
let test_duration = Duration::new(
@@ -359,6 +366,7 @@ fn basic_positive_expand_rounding() {
359366
assert_eq!(&result, &[5, 7, 0, 27, 16, 30, 20, 123, 987, 500],);
360367
}
361368

369+
#[cfg(feature = "experimental")]
362370
#[test]
363371
fn basic_negative_expand_rounding() {
364372
let test_duration = Duration::new(
@@ -429,6 +437,7 @@ fn basic_negative_expand_rounding() {
429437
}
430438

431439
// test262/test/built-ins/Temporal/Duration/prototype/round/roundingincrement-non-integer.js
440+
#[cfg(feature = "experimental")]
432441
#[test]
433442
fn rounding_increment_non_integer() {
434443
let test_duration = Duration::from(
@@ -609,6 +618,7 @@ fn partial_duration_values() {
609618
}
610619

611620
// days-24-hours-relative-to-zoned-date-time.js
621+
#[cfg(feature = "experimental")]
612622
#[test]
613623
fn round_relative_to_zoned_datetime() {
614624
let duration = Duration::from(
@@ -641,3 +651,40 @@ fn round_relative_to_zoned_datetime() {
641651
assert_eq!(result.days(), 1.0);
642652
assert_eq!(result.hours(), 1.0);
643653
}
654+
655+
#[test]
656+
fn default_duration_string() {
657+
let duration = Duration::default();
658+
659+
let options = ToStringRoundingOptions {
660+
precision: Precision::Auto,
661+
smallest_unit: None,
662+
rounding_mode: None,
663+
};
664+
let result = duration.to_temporal_string(options).unwrap();
665+
assert_eq!(&result, "PT0S");
666+
667+
let options = ToStringRoundingOptions {
668+
precision: Precision::Digit(0),
669+
smallest_unit: None,
670+
rounding_mode: None,
671+
};
672+
let result = duration.to_temporal_string(options).unwrap();
673+
assert_eq!(&result, "PT0S");
674+
675+
let options = ToStringRoundingOptions {
676+
precision: Precision::Digit(1),
677+
smallest_unit: None,
678+
rounding_mode: None,
679+
};
680+
let result = duration.to_temporal_string(options).unwrap();
681+
assert_eq!(&result, "PT0.0S");
682+
683+
let options = ToStringRoundingOptions {
684+
precision: Precision::Digit(3),
685+
smallest_unit: None,
686+
rounding_mode: None,
687+
};
688+
let result = duration.to_temporal_string(options).unwrap();
689+
assert_eq!(&result, "PT0.000S");
690+
}

src/components/month_day.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
//! This module implements `MonthDay` and any directly related algorithms.
22
3+
use alloc::string::String;
34
use core::str::FromStr;
45

56
use tinystr::TinyAsciiStr;
67

78
use crate::{
8-
components::calendar::Calendar, iso::IsoDate, options::ArithmeticOverflow, TemporalError,
9-
TemporalResult, TemporalUnwrap,
9+
components::calendar::Calendar,
10+
iso::IsoDate,
11+
options::{ArithmeticOverflow, DisplayCalendar},
12+
parsers::{FormattableCalendar, FormattableDate, FormattableMonthDay},
13+
TemporalError, TemporalResult, TemporalUnwrap,
1014
};
1115

1216
use super::calendar::{CalendarDateLike, GetTemporalCalendar};
@@ -19,6 +23,12 @@ pub struct PlainMonthDay {
1923
calendar: Calendar,
2024
}
2125

26+
impl core::fmt::Display for PlainMonthDay {
27+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28+
f.write_str(&self.to_ixdtf_string(DisplayCalendar::Auto))
29+
}
30+
}
31+
2232
impl PlainMonthDay {
2333
/// Creates a new unchecked `MonthDay`
2434
#[inline]
@@ -82,6 +92,17 @@ impl PlainMonthDay {
8292
pub fn month_code(&self) -> TemporalResult<TinyAsciiStr<4>> {
8393
self.calendar.month_code(&CalendarDateLike::MonthDay(self))
8494
}
95+
96+
pub fn to_ixdtf_string(&self, display_calendar: DisplayCalendar) -> String {
97+
let ixdtf = FormattableMonthDay {
98+
date: FormattableDate(self.iso_year(), self.iso_month(), self.iso.day),
99+
calendar: FormattableCalendar {
100+
show: display_calendar,
101+
calendar: self.calendar().identifier(),
102+
},
103+
};
104+
ixdtf.to_string()
105+
}
85106
}
86107

87108
impl GetTemporalCalendar for PlainMonthDay {

src/components/timezone.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ impl TzProvider for NeverProvider {
7474
}
7575
}
7676

77+
// TODO: There may be an argument to have Offset minutes be a (Cow<'a, str>,, i16) to
78+
// prevent allocations / writing, TBD
79+
// TODO: Potentially use Cow?
7780
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
7881
pub enum TimeZone {
7982
IanaIdentifier(String),

src/components/year_month.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ use core::str::FromStr;
66
use tinystr::TinyAsciiStr;
77

88
use crate::{
9-
components::calendar::Calendar, iso::IsoDate, options::ArithmeticOverflow, utils::pad_iso_year,
9+
components::calendar::Calendar,
10+
iso::IsoDate,
11+
options::{ArithmeticOverflow, DisplayCalendar},
12+
parsers::{FormattableCalendar, FormattableDate, FormattableYearMonth},
13+
utils::pad_iso_year,
1014
TemporalError, TemporalResult, TemporalUnwrap,
1115
};
1216

@@ -23,6 +27,12 @@ pub struct PlainYearMonth {
2327
calendar: Calendar,
2428
}
2529

30+
impl core::fmt::Display for PlainYearMonth {
31+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32+
f.write_str(&self.to_ixdtf_string(DisplayCalendar::Auto))
33+
}
34+
}
35+
2636
impl PlainYearMonth {
2737
/// Creates an unvalidated `YearMonth`.
2838
#[inline]
@@ -158,6 +168,17 @@ impl PlainYearMonth {
158168
self.get_calendar()
159169
.year_month_from_partial(&result_fields, overflow)
160170
}
171+
172+
pub fn to_ixdtf_string(&self, display_calendar: DisplayCalendar) -> String {
173+
let ixdtf = FormattableYearMonth {
174+
date: FormattableDate(self.iso_year(), self.iso_month(), self.iso.day),
175+
calendar: FormattableCalendar {
176+
show: display_calendar,
177+
calendar: self.calendar().identifier(),
178+
},
179+
};
180+
ixdtf.to_string()
181+
}
161182
}
162183

163184
impl GetTemporalCalendar for PlainYearMonth {

0 commit comments

Comments
 (0)