Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds accessor methods for Timestamp #482

Merged
merged 6 commits into from
Mar 17, 2023
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 248 additions & 3 deletions src/types/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,6 @@ pub struct Timestamp {
pub(crate) fractional_seconds: Option<Mantissa>,
}

// TODO: Timestamp does not yet provide useful accessors for its individual fields. It can be
// instantiated and tested for equality, but will not very useful as a general purpose
// datetime until these methods are added.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

impl Timestamp {
/// Converts a [`NaiveDateTime`] or [`DateTime<FixedOffset>`] to a Timestamp with the specified
/// precision. If the precision is [`Precision::Second`], nanosecond precision (the maximum
Expand Down Expand Up @@ -572,6 +569,102 @@ impl Timestamp {
pub fn precision(&self) -> Precision {
self.precision
}

/// Returns the year that has been specified in the [Timestamp].
desaikd marked this conversation as resolved.
Show resolved Hide resolved
pub fn year(&self) -> i32 {
// verify if the timestamp has an offset
if let Some(offset) = self.offset {
// `NaiveDateTime#hours()` returns hours normalized as per UTC
// for local time we need to +/- the difference
let local_date_time = DateTime::<FixedOffset>::from_utc(self.date_time, offset);
return local_date_time.year();
}
self.date_time.year()
}

/// Returns the month that has been specified in the [Timestamp].
/// Returns the month number starting from 1.
/// The return value ranges from 1 to 12.
pub fn month(&self) -> u32 {
// verify if the timestamp has an offset
if let Some(offset) = self.offset {
// `NaiveDateTime#hours()` returns hours normalized as per UTC
// for local time we need to +/- the difference
let local_date_time = DateTime::<FixedOffset>::from_utc(self.date_time, offset);
return local_date_time.month();
}
self.date_time.month()
}

/// Returns the day that has been specified in the [Timestamp].
/// Returns the day of month starting from 1.
// The return value ranges from 1 to 31. (The last day of month differs by months.)
pub fn day(&self) -> u32 {
// verify if the timestamp has an offset
if let Some(offset) = self.offset {
// `NaiveDateTime#hours()` returns hours normalized as per UTC
// for local time we need to +/- the difference
let local_date_time = DateTime::<FixedOffset>::from_utc(self.date_time, offset);
return local_date_time.day();
}
self.date_time.day()
}

/// Returns the hour(s) that has been specified in the [Timestamp].
/// Returns the hour number from 0 to 23.
pub fn hour(&self) -> u32 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about returning an Option from these functions so that they can reflect the precision of the timestamp?
E.g.

/// Returns the hour(s) that has been specified in the [Timestamp] if the timestamp's precision 
/// includes hours. If you need hour, regardless of precision, use `hour().unwrap_or_default()`,
/// which will correctly return an hour of `0` when the precision does not include hours.
pub fn hour(&self) -> Option<u32> {
    // ...
}

I think it makes it match the data model a little better. It's a little more verbose when you always want some value, but it's a little more ergonomic when you only want to deal with the subfields that are included by the timestamp's precision. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good observation. I'm unsure which way to go on it; Ion Timestamps always have a hour/minutes/second in that they represent a fixed point in time and those fields are 0 when the precision is too low. However, this does represent an opportunity to remind users that the 0 might have been implicit rather than explicitly part of the value.

I suspect that as a generalized datetime value, providing field defaults is probably what users would prefer. @jpschorr, what's your take on this? Would you rather check the timestamp's precision, or hour().is_none()?

Copy link
Contributor Author

@desaikd desaikd Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+ 1. I think it would rather simple to check precision instead of hour().is_none().

// verify if the timestamp has an offset
if let Some(offset) = self.offset {
// `NaiveDateTime#hours()` returns hours normalized as per UTC
// for local time we need to +/- the difference
let local_date_time = DateTime::<FixedOffset>::from_utc(self.date_time, offset);
return local_date_time.hour();
}
self.date_time.hour()
}

/// Returns the minute(s) that has been specified in the [Timestamp].
/// Returns the minute number from 0 to 59.
pub fn minute(&self) -> u32 {
// verify if the timestamp has an offset
if let Some(offset) = self.offset {
// `NaiveDateTime#hours()` returns minutes normalized as per UTC
// for local time we need to +/- the difference
let local_date_time = DateTime::<FixedOffset>::from_utc(self.date_time, offset);
return local_date_time.minute();
}
self.date_time.minute()
}

/// Returns the second(s) that has been specified in the [Timestamp].
/// Returns the second number from 0 to 59.
pub fn second(&self) -> u32 {
self.date_time.second()
}

/// Return a UTC timestamp for this [Timestamp]
pub fn to_utc(&self) -> Timestamp {
self.date_time.into()
}

/// Returns this Timestamp's fractional seconds in nanoseconds
///
/// NOTE: This is a potentially lossy operation. A Timestamp with picoseconds would return a
/// number of nanoseconds, losing precision. Similarly, a Timestamp with milliseconds would
/// also return a number of nanoseconds, erroneously gaining precision.
pub fn nanoseconds(&self) -> u32 {
self.fractional_seconds_as_nanoseconds().unwrap_or_default()
}
desaikd marked this conversation as resolved.
Show resolved Hide resolved

/// Returns this Timestamp's fractional seconds in milliseconds
///
/// NOTE: This is a potentially lossy operation. A Timestamp with picoseconds would return a
/// number of milliseconds, losing precision.
pub fn milliseconds(&self) -> u32 {
self.fractional_seconds_as_nanoseconds()
.map(|s| s / 1000000)
.unwrap_or_default()
}
}

/// Formats an ISO-8601 timestamp of appropriate precision and offset.
Expand Down Expand Up @@ -1584,6 +1677,158 @@ mod timestamp_tests {
Ok(())
}

#[test]
fn test_timestamp_year() -> IonResult<()> {
let timestamp_1 = Timestamp::with_year(2021).with_month(2).build()?;
assert_eq!(timestamp_1.year(), 2021);

let timestamp_2 =
Timestamp::with_ymd_hms(2021, 12, 31, 10, 15, 30).build_at_offset(-11 * 60)?;

assert_eq!(timestamp_2.month(), 12);

let timestamp_3 =
Timestamp::with_ymd_hms(2021, 12, 31, 15, 15, 30).build_at_offset(10 * 60)?;

assert_eq!(timestamp_3.month(), 12);
desaikd marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}

#[test]
fn test_timestamp_month() -> IonResult<()> {
let timestamp_1 = Timestamp::with_year(2021).with_month(2).build()?;
assert_eq!(timestamp_1.month(), 2);

let timestamp_2 =
Timestamp::with_ymd_hms(2021, 1, 31, 10, 15, 30).build_at_offset(-11 * 60)?;

assert_eq!(timestamp_2.month(), 1);

let timestamp_3 =
Timestamp::with_ymd_hms(2021, 1, 31, 15, 15, 30).build_at_offset(10 * 60)?;

assert_eq!(timestamp_3.month(), 1);

Ok(())
}

#[test]
fn test_timestamp_day() -> IonResult<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some tests for the year/month/day accessors where the Timestamp has high precision and an offset.

let timestamp_1 = Timestamp::with_year(2021).with_month(2).build()?;
assert_eq!(timestamp_1.day(), 1);

let timestamp_2 = Timestamp::with_year(2021)
.with_month(2)
.with_day(4)
.build()?;

assert_eq!(timestamp_2.day(), 4);

let timestamp_3 =
Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-11 * 60)?;

assert_eq!(timestamp_3.day(), 6);

let timestamp_4 =
Timestamp::with_ymd_hms(2021, 4, 6, 15, 15, 30).build_at_offset(10 * 60)?;

assert_eq!(timestamp_4.day(), 6);

Ok(())
}

#[rstest]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-90), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-5 * 60), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(5 * 60), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(15), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(30), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(0), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 0, 15, 30).build_at_offset(5 * 60), 0)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 23, 15, 30).build_at_offset(-5 * 60), 23)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 0, 15, 30).build_at_offset(23 * 60), 0)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-11 * 60), 10)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 15, 15, 30).build_at_offset(10 * 60), 15)]
fn test_timestamp_hour(
#[case] timestamp: IonResult<Timestamp>,
#[case] expected_hours: u32,
) -> IonResult<()> {
assert_eq!(timestamp?.hour(), expected_hours);
Ok(())
}

#[rstest]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-90), 15)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-5 * 60), 15)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(5 * 60), 15)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(0), 15)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 0, 30).build_at_offset(5 * 60), 0)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 59, 30).build_at_offset(5 * 60), 59)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-11 * 60), 15)]
#[case(Timestamp::with_ymd_hms(2021, 4, 6, 15, 15, 30).build_at_offset(10 * 60), 15)]
fn test_timestamp_minute(
#[case] timestamp: IonResult<Timestamp>,
#[case] expected_minutes: u32,
) -> IonResult<()> {
assert_eq!(timestamp?.minute(), expected_minutes);
Ok(())
}

#[test]
fn test_timestamp_second() -> IonResult<()> {
let timestamp = Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-5 * 60)?;
assert_eq!(timestamp.second(), 30);
Ok(())
}

#[test]
fn test_timestamp_nanoseconds() -> IonResult<()> {
let timestamp_1 = Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30)
.with_nanoseconds(192)
.build_at_offset(-5 * 60)?;
assert_eq!(timestamp_1.nanoseconds(), 192);

let timestamp_2 = Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30)
.with_milliseconds(192)
.build_at_offset(-5 * 60)?;
assert_eq!(timestamp_2.nanoseconds(), 192000000);

let timestamp_3 =
Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-5 * 60)?;
assert_eq!(timestamp_3.nanoseconds(), 0);

Ok(())
}

#[test]
fn test_timestamp_milliseconds() -> IonResult<()> {
let timestamp_1 = Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30)
.with_milliseconds(192)
.build_at_offset(-5 * 60)?;
assert_eq!(timestamp_1.milliseconds(), 192);

let timestamp_2 =
Timestamp::with_ymd_hms(2021, 4, 6, 10, 15, 30).build_at_offset(-5 * 60)?;
assert_eq!(timestamp_2.milliseconds(), 0);
Ok(())
}

#[test]
fn test_timestamp_to_utc() -> IonResult<()> {
let new_years_eve_nyc =
Timestamp::with_ymd_hms(2022, 12, 31, 23, 59, 00).build_at_offset(-5 * 60)?;

let london = new_years_eve_nyc.to_utc();
assert_eq!(london.year(), 2023);
assert_eq!(london.month(), 1);
assert_eq!(london.day(), 1);
assert_eq!(london.hour(), 4);
assert_eq!(london.minute(), 59);
assert_eq!(london.second(), 0);
Ok(())
}

#[test]
fn test_timestamp_fractional_seconds_scale() -> IonResult<()> {
// Set fractional seconds as Decimal
Expand Down