diff --git a/Cargo.lock b/Cargo.lock index f1b7addd7..c58f00e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,8 +232,7 @@ dependencies = [ [[package]] name = "ixdtf" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6cd1080e64f68b07c577e3c687f4a894b3d1bd6093cb36b55c7bd07675aa3a" +source = "git+https://github.com/unicode-org/icu4x.git?rev=3d187da4d3f05b7e37603c4be3f2c1ce45100e03#3d187da4d3f05b7e37603c4be3f2c1ce45100e03" dependencies = [ "displaydoc", "utf8_iter", diff --git a/Cargo.toml b/Cargo.toml index 47484b300..0c9667936 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ icu_calendar = { version = "2.0.0-beta1", default-features = false} rustc-hash = "2.1.0" bitflags = "2.7.0" num-traits = "0.2.19" -ixdtf = "0.3.0" +ixdtf = { git = "https://github.com/unicode-org/icu4x.git", rev = "3d187da4d3f05b7e37603c4be3f2c1ce45100e03" } iana-time-zone = "0.1.61" log = "0.4.0" tzif = "0.3.0" diff --git a/src/components/duration.rs b/src/components/duration.rs index 6ea5bf219..b2924b213 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -4,16 +4,22 @@ use crate::{ components::{timezone::TimeZoneProvider, PlainDateTime, PlainTime}, iso::{IsoDateTime, IsoTime}, options::{ - ArithmeticOverflow, RelativeTo, ResolvedRoundingOptions, RoundingOptions, TemporalUnit, + ArithmeticOverflow, RelativeTo, ResolvedRoundingOptions, RoundingIncrement, + RoundingOptions, TemporalUnit, ToStringRoundingOptions, }, + parsers::{FormattableDuration, Precision}, primitive::FiniteF64, temporal_assert, Sign, TemporalError, TemporalResult, }; use alloc::format; +use alloc::string::String; use alloc::vec; use alloc::vec::Vec; use core::str::FromStr; -use ixdtf::parsers::{records::TimeDurationRecord, IsoDurationParser}; +use ixdtf::parsers::{ + records::{DateDurationRecord, DurationParseRecord, Sign as IxdtfSign, TimeDurationRecord}, + IsoDurationParser, +}; use normalized::NormalizedDurationRecord; use num_traits::AsPrimitive; @@ -26,7 +32,6 @@ mod date; pub(crate) mod normalized; mod time; -#[cfg(feature = "experimental")] #[cfg(test)] mod tests; @@ -82,6 +87,16 @@ pub struct Duration { time: TimeDuration, } +impl core::fmt::Display for Duration { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str( + &self + .to_temporal_string(ToStringRoundingOptions::default()) + .expect("Duration must return a valid string with default options."), + ) + } +} + // NOTE(nekevss): Structure of the below is going to be a little convoluted, // but intended to section everything based on the below // @@ -128,13 +143,18 @@ impl Duration { duration_record.normalized_time_duration(), largest_unit, )?; - let date = DateDuration::new( + Self::new( duration_record.date().years, duration_record.date().months, duration_record.date().weeks, duration_record.date().days.checked_add(&overflow_day)?, - )?; - Ok(Self::new_unchecked(date, time)) + time.hours, + time.minutes, + time.seconds, + time.milliseconds, + time.microseconds, + time.nanoseconds, + ) } /// Returns the a `Vec` of the fields values. @@ -618,6 +638,98 @@ impl Duration { } } } + + pub fn to_temporal_string(&self, options: ToStringRoundingOptions) -> TemporalResult { + if options.smallest_unit == Some(TemporalUnit::Hour) + || options.smallest_unit == Some(TemporalUnit::Minute) + { + return Err(TemporalError::range().with_message( + "string rounding options cannot have hour or minute smallest unit.", + )); + } + + let resolved_options = options.resolve()?; + if resolved_options.smallest_unit == TemporalUnit::Nanosecond + && resolved_options.increment == RoundingIncrement::ONE + { + let duration = duration_to_formattable(self, resolved_options.precision)?; + return Ok(duration.to_string()); + } + + let rounding_options = ResolvedRoundingOptions::from_to_string_options(&resolved_options); + + // 11. Let largestUnit be DefaultTemporalLargestUnit(duration). + let largest = self.default_largest_unit(); + // 12. Let internalDuration be ToInternalDurationRecord(duration). + let norm = NormalizedDurationRecord::new( + self.date, + NormalizedTimeDuration::from_time_duration(&self.time), + )?; + // 13. Let timeDuration be ? RoundTimeDuration(internalDuration.[[Time]], precision.[[Increment]], precision.[[Unit]], roundingMode). + let (rounded, _) = norm + .normalized_time_duration() + .round(FiniteF64::default(), rounding_options)?; + // 14. Set internalDuration to CombineDateAndTimeDuration(internalDuration.[[Date]], timeDuration). + let norm = NormalizedDurationRecord::new(norm.date(), rounded.normalized_time_duration())?; + // 15. Let roundedLargestUnit be LargerOfTwoTemporalUnits(largestUnit, second). + let rounded_largest = largest.max(TemporalUnit::Second); + // 16. Let roundedDuration be ? TemporalDurationFromInternal(internalDuration, roundedLargestUnit). + let rounded = Self::from_normalized(norm, rounded_largest)?; + + // 17. Return TemporalDurationToString(roundedDuration, precision.[[Precision]]). + Ok(duration_to_formattable(&rounded, resolved_options.precision)?.to_string()) + } +} + +pub fn duration_to_formattable( + duration: &Duration, + precision: Precision, +) -> TemporalResult { + let sign = duration.sign(); + let sign = if sign == Sign::Negative { + IxdtfSign::Negative + } else { + IxdtfSign::Positive + }; + let duration = duration.abs(); + let date = duration.years().0 + duration.months().0 + duration.weeks().0 + duration.days().0; + let date = if date != 0.0 { + Some(DateDurationRecord { + years: duration.years().0 as u32, + months: duration.months().0 as u32, + weeks: duration.weeks().0 as u32, + days: duration.days().0 as u64, + }) + } else { + None + }; + + let hours = duration.hours().abs(); + let minutes = duration.minutes().abs(); + + let time = NormalizedTimeDuration::from_time_duration(&TimeDuration::new_unchecked( + FiniteF64::default(), + FiniteF64::default(), + duration.seconds(), + duration.milliseconds(), + duration.microseconds(), + duration.nanoseconds(), + )); + + let seconds = time.seconds().unsigned_abs(); + let subseconds = time.subseconds().unsigned_abs(); + + let time = Some(TimeDurationRecord::Seconds { + hours: hours.0 as u64, + minutes: minutes.0 as u64, + seconds, + fraction: subseconds, + }); + + Ok(FormattableDuration { + precision, + duration: DurationParseRecord { sign, date, time }, + }) } #[cfg(feature = "experimental")] @@ -636,6 +748,8 @@ impl Duration { // TODO: Update, optimize, and fix the below. is_valid_duration should probably be generic over a T. +const TWO_POWER_FIFTY_THREE: i128 = 9_007_199_254_740_992; + // NOTE: Can FiniteF64 optimize the duration_validation /// Utility function to check whether the `Duration` fields are valid. #[inline] @@ -701,21 +815,18 @@ pub(crate) fn is_valid_duration( // in C++ with an implementation of core::remquo() with sufficient bits in the quotient. // String manipulation will also give an exact result, since the multiplication is by a power of 10. // Seconds part - let normalized_seconds = days.0.mul_add( - 86_400.0, - hours.0.mul_add(3600.0, minutes.0.mul_add(60.0, seconds.0)), - ); + let normalized_seconds = (days.0 as i128 * 86_400) + + (hours.0 as i128) * 3600 + + minutes.0 as i128 * 60 + + seconds.0 as i128; // Subseconds part - let normalized_subseconds_parts = milliseconds.0.mul_add( - 10e-3, - microseconds - .0 - .mul_add(10e-6, nanoseconds.0.mul_add(10e-9, 0.0)), - ); + let normalized_subseconds_parts = (milliseconds.0 as i128 / 1_000) + + (microseconds.0 as i128 / 1_000_000) + + (nanoseconds.0 as i128 / 1_000_000_000); let normalized_seconds = normalized_seconds + normalized_subseconds_parts; // 8. If abs(normalizedSeconds) ≥ 2**53, return false. - if normalized_seconds.abs() >= 2e53 { + if normalized_seconds.abs() >= TWO_POWER_FIFTY_THREE { return false; } @@ -786,7 +897,7 @@ impl FromStr for Duration { let nanoseconds = rem.rem_euclid(1_000); ( - f64::from(hours), + hours as f64, minutes as f64, seconds as f64, milliseconds as f64, @@ -810,8 +921,8 @@ impl FromStr for Duration { let nanoseconds = rem.rem_euclid(1_000); ( - f64::from(hours), - f64::from(minutes), + hours as f64, + minutes as f64, seconds as f64, milliseconds as f64, microseconds as f64, @@ -832,9 +943,9 @@ impl FromStr for Duration { let nanoseconds = rem.rem_euclid(1_000); ( - f64::from(hours), - f64::from(minutes), - f64::from(seconds), + hours as f64, + minutes as f64, + seconds as f64, milliseconds as f64, microseconds as f64, nanoseconds as f64, @@ -855,7 +966,7 @@ impl FromStr for Duration { FiniteF64::from(years).copysign(sign), FiniteF64::from(months).copysign(sign), FiniteF64::from(weeks).copysign(sign), - FiniteF64::from(days).copysign(sign), + FiniteF64::try_from(days)?.copysign(sign), FiniteF64::try_from(hours)?.copysign(sign), FiniteF64::try_from(minutes)?.copysign(sign), FiniteF64::try_from(seconds)?.copysign(sign), diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs index 03c078bb7..77c19c17c 100644 --- a/src/components/duration/normalized.rs +++ b/src/components/duration/normalized.rs @@ -26,8 +26,8 @@ const MAX_TIME_DURATION: i128 = 9_007_199_254_740_991_999_999_999; // Nanoseconds constants const NS_PER_DAY_128BIT: i128 = NS_PER_DAY as i128; -const NANOSECONDS_PER_MINUTE: f64 = 60.0 * 1e9; -const NANOSECONDS_PER_HOUR: f64 = 60.0 * 60.0 * 1e9; +const NANOSECONDS_PER_MINUTE: i128 = 60 * 1_000_000_000; +const NANOSECONDS_PER_HOUR: i128 = 60 * NANOSECONDS_PER_MINUTE; // ==== NormalizedTimeDuration ==== // @@ -44,12 +44,12 @@ pub(crate) struct NormalizedTimeDuration(pub(crate) i128); impl NormalizedTimeDuration { /// Equivalent: 7.5.20 NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, nanoseconds ) pub(crate) fn from_time_duration(time: &TimeDuration) -> Self { - // TODO: Determine if there is a loss in precision from casting. If so, times by 1,000 (calculate in picoseconds) than truncate? - let mut nanoseconds: i128 = (time.hours.0 * NANOSECONDS_PER_HOUR) as i128; - nanoseconds += (time.minutes.0 * NANOSECONDS_PER_MINUTE) as i128; - nanoseconds += (time.seconds.0 * 1_000_000_000.0) as i128; - nanoseconds += (time.milliseconds.0 * 1_000_000.0) as i128; - nanoseconds += (time.microseconds.0 * 1_000.0) as i128; + // Note: Calculations must be done after casting to `i128` in order to preserve precision + let mut nanoseconds: i128 = time.hours.0 as i128 * NANOSECONDS_PER_HOUR; + nanoseconds += time.minutes.0 as i128 * NANOSECONDS_PER_MINUTE; + nanoseconds += time.seconds.0 as i128 * 1_000_000_000; + nanoseconds += time.milliseconds.0 as i128 * 1_000_000; + nanoseconds += time.microseconds.0 as i128 * 1_000; nanoseconds += time.nanoseconds.0 as i128; // NOTE(nekevss): Is it worth returning a `RangeError` below. debug_assert!(nanoseconds.abs() <= MAX_TIME_DURATION); diff --git a/src/components/duration/tests.rs b/src/components/duration/tests.rs index 105b38a14..ff0ab0b3d 100644 --- a/src/components/duration/tests.rs +++ b/src/components/duration/tests.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "experimental")] use crate::{ components::{calendar::Calendar, PlainDate}, options::{RoundingIncrement, TemporalRoundingMode}, @@ -6,6 +7,7 @@ use crate::{ use super::*; +#[cfg(feature = "experimental")] fn get_round_result( test_duration: &Duration, relative_to: RelativeTo, @@ -21,6 +23,7 @@ fn get_round_result( } // roundingmode-floor.js +#[cfg(feature = "experimental")] #[test] fn basic_positive_floor_rounding_v2() { let test_duration = Duration::new( @@ -89,6 +92,7 @@ fn basic_positive_floor_rounding_v2() { } #[test] +#[cfg(feature = "experimental")] fn basic_negative_floor_rounding_v2() { // Test setup let test_duration = Duration::new( @@ -158,6 +162,7 @@ fn basic_negative_floor_rounding_v2() { } // roundingmode-ceil.js +#[cfg(feature = "experimental")] #[test] fn basic_positive_ceil_rounding() { let test_duration = Duration::new( @@ -225,6 +230,7 @@ fn basic_positive_ceil_rounding() { assert_eq!(&result, &[5, 7, 0, 27, 16, 30, 20, 123, 987, 500],); } +#[cfg(feature = "experimental")] #[test] fn basic_negative_ceil_rounding() { let test_duration = Duration::new( @@ -293,6 +299,7 @@ fn basic_negative_ceil_rounding() { } // roundingmode-expand.js +#[cfg(feature = "experimental")] #[test] fn basic_positive_expand_rounding() { let test_duration = Duration::new( @@ -359,6 +366,7 @@ fn basic_positive_expand_rounding() { assert_eq!(&result, &[5, 7, 0, 27, 16, 30, 20, 123, 987, 500],); } +#[cfg(feature = "experimental")] #[test] fn basic_negative_expand_rounding() { let test_duration = Duration::new( @@ -429,6 +437,7 @@ fn basic_negative_expand_rounding() { } // test262/test/built-ins/Temporal/Duration/prototype/round/roundingincrement-non-integer.js +#[cfg(feature = "experimental")] #[test] fn rounding_increment_non_integer() { let test_duration = Duration::from( @@ -609,6 +618,7 @@ fn partial_duration_values() { } // days-24-hours-relative-to-zoned-date-time.js +#[cfg(feature = "experimental")] #[test] fn round_relative_to_zoned_datetime() { let duration = Duration::from( @@ -641,3 +651,171 @@ fn round_relative_to_zoned_datetime() { assert_eq!(result.days(), 1.0); assert_eq!(result.hours(), 1.0); } + +#[test] +fn default_duration_string() { + let duration = Duration::default(); + + let options = ToStringRoundingOptions { + precision: Precision::Auto, + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.to_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0S"); + + let options = ToStringRoundingOptions { + precision: Precision::Digit(0), + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.to_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0S"); + + let options = ToStringRoundingOptions { + precision: Precision::Digit(1), + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.to_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0.0S"); + + let options = ToStringRoundingOptions { + precision: Precision::Digit(3), + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.to_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0.000S"); +} + +#[test] +fn duration_to_string_auto_precision() { + let duration = Duration::new( + 1.into(), + 2.into(), + 3.into(), + 4.into(), + 5.into(), + 6.into(), + 7.into(), + FiniteF64::default(), + FiniteF64::default(), + FiniteF64::default(), + ) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "P1Y2M3W4DT5H6M7S"); + + let duration = Duration::new( + 1.into(), + 2.into(), + 3.into(), + 4.into(), + 5.into(), + 6.into(), + 7.into(), + 987.into(), + 650.into(), + FiniteF64::default(), + ) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "P1Y2M3W4DT5H6M7.98765S"); +} + +#[test] +fn empty_date_duration() { + let duration = Duration::from_partial_duration(PartialDuration { + hours: Some(1.into()), + ..Default::default() + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "PT1H"); +} + +#[test] +fn negative_fields_to_string() { + let duration = Duration::from_partial_duration(PartialDuration { + years: Some(FiniteF64::from(-1)), + months: Some(FiniteF64::from(-1)), + weeks: Some(FiniteF64::from(-1)), + days: Some(FiniteF64::from(-1)), + hours: Some(FiniteF64::from(-1)), + minutes: Some(FiniteF64::from(-1)), + seconds: Some(FiniteF64::from(-1)), + milliseconds: Some(FiniteF64::from(-1)), + microseconds: Some(FiniteF64::from(-1)), + nanoseconds: Some(FiniteF64::from(-1)), + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-P1Y1M1W1DT1H1M1.001001001S"); + + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::from(-250)), + ..Default::default() + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-PT0.25S"); + + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::from(-3500)), + ..Default::default() + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-PT3.5S"); + + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::from(-3500)), + ..Default::default() + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-PT3.5S"); + + let duration = Duration::from_partial_duration(PartialDuration { + weeks: Some(FiniteF64::from(-1)), + days: Some(FiniteF64::from(-1)), + ..Default::default() + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + + assert_eq!(&result, "-P1W1D"); +} + +#[test] +fn preserve_precision_loss() { + const MAX_SAFE_INT: f64 = 9_007_199_254_740_991.0; + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::try_from(MAX_SAFE_INT).unwrap()), + microseconds: Some(FiniteF64::try_from(MAX_SAFE_INT).unwrap()), + ..Default::default() + }) + .unwrap(); + let result = duration + .to_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + + assert_eq!(&result, "PT9016206453995.731991S"); +} diff --git a/src/components/month_day.rs b/src/components/month_day.rs index 956065639..a42ec972c 100644 --- a/src/components/month_day.rs +++ b/src/components/month_day.rs @@ -1,12 +1,16 @@ //! This module implements `MonthDay` and any directly related algorithms. +use alloc::string::String; use core::str::FromStr; use tinystr::TinyAsciiStr; use crate::{ - components::calendar::Calendar, iso::IsoDate, options::ArithmeticOverflow, TemporalError, - TemporalResult, TemporalUnwrap, + components::calendar::Calendar, + iso::IsoDate, + options::{ArithmeticOverflow, DisplayCalendar}, + parsers::{FormattableCalendar, FormattableDate, FormattableMonthDay}, + TemporalError, TemporalResult, TemporalUnwrap, }; /// The native Rust implementation of `Temporal.PlainMonthDay` @@ -17,6 +21,12 @@ pub struct PlainMonthDay { calendar: Calendar, } +impl core::fmt::Display for PlainMonthDay { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.to_ixdtf_string(DisplayCalendar::Auto)) + } +} + impl PlainMonthDay { /// Creates a new unchecked `MonthDay` #[inline] @@ -80,6 +90,17 @@ impl PlainMonthDay { pub fn month_code(&self) -> TemporalResult> { self.calendar.month_code(&self.iso) } + + pub fn to_ixdtf_string(&self, display_calendar: DisplayCalendar) -> String { + let ixdtf = FormattableMonthDay { + date: FormattableDate(self.iso_year(), self.iso_month(), self.iso.day), + calendar: FormattableCalendar { + show: display_calendar, + calendar: self.calendar().identifier(), + }, + }; + ixdtf.to_string() + } } impl FromStr for PlainMonthDay { diff --git a/src/components/timezone.rs b/src/components/timezone.rs index 22c70d1d0..1af3e3b31 100644 --- a/src/components/timezone.rs +++ b/src/components/timezone.rs @@ -77,7 +77,9 @@ impl TimeZoneProvider for NeverProvider { } } -// TODO: migrate to Cow<'a, str> +// TODO: Potentially migrate to Cow<'a, str> +// TODO: There may be an argument to have Offset minutes be a (Cow<'a, str>,, i16) to +// prevent allocations / writing, TBD #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum TimeZone { IanaIdentifier(String), diff --git a/src/components/year_month.rs b/src/components/year_month.rs index 617c3e204..c88214399 100644 --- a/src/components/year_month.rs +++ b/src/components/year_month.rs @@ -6,7 +6,11 @@ use core::str::FromStr; use tinystr::TinyAsciiStr; use crate::{ - components::calendar::Calendar, iso::IsoDate, options::ArithmeticOverflow, utils::pad_iso_year, + components::calendar::Calendar, + iso::IsoDate, + options::{ArithmeticOverflow, DisplayCalendar}, + parsers::{FormattableCalendar, FormattableDate, FormattableYearMonth}, + utils::pad_iso_year, TemporalError, TemporalResult, TemporalUnwrap, }; @@ -20,6 +24,12 @@ pub struct PlainYearMonth { calendar: Calendar, } +impl core::fmt::Display for PlainYearMonth { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.to_ixdtf_string(DisplayCalendar::Auto)) + } +} + impl PlainYearMonth { /// Creates an unvalidated `YearMonth`. #[inline] @@ -151,6 +161,17 @@ impl PlainYearMonth { self.calendar() .year_month_from_partial(&result_fields, overflow) } + + pub fn to_ixdtf_string(&self, display_calendar: DisplayCalendar) -> String { + let ixdtf = FormattableYearMonth { + date: FormattableDate(self.iso_year(), self.iso_month(), self.iso.day), + calendar: FormattableCalendar { + show: display_calendar, + calendar: self.calendar().identifier(), + }, + }; + ixdtf.to_string() + } } impl FromStr for PlainYearMonth { diff --git a/src/parsers.rs b/src/parsers.rs index f6fa3c316..0c31bfa02 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -7,7 +7,10 @@ use crate::{ }; use alloc::format; use ixdtf::parsers::{ - records::{Annotation, DateRecord, IxdtfParseRecord, TimeRecord, UtcOffsetRecordOrZ}, + records::{ + Annotation, DateRecord, DurationParseRecord, IxdtfParseRecord, Sign as IxdtfSign, + TimeDurationRecord, TimeRecord, UtcOffsetRecordOrZ, + }, IxdtfParser, }; use writeable::{impl_display_with_writeable, LengthHint, Writeable}; @@ -245,6 +248,9 @@ impl Writeable for FormattableOffset { } impl_display_with_writeable!(FormattableIxdtf<'_>); +impl_display_with_writeable!(FormattableMonthDay<'_>); +impl_display_with_writeable!(FormattableYearMonth<'_>); +impl_display_with_writeable!(FormattableDuration); impl_display_with_writeable!(FormattableDate); impl_display_with_writeable!(FormattableTime); impl_display_with_writeable!(FormattableUtcOffset); @@ -257,11 +263,7 @@ pub struct FormattableDate(pub i32, pub u8, pub u8); impl Writeable for FormattableDate { fn write_to(&self, sink: &mut W) -> core::fmt::Result { - if (0..=9999).contains(&self.0) { - write_four_digit_year(self.0, sink)?; - } else { - write_extended_year(self.0, sink)?; - } + write_year(self.0, sink)?; sink.write_char('-')?; write_padded_u8(self.1, sink)?; sink.write_char('-')?; @@ -275,6 +277,14 @@ impl Writeable for FormattableDate { } } +fn write_year(year: i32, sink: &mut W) -> core::fmt::Result { + if (0..=9999).contains(&year) { + write_four_digit_year(year, sink) + } else { + write_extended_year(year, sink) + } +} + fn write_four_digit_year( mut y: i32, sink: &mut W, @@ -357,6 +367,84 @@ impl Writeable for FormattableCalendar<'_> { } } +#[derive(Debug)] +pub struct FormattableMonthDay<'a> { + pub date: FormattableDate, + pub calendar: FormattableCalendar<'a>, +} + +impl Writeable for FormattableMonthDay<'_> { + fn write_to(&self, sink: &mut W) -> core::fmt::Result { + if self.calendar.show == DisplayCalendar::Always + || self.calendar.show == DisplayCalendar::Critical + || self.calendar.calendar != "iso8601" + { + write_year(self.date.0, sink)?; + sink.write_char('-')?; + } + write_padded_u8(self.date.1, sink)?; + sink.write_char('-')?; + write_padded_u8(self.date.2, sink)?; + self.calendar.write_to(sink) + } + + fn writeable_length_hint(&self) -> LengthHint { + let base_length = self.calendar.writeable_length_hint() + LengthHint::exact(5); + if self.calendar.show == DisplayCalendar::Always + || self.calendar.show == DisplayCalendar::Critical + || self.calendar.calendar != "iso8601" + { + let year_length = if (0..=9999).contains(&self.date.0) { + 4 + } else { + 7 + }; + return base_length + LengthHint::exact(year_length); + } + base_length + } +} + +#[derive(Debug)] +pub struct FormattableYearMonth<'a> { + pub date: FormattableDate, + pub calendar: FormattableCalendar<'a>, +} + +impl Writeable for FormattableYearMonth<'_> { + fn write_to(&self, sink: &mut W) -> core::fmt::Result { + write_year(self.date.0, sink)?; + sink.write_char('-')?; + write_padded_u8(self.date.1, sink)?; + if self.calendar.show == DisplayCalendar::Always + || self.calendar.show == DisplayCalendar::Critical + || self.calendar.calendar != "iso8601" + { + sink.write_char('-')?; + write_padded_u8(self.date.2, sink)?; + } + + self.calendar.write_to(sink) + } + + fn writeable_length_hint(&self) -> LengthHint { + let year_length = if (0..=9999).contains(&self.date.0) { + 4 + } else { + 7 + }; + let base_length = + self.calendar.writeable_length_hint() + LengthHint::exact(year_length + 3); + if self.calendar.show == DisplayCalendar::Always + || self.calendar.show == DisplayCalendar::Critical + || self.calendar.calendar != "iso8601" + { + return base_length + LengthHint::exact(3); + } + base_length + } +} + #[derive(Debug, Default)] pub struct FormattableIxdtf<'a> { pub date: Option, @@ -424,6 +512,120 @@ impl Writeable for FormattableIxdtf<'_> { } } +pub struct FormattableDuration { + pub precision: Precision, + pub duration: DurationParseRecord, +} + +impl Writeable for FormattableDuration { + fn write_to(&self, sink: &mut W) -> core::fmt::Result { + if self.duration.sign == IxdtfSign::Negative { + sink.write_char('-')?; + } + sink.write_char('P')?; + if let Some(date) = self.duration.date { + checked_write_u32_with_suffix(date.years, 'Y', sink)?; + checked_write_u32_with_suffix(date.months, 'M', sink)?; + checked_write_u32_with_suffix(date.weeks, 'W', sink)?; + checked_write_u64_with_suffix(date.days, 'D', sink)?; + } + if let Some(time) = self.duration.time { + match time { + TimeDurationRecord::Hours { hours, fraction } => { + if hours + fraction != 0 { + sink.write_char('T')?; + } + if hours == 0 { + return Ok(()); + } + hours.write_to(sink)?; + if fraction != 0 { + sink.write_char('.')?; + fraction.write_to(sink)?; + } + sink.write_char('H')?; + } + TimeDurationRecord::Minutes { + hours, + minutes, + fraction, + } => { + if hours + minutes + fraction != 0 { + sink.write_char('T')?; + } + checked_write_u64_with_suffix(hours, 'H', sink)?; + if minutes == 0 { + return Ok(()); + } + minutes.write_to(sink)?; + if fraction != 0 { + sink.write_char('.')?; + fraction.write_to(sink)?; + } + sink.write_char('M')?; + } + TimeDurationRecord::Seconds { + hours, + minutes, + seconds, + fraction, + } => { + let unit_below_minute = + self.duration.date.is_none() && hours == 0 && minutes == 0; + + let write_second = seconds != 0 + || unit_below_minute + || matches!(self.precision, Precision::Digit(_)); + + if hours != 0 || minutes != 0 || write_second { + sink.write_char('T')?; + } + + checked_write_u64_with_suffix(hours, 'H', sink)?; + checked_write_u64_with_suffix(minutes, 'M', sink)?; + if write_second { + seconds.write_to(sink)?; + if self.precision == Precision::Digit(0) + || (self.precision == Precision::Auto && fraction == 0) + { + sink.write_char('S')?; + return Ok(()); + } + sink.write_char('.')?; + write_nanosecond(fraction, self.precision, sink)?; + sink.write_char('S')?; + } + } + } + } + Ok(()) + } +} + +fn checked_write_u32_with_suffix( + val: u32, + suffix: char, + sink: &mut W, +) -> core::fmt::Result { + if val == 0 { + return Ok(()); + } + val.write_to(sink)?; + sink.write_char(suffix) +} + +fn checked_write_u64_with_suffix( + val: u64, + suffix: char, + sink: &mut W, +) -> core::fmt::Result { + if val == 0 { + return Ok(()); + } + val.write_to(sink)?; + sink.write_char(suffix) +} + // TODO: Determine if these should be separate structs, i.e. TemporalDateTimeParser/TemporalInstantParser, or // maybe on global `TemporalParser` around `IxdtfParser` that handles the Temporal idiosyncracies. enum ParseVariant { diff --git a/src/primitive.rs b/src/primitive.rs index bd40f6f10..c6233a92e 100644 --- a/src/primitive.rs +++ b/src/primitive.rs @@ -6,6 +6,12 @@ use num_traits::{AsPrimitive, FromPrimitive, PrimInt}; #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] pub struct FiniteF64(pub(crate) f64); +impl core::fmt::Display for FiniteF64 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_fmt(format_args!("{}", self.0)) + } +} + impl FiniteF64 { #[inline] pub fn as_inner(&self) -> f64 {