Skip to content

Commit c614682

Browse files
authored
Implement to_string functionality and methods for Duration, PlainYearMonth, and PlainMonthDay (#164)
This PR implements the remaining functionality needed for the final `to_string` methods on `Duration`, `PlainYearMonth`, and `PlainMonthDay` and also implements the methods on their intended structs. There is also an early bump of the `ixdtf` crate to the git revision to begin taking advantage of recent changes. Each of these have been implemented and tested in Boa locally. PlainYearMonth - 100% PlainMonthDay - 100% Duration - 100% For reference, the 4 final failing tests in Duration that were fixed with subsequent bug fix commits to this PR were the below: `/home/nekevss/Projects/boa/test262/test/built-ins/Temporal/Duration/prototype/toString/total-of-duration-time-units-out-of-range.js`: Failed `/home/nekevss/Projects/boa/test262/test/built-ins/Temporal/Duration/prototype/toString/max-value.js`: Failed `/home/nekevss/Projects/boa/test262/test/built-ins/Temporal/Duration/prototype/toString/no-precision-loss.js`: Failed `/home/nekevss/Projects/boa/test262/test/built-ins/Temporal/Duration/prototype/toString/negative-components.js`: Failed
1 parent 88961f9 commit c614682

File tree

10 files changed

+585
-45
lines changed

10 files changed

+585
-45
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ icu_calendar = { version = "2.0.0-beta1", default-features = false}
2424
rustc-hash = "2.1.0"
2525
bitflags = "2.7.0"
2626
num-traits = "0.2.19"
27-
ixdtf = "0.3.0"
27+
ixdtf = { git = "https://github.com/unicode-org/icu4x.git", rev = "3d187da4d3f05b7e37603c4be3f2c1ce45100e03" }
2828
iana-time-zone = "0.1.61"
2929
log = "0.4.0"
3030
tzif = "0.3.0"

src/components/duration.rs

Lines changed: 135 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ use crate::{
44
components::{timezone::TimeZoneProvider, 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

@@ -82,6 +87,16 @@ pub struct Duration {
8287
time: TimeDuration,
8388
}
8489

90+
impl core::fmt::Display for Duration {
91+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
92+
f.write_str(
93+
&self
94+
.to_temporal_string(ToStringRoundingOptions::default())
95+
.expect("Duration must return a valid string with default options."),
96+
)
97+
}
98+
}
99+
85100
// NOTE(nekevss): Structure of the below is going to be a little convoluted,
86101
// but intended to section everything based on the below
87102
//
@@ -128,13 +143,18 @@ impl Duration {
128143
duration_record.normalized_time_duration(),
129144
largest_unit,
130145
)?;
131-
let date = DateDuration::new(
146+
Self::new(
132147
duration_record.date().years,
133148
duration_record.date().months,
134149
duration_record.date().weeks,
135150
duration_record.date().days.checked_add(&overflow_day)?,
136-
)?;
137-
Ok(Self::new_unchecked(date, time))
151+
time.hours,
152+
time.minutes,
153+
time.seconds,
154+
time.milliseconds,
155+
time.microseconds,
156+
time.nanoseconds,
157+
)
138158
}
139159

140160
/// Returns the a `Vec` of the fields values.
@@ -618,6 +638,98 @@ impl Duration {
618638
}
619639
}
620640
}
641+
642+
pub fn to_temporal_string(&self, options: ToStringRoundingOptions) -> TemporalResult<String> {
643+
if options.smallest_unit == Some(TemporalUnit::Hour)
644+
|| options.smallest_unit == Some(TemporalUnit::Minute)
645+
{
646+
return Err(TemporalError::range().with_message(
647+
"string rounding options cannot have hour or minute smallest unit.",
648+
));
649+
}
650+
651+
let resolved_options = options.resolve()?;
652+
if resolved_options.smallest_unit == TemporalUnit::Nanosecond
653+
&& resolved_options.increment == RoundingIncrement::ONE
654+
{
655+
let duration = duration_to_formattable(self, resolved_options.precision)?;
656+
return Ok(duration.to_string());
657+
}
658+
659+
let rounding_options = ResolvedRoundingOptions::from_to_string_options(&resolved_options);
660+
661+
// 11. Let largestUnit be DefaultTemporalLargestUnit(duration).
662+
let largest = self.default_largest_unit();
663+
// 12. Let internalDuration be ToInternalDurationRecord(duration).
664+
let norm = NormalizedDurationRecord::new(
665+
self.date,
666+
NormalizedTimeDuration::from_time_duration(&self.time),
667+
)?;
668+
// 13. Let timeDuration be ? RoundTimeDuration(internalDuration.[[Time]], precision.[[Increment]], precision.[[Unit]], roundingMode).
669+
let (rounded, _) = norm
670+
.normalized_time_duration()
671+
.round(FiniteF64::default(), rounding_options)?;
672+
// 14. Set internalDuration to CombineDateAndTimeDuration(internalDuration.[[Date]], timeDuration).
673+
let norm = NormalizedDurationRecord::new(norm.date(), rounded.normalized_time_duration())?;
674+
// 15. Let roundedLargestUnit be LargerOfTwoTemporalUnits(largestUnit, second).
675+
let rounded_largest = largest.max(TemporalUnit::Second);
676+
// 16. Let roundedDuration be ? TemporalDurationFromInternal(internalDuration, roundedLargestUnit).
677+
let rounded = Self::from_normalized(norm, rounded_largest)?;
678+
679+
// 17. Return TemporalDurationToString(roundedDuration, precision.[[Precision]]).
680+
Ok(duration_to_formattable(&rounded, resolved_options.precision)?.to_string())
681+
}
682+
}
683+
684+
pub fn duration_to_formattable(
685+
duration: &Duration,
686+
precision: Precision,
687+
) -> TemporalResult<FormattableDuration> {
688+
let sign = duration.sign();
689+
let sign = if sign == Sign::Negative {
690+
IxdtfSign::Negative
691+
} else {
692+
IxdtfSign::Positive
693+
};
694+
let duration = duration.abs();
695+
let date = duration.years().0 + duration.months().0 + duration.weeks().0 + duration.days().0;
696+
let date = if date != 0.0 {
697+
Some(DateDurationRecord {
698+
years: duration.years().0 as u32,
699+
months: duration.months().0 as u32,
700+
weeks: duration.weeks().0 as u32,
701+
days: duration.days().0 as u64,
702+
})
703+
} else {
704+
None
705+
};
706+
707+
let hours = duration.hours().abs();
708+
let minutes = duration.minutes().abs();
709+
710+
let time = NormalizedTimeDuration::from_time_duration(&TimeDuration::new_unchecked(
711+
FiniteF64::default(),
712+
FiniteF64::default(),
713+
duration.seconds(),
714+
duration.milliseconds(),
715+
duration.microseconds(),
716+
duration.nanoseconds(),
717+
));
718+
719+
let seconds = time.seconds().unsigned_abs();
720+
let subseconds = time.subseconds().unsigned_abs();
721+
722+
let time = Some(TimeDurationRecord::Seconds {
723+
hours: hours.0 as u64,
724+
minutes: minutes.0 as u64,
725+
seconds,
726+
fraction: subseconds,
727+
});
728+
729+
Ok(FormattableDuration {
730+
precision,
731+
duration: DurationParseRecord { sign, date, time },
732+
})
621733
}
622734

623735
#[cfg(feature = "experimental")]
@@ -636,6 +748,8 @@ impl Duration {
636748

637749
// TODO: Update, optimize, and fix the below. is_valid_duration should probably be generic over a T.
638750

751+
const TWO_POWER_FIFTY_THREE: i128 = 9_007_199_254_740_992;
752+
639753
// NOTE: Can FiniteF64 optimize the duration_validation
640754
/// Utility function to check whether the `Duration` fields are valid.
641755
#[inline]
@@ -701,21 +815,18 @@ pub(crate) fn is_valid_duration(
701815
// in C++ with an implementation of core::remquo() with sufficient bits in the quotient.
702816
// String manipulation will also give an exact result, since the multiplication is by a power of 10.
703817
// Seconds part
704-
let normalized_seconds = days.0.mul_add(
705-
86_400.0,
706-
hours.0.mul_add(3600.0, minutes.0.mul_add(60.0, seconds.0)),
707-
);
818+
let normalized_seconds = (days.0 as i128 * 86_400)
819+
+ (hours.0 as i128) * 3600
820+
+ minutes.0 as i128 * 60
821+
+ seconds.0 as i128;
708822
// Subseconds part
709-
let normalized_subseconds_parts = milliseconds.0.mul_add(
710-
10e-3,
711-
microseconds
712-
.0
713-
.mul_add(10e-6, nanoseconds.0.mul_add(10e-9, 0.0)),
714-
);
823+
let normalized_subseconds_parts = (milliseconds.0 as i128 / 1_000)
824+
+ (microseconds.0 as i128 / 1_000_000)
825+
+ (nanoseconds.0 as i128 / 1_000_000_000);
715826

716827
let normalized_seconds = normalized_seconds + normalized_subseconds_parts;
717828
// 8. If abs(normalizedSeconds) ≥ 2**53, return false.
718-
if normalized_seconds.abs() >= 2e53 {
829+
if normalized_seconds.abs() >= TWO_POWER_FIFTY_THREE {
719830
return false;
720831
}
721832

@@ -786,7 +897,7 @@ impl FromStr for Duration {
786897
let nanoseconds = rem.rem_euclid(1_000);
787898

788899
(
789-
f64::from(hours),
900+
hours as f64,
790901
minutes as f64,
791902
seconds as f64,
792903
milliseconds as f64,
@@ -810,8 +921,8 @@ impl FromStr for Duration {
810921
let nanoseconds = rem.rem_euclid(1_000);
811922

812923
(
813-
f64::from(hours),
814-
f64::from(minutes),
924+
hours as f64,
925+
minutes as f64,
815926
seconds as f64,
816927
milliseconds as f64,
817928
microseconds as f64,
@@ -832,9 +943,9 @@ impl FromStr for Duration {
832943
let nanoseconds = rem.rem_euclid(1_000);
833944

834945
(
835-
f64::from(hours),
836-
f64::from(minutes),
837-
f64::from(seconds),
946+
hours as f64,
947+
minutes as f64,
948+
seconds as f64,
838949
milliseconds as f64,
839950
microseconds as f64,
840951
nanoseconds as f64,
@@ -855,7 +966,7 @@ impl FromStr for Duration {
855966
FiniteF64::from(years).copysign(sign),
856967
FiniteF64::from(months).copysign(sign),
857968
FiniteF64::from(weeks).copysign(sign),
858-
FiniteF64::from(days).copysign(sign),
969+
FiniteF64::try_from(days)?.copysign(sign),
859970
FiniteF64::try_from(hours)?.copysign(sign),
860971
FiniteF64::try_from(minutes)?.copysign(sign),
861972
FiniteF64::try_from(seconds)?.copysign(sign),

src/components/duration/normalized.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const MAX_TIME_DURATION: i128 = 9_007_199_254_740_991_999_999_999;
2626
// Nanoseconds constants
2727

2828
const NS_PER_DAY_128BIT: i128 = NS_PER_DAY as i128;
29-
const NANOSECONDS_PER_MINUTE: f64 = 60.0 * 1e9;
30-
const NANOSECONDS_PER_HOUR: f64 = 60.0 * 60.0 * 1e9;
29+
const NANOSECONDS_PER_MINUTE: i128 = 60 * 1_000_000_000;
30+
const NANOSECONDS_PER_HOUR: i128 = 60 * NANOSECONDS_PER_MINUTE;
3131

3232
// ==== NormalizedTimeDuration ====
3333
//
@@ -44,12 +44,12 @@ pub(crate) struct NormalizedTimeDuration(pub(crate) i128);
4444
impl NormalizedTimeDuration {
4545
/// Equivalent: 7.5.20 NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, nanoseconds )
4646
pub(crate) fn from_time_duration(time: &TimeDuration) -> Self {
47-
// TODO: Determine if there is a loss in precision from casting. If so, times by 1,000 (calculate in picoseconds) than truncate?
48-
let mut nanoseconds: i128 = (time.hours.0 * NANOSECONDS_PER_HOUR) as i128;
49-
nanoseconds += (time.minutes.0 * NANOSECONDS_PER_MINUTE) as i128;
50-
nanoseconds += (time.seconds.0 * 1_000_000_000.0) as i128;
51-
nanoseconds += (time.milliseconds.0 * 1_000_000.0) as i128;
52-
nanoseconds += (time.microseconds.0 * 1_000.0) as i128;
47+
// Note: Calculations must be done after casting to `i128` in order to preserve precision
48+
let mut nanoseconds: i128 = time.hours.0 as i128 * NANOSECONDS_PER_HOUR;
49+
nanoseconds += time.minutes.0 as i128 * NANOSECONDS_PER_MINUTE;
50+
nanoseconds += time.seconds.0 as i128 * 1_000_000_000;
51+
nanoseconds += time.milliseconds.0 as i128 * 1_000_000;
52+
nanoseconds += time.microseconds.0 as i128 * 1_000;
5353
nanoseconds += time.nanoseconds.0 as i128;
5454
// NOTE(nekevss): Is it worth returning a `RangeError` below.
5555
debug_assert!(nanoseconds.abs() <= MAX_TIME_DURATION);

0 commit comments

Comments
 (0)