Skip to content

Commit bd96e91

Browse files
authored
Extend implementation of to_ixdtf_string to more types (#155)
This PR continues the work on implementing `toString` functionality in `temporal_rs`. It completes the following: - Adds `to_ixdtf_string` functions on `DateTime`, `Time`, `ZonedDateTime`, and `Instant`. - Adds `ToStringRoundingOptions` options type. - Adds an `IxdtfStringBuilder` struct to assist with building `ixdtf` strings. So far I have verfied that the PR has a 100% conformance rate for PlainDateTime. I plan to test the others, but any poentially issues that may arise with those could always be completed in subsequent PRs.
1 parent 436b07d commit bd96e91

File tree

8 files changed

+537
-44
lines changed

8 files changed

+537
-44
lines changed

src/components/date.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar,
1212
ResolvedRoundingOptions, TemporalUnit,
1313
},
14-
parsers::{parse_date_time, FormattableCalendar, FormattableDate, FormattableIxdtf},
14+
parsers::{parse_date_time, IxdtfStringBuilder},
1515
primitive::FiniteF64,
1616
Sign, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
1717
};
@@ -593,17 +593,10 @@ impl PlainDate {
593593

594594
#[inline]
595595
pub fn to_ixdtf_string(&self, display_calendar: DisplayCalendar) -> String {
596-
let ixdtf = FormattableIxdtf {
597-
date: Some(FormattableDate(self.iso.year, self.iso.month, self.iso.day)),
598-
time: None,
599-
utc_offset: None,
600-
timezone: None,
601-
calendar: Some(FormattableCalendar {
602-
show: display_calendar,
603-
calendar: self.calendar.identifier(),
604-
}),
605-
};
606-
ixdtf.to_string()
596+
IxdtfStringBuilder::default()
597+
.with_date(self.iso)
598+
.with_calendar(self.calendar.identifier(), display_calendar)
599+
.build()
607600
}
608601
}
609602

src/components/datetime.rs

Lines changed: 191 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ use crate::{
44
components::{calendar::Calendar, Instant},
55
iso::{IsoDate, IsoDateTime, IsoTime},
66
options::{
7-
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, ResolvedRoundingOptions,
8-
RoundingOptions, TemporalUnit,
7+
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar,
8+
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions,
99
},
10-
parsers::parse_date_time,
10+
parsers::{parse_date_time, IxdtfStringBuilder},
1111
temporal_assert, Sign, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
1212
};
13-
13+
use alloc::string::String;
1414
use core::{cmp::Ordering, str::FromStr};
15-
use tinystr::TinyAsciiStr;
1615

1716
use super::{
1817
calendar::{CalendarDateLike, GetTemporalCalendar},
1918
duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration},
2019
timezone::NeverProvider,
2120
Duration, PartialDate, PartialTime, PlainDate, PlainTime,
2221
};
22+
use tinystr::TinyAsciiStr;
2323

2424
/// A partial PlainDateTime record
2525
#[derive(Debug, Default, Clone)]
@@ -38,6 +38,15 @@ pub struct PlainDateTime {
3838
calendar: Calendar,
3939
}
4040

41+
impl core::fmt::Display for PlainDateTime {
42+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43+
let ixdtf_str = self
44+
.to_ixdtf_string(ToStringRoundingOptions::default(), DisplayCalendar::Auto)
45+
.expect("ixdtf default configuration should not fail.");
46+
f.write_str(&ixdtf_str)
47+
}
48+
}
49+
4150
impl Ord for PlainDateTime {
4251
fn cmp(&self, other: &Self) -> Ordering {
4352
self.iso.cmp(&other.iso)
@@ -619,6 +628,28 @@ impl PlainDateTime {
619628

620629
Ok(Self::new_unchecked(result, self.calendar.clone()))
621630
}
631+
632+
pub fn to_ixdtf_string(
633+
&self,
634+
options: ToStringRoundingOptions,
635+
display_calendar: DisplayCalendar,
636+
) -> TemporalResult<String> {
637+
let resolved_options = options.resolve()?;
638+
let result = self
639+
.iso
640+
.round(ResolvedRoundingOptions::from_to_string_options(
641+
&resolved_options,
642+
))?;
643+
if !result.is_within_limits() {
644+
return Err(TemporalError::range().with_message("DateTime is not within valid limits."));
645+
}
646+
let ixdtf_string = IxdtfStringBuilder::default()
647+
.with_date(result.date)
648+
.with_time(result.time, resolved_options.precision)
649+
.with_calendar(self.calendar.identifier(), display_calendar)
650+
.build();
651+
Ok(ixdtf_string)
652+
}
622653
}
623654

624655
// ==== Trait impls ====
@@ -682,9 +713,10 @@ mod tests {
682713
},
683714
iso::{IsoDate, IsoDateTime, IsoTime},
684715
options::{
685-
DifferenceSettings, RoundingIncrement, RoundingOptions, TemporalRoundingMode,
686-
TemporalUnit,
716+
DifferenceSettings, DisplayCalendar, RoundingIncrement, RoundingOptions,
717+
TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions,
687718
},
719+
parsers::Precision,
688720
primitive::FiniteF64,
689721
TemporalResult,
690722
};
@@ -1098,4 +1130,156 @@ mod tests {
10981130
.unwrap();
10991131
assert_datetime(result, (1976, 11, 18, 14, 23, 30, 123, 456, 790));
11001132
}
1133+
1134+
// Mapped from fractionaldigits-number.js
1135+
#[test]
1136+
fn to_string_precision_digits() {
1137+
let few_seconds =
1138+
PlainDateTime::try_new(1976, 2, 4, 5, 3, 1, 0, 0, 0, Calendar::default()).unwrap();
1139+
let zero_seconds =
1140+
PlainDateTime::try_new(1976, 11, 18, 15, 23, 0, 0, 0, 0, Calendar::default()).unwrap();
1141+
let whole_seconds =
1142+
PlainDateTime::try_new(1976, 11, 18, 15, 23, 30, 0, 0, 0, Calendar::default()).unwrap();
1143+
let subseconds =
1144+
PlainDateTime::try_new(1976, 11, 18, 15, 23, 30, 123, 400, 0, Calendar::default())
1145+
.unwrap();
1146+
1147+
let options = ToStringRoundingOptions {
1148+
precision: Precision::Digit(0),
1149+
smallest_unit: None,
1150+
rounding_mode: None,
1151+
};
1152+
assert_eq!(
1153+
&few_seconds
1154+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1155+
.unwrap(),
1156+
"1976-02-04T05:03:01",
1157+
"pads parts with 0"
1158+
);
1159+
1160+
let options = ToStringRoundingOptions {
1161+
precision: Precision::Digit(0),
1162+
smallest_unit: None,
1163+
rounding_mode: None,
1164+
};
1165+
assert_eq!(
1166+
&subseconds
1167+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1168+
.unwrap(),
1169+
"1976-11-18T15:23:30",
1170+
"truncates 4 decimal places to 0"
1171+
);
1172+
1173+
let options = ToStringRoundingOptions {
1174+
precision: Precision::Digit(2),
1175+
smallest_unit: None,
1176+
rounding_mode: None,
1177+
};
1178+
assert_eq!(
1179+
&zero_seconds
1180+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1181+
.unwrap(),
1182+
"1976-11-18T15:23:00.00",
1183+
"pads zero seconds to 2 decimal places"
1184+
);
1185+
let options = ToStringRoundingOptions {
1186+
precision: Precision::Digit(2),
1187+
smallest_unit: None,
1188+
rounding_mode: None,
1189+
};
1190+
1191+
assert_eq!(
1192+
&whole_seconds
1193+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1194+
.unwrap(),
1195+
"1976-11-18T15:23:30.00",
1196+
"pads whole seconds to 2 decimal places"
1197+
);
1198+
let options = ToStringRoundingOptions {
1199+
precision: Precision::Digit(2),
1200+
smallest_unit: None,
1201+
rounding_mode: None,
1202+
};
1203+
assert_eq!(
1204+
&subseconds
1205+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1206+
.unwrap(),
1207+
"1976-11-18T15:23:30.12",
1208+
"truncates 4 decimal places to 2"
1209+
);
1210+
1211+
let options = ToStringRoundingOptions {
1212+
precision: Precision::Digit(3),
1213+
smallest_unit: None,
1214+
rounding_mode: None,
1215+
};
1216+
assert_eq!(
1217+
&subseconds
1218+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1219+
.unwrap(),
1220+
"1976-11-18T15:23:30.123",
1221+
"truncates 4 decimal places to 3"
1222+
);
1223+
1224+
let options = ToStringRoundingOptions {
1225+
precision: Precision::Digit(6),
1226+
smallest_unit: None,
1227+
rounding_mode: None,
1228+
};
1229+
assert_eq!(
1230+
&subseconds
1231+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1232+
.unwrap(),
1233+
"1976-11-18T15:23:30.123400",
1234+
"pads 4 decimal places to 6"
1235+
);
1236+
let options = ToStringRoundingOptions {
1237+
precision: Precision::Digit(7),
1238+
smallest_unit: None,
1239+
rounding_mode: None,
1240+
};
1241+
assert_eq!(
1242+
&zero_seconds
1243+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1244+
.unwrap(),
1245+
"1976-11-18T15:23:00.0000000",
1246+
"pads zero seconds to 7 decimal places"
1247+
);
1248+
let options = ToStringRoundingOptions {
1249+
precision: Precision::Digit(7),
1250+
smallest_unit: None,
1251+
rounding_mode: None,
1252+
};
1253+
assert_eq!(
1254+
&whole_seconds
1255+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1256+
.unwrap(),
1257+
"1976-11-18T15:23:30.0000000",
1258+
"pads whole seconds to 7 decimal places"
1259+
);
1260+
let options = ToStringRoundingOptions {
1261+
precision: Precision::Digit(7),
1262+
smallest_unit: None,
1263+
rounding_mode: None,
1264+
};
1265+
assert_eq!(
1266+
&subseconds
1267+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1268+
.unwrap(),
1269+
"1976-11-18T15:23:30.1234000",
1270+
"pads 4 decimal places to 7"
1271+
);
1272+
let options = ToStringRoundingOptions {
1273+
precision: Precision::Digit(9),
1274+
smallest_unit: None,
1275+
rounding_mode: None,
1276+
};
1277+
assert_eq!(
1278+
&subseconds
1279+
.to_ixdtf_string(options, DisplayCalendar::Auto)
1280+
.unwrap(),
1281+
"1976-11-18T15:23:30.123400000",
1282+
"pads 4 decimal places to 9"
1283+
);
1284+
}
11011285
}

src/components/instant.rs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
//! An implementation of the Temporal Instant.
22
3+
use alloc::string::String;
34
use core::{num::NonZeroU128, str::FromStr};
45

56
use crate::{
6-
components::{duration::TimeDuration, Duration},
7+
components::{
8+
duration::TimeDuration, zoneddatetime::nanoseconds_to_formattable_offset_minutes, Duration,
9+
},
710
iso::{IsoDate, IsoDateTime, IsoTime},
811
options::{
9-
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, ResolvedRoundingOptions,
10-
RoundingOptions, TemporalUnit,
12+
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayOffset,
13+
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions,
1114
},
12-
parsers::parse_instant,
15+
parsers::{parse_instant, IxdtfStringBuilder},
1316
primitive::FiniteF64,
1417
rounding::{IncrementRounder, Round},
15-
Sign, TemporalError, TemporalResult, TemporalUnwrap, NS_MAX_INSTANT,
18+
Sign, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, NS_MAX_INSTANT,
1619
};
1720

1821
use ixdtf::parsers::records::UtcOffsetRecordOrZ;
1922
use num_traits::FromPrimitive;
2023

21-
use super::duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration};
24+
use super::{
25+
duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration},
26+
timezone::TzProvider,
27+
};
2228

2329
const NANOSECONDS_PER_SECOND: f64 = 1e9;
2430
const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND;
@@ -264,6 +270,41 @@ impl Instant {
264270
}
265271
}
266272

273+
// ==== Instant Provider API ====
274+
275+
impl Instant {
276+
pub fn to_ixdtf_string_with_provider(
277+
&self,
278+
timezone: Option<&TimeZone>,
279+
options: ToStringRoundingOptions,
280+
provider: &impl TzProvider,
281+
) -> TemporalResult<String> {
282+
let resolved_options = options.resolve()?;
283+
let round = self.round_instant(ResolvedRoundingOptions::from_to_string_options(
284+
&resolved_options,
285+
))?;
286+
let rounded_instant = Instant::try_new(round)?;
287+
288+
let mut ixdtf = IxdtfStringBuilder::default();
289+
let datetime = if let Some(timezone) = timezone {
290+
let datetime = timezone.get_iso_datetime_for(&rounded_instant, provider)?;
291+
let nanoseconds = timezone.get_offset_nanos_for(rounded_instant.as_i128(), provider)?;
292+
let (sign, hour, minute) = nanoseconds_to_formattable_offset_minutes(nanoseconds)?;
293+
ixdtf = ixdtf.with_minute_offset(sign, hour, minute, DisplayOffset::Auto);
294+
datetime
295+
} else {
296+
ixdtf = ixdtf.with_z(DisplayOffset::Auto);
297+
TimeZone::default().get_iso_datetime_for(&rounded_instant, provider)?
298+
};
299+
let ixdtf_string = ixdtf
300+
.with_date(datetime.date)
301+
.with_time(datetime.time, resolved_options.precision)
302+
.build();
303+
304+
Ok(ixdtf_string)
305+
}
306+
}
307+
267308
// ==== Utility Functions ====
268309

269310
/// Utility for determining if the nanos are within a valid range.

src/components/time.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
//! This module implements `Time` and any directly related algorithms.
22
3-
use num_traits::AsPrimitive;
4-
53
use crate::{
64
components::{duration::TimeDuration, Duration},
75
iso::IsoTime,
86
options::{
97
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, ResolvedRoundingOptions,
10-
RoundingIncrement, TemporalRoundingMode, TemporalUnit,
8+
RoundingIncrement, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions,
119
},
12-
parsers::parse_time,
10+
parsers::{parse_time, IxdtfStringBuilder},
1311
primitive::FiniteF64,
1412
Sign, TemporalError, TemporalResult,
1513
};
14+
use alloc::string::String;
15+
use core::str::FromStr;
16+
use num_traits::AsPrimitive;
1617

1718
use super::{duration::normalized::NormalizedTimeDuration, PlainDateTime};
1819

19-
use core::str::FromStr;
20-
2120
/// A `PartialTime` represents partially filled `Time` fields.
2221
#[derive(Debug, Default, Clone, Copy, PartialEq)]
2322
pub struct PartialTime {
@@ -434,6 +433,17 @@ impl PlainTime {
434433

435434
Ok(Self::new_unchecked(result))
436435
}
436+
437+
pub fn to_ixdtf_string(&self, options: ToStringRoundingOptions) -> TemporalResult<String> {
438+
let resolved = options.resolve()?;
439+
let (_, result) = self
440+
.iso
441+
.round(ResolvedRoundingOptions::from_to_string_options(&resolved))?;
442+
let ixdtf_string = IxdtfStringBuilder::default()
443+
.with_time(result, resolved.precision)
444+
.build();
445+
Ok(ixdtf_string)
446+
}
437447
}
438448

439449
impl From<PlainDateTime> for PlainTime {

0 commit comments

Comments
 (0)