Skip to content

[core] Change the date/time API to a more race-unfriendly one#737

Merged
Enet4 merged 2 commits intoEnet4:masterfrom
pgimeno4d:date-time-now-api
Mar 2, 2026
Merged

[core] Change the date/time API to a more race-unfriendly one#737
Enet4 merged 2 commits intoEnet4:masterfrom
pgimeno4d:date-time-now-api

Conversation

@pgimeno4d
Copy link
Copy Markdown
Contributor

@pgimeno4d pgimeno4d commented Feb 9, 2026

Fix #691 with a different approach: DicomDateTime now implements two methods separate_date_time_now_local and separate_date_time_now_utc, both of which return both a DicomDate and a DicomTime at the same time, based on the same timestamp. Also, DicomDateTime::now_local() and DicomDateTime::now_utc() are no longer based on separate calls to obtain a date and a time, which was open to races.

Add some docs while on it.

@pgimeno4d
Copy link
Copy Markdown
Contributor Author

There has been no activity here, so I wonder if it's because the approach is unacceptable. I'm willing to make corrections - I'm aware that returning a DicomDate and a DicomTime element from within the DicomDateTime type may look a bit awkward, for one.

However, returning both at the same time encourages using both when both are required, which is probably most of the time, and that'd be race-free, so if parasiting DicomDateTime for that purpose is considered bad design, maybe it can be rewritten as just an isolated function instead of an associated one.

Removing that part and leaving only DicomDateTime::now_local() and DicomDateTime::now_utc() is also OK.

@Enet4 Enet4 added A-lib Area: library C-core Crate: dicom-core labels Feb 25, 2026
Copy link
Copy Markdown
Owner

@Enet4 Enet4 left a comment

Choose a reason for hiding this comment

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

I just have not had a lot of time lately for a more thorough look into the PRs. 😓

I agree that now_* should be dropped from DicomDate and DicomTime, thus forcing users to call DicomDateTime::now_* instead. I would not include separate_date_time_now_local or separate_date_time_now_utc, but rather tell users to call one of the now_* methods and then offer a way to split a DicomDateTime into those two parts (something like pub fn into_separate(self) -> (DicomDate, DicomTime)).

@pgimeno4d
Copy link
Copy Markdown
Contributor Author

I do see issues with into_separate(), as it involves string manipulation of an ambiguous grammar. A DT VR can contain a range, when used inside a query object for a C-FIND identifier. Furthermore, the DT specification allows omitting all fields beyond any given one; for example "2000" is a valid DT value that omits all fields beyond the year. It can also contain a timezone offset, which can be negative; for example, "-2000" is a time zone offset that can be contained as part of a DT element. So, 1999-2000 could be either the date/time 1999 with time zone offset -2000, or the date/time range covering from the start of 1999 to the end of 2000. (Arguably, that's an invalid timezone offset, as the standard specifies that the range of the offset is from -1200 to +1400, but discerning that just adds to the overall difficulty.)

It's quite difficult to handle all special cases than can arise in such situations. I guess that such a function could return an error if the DT is not complete at least up to the hours, because if there's no hour at least, there would be no valid DicomTime object to return. It could also return an error if the date/time represents a range. It's not clear what it should do in presence of multiple values (VM > 1), but that case should probably be contemplated as well in a generic into_separate() function.

In short, a generic function that splits any DicomDateTime into a DicomDate and a DicomTime is much harder than just producing a single value from the current date and time.

One way it could be made to work with little effort, however, is to only accept exactly the format produced by DicomDateTime::now_local() or now_utc() and err on others. Still, parsing that for verification purposes in order to give an error if it's not the expected format, is a bit involved without using regular expressions.

@Enet4
Copy link
Copy Markdown
Owner

Enet4 commented Mar 2, 2026

Those are good concerns, but wouldn't the textual ambiguity be located at the parsing level? A DicomDateTime already contains a DicomDate and a DicomTime underneath, and these are binary encoded types with compliant invariants.
If anything, we may revisit the existing date-time parsers to mitigate some of the situations mentioned here. I honestly do not remember right now how date ranges are parsed, though I know there was some level of consideration for this use case.

It is true that the operation might not succeed or lose some information due to the inherent variable precision of DT, so we would need to adjust the return type accordingly: (DicomDate, Option<DicomTime>). Users of the method would also lose the time zone if they split it into two parts, but we can also add a method pub fn into_parts(self) -> (DicomDate, Option<DicomTime>, Option<FixedOffset>) to the bunch.

impl DicomDateTime {
    fn into_separated(self) -> (DicomDate, Option<DicomTime>) {
        (self.date, self.time)
    }
}

@pgimeno4d
Copy link
Copy Markdown
Contributor Author

pgimeno4d commented Mar 2, 2026

You're totally right, of course. I didn't look at the implementation of DicomDateTime at all, I just focused on what the prior PR author did. I wasn't aware that a DicomDateTime was actually a DicomDate + Option<DicomTime> + Option<FixedOffset>; I was under the impression that the value was stored as a string, but if it's already like that, there's no problem in doing what you suggest. I'll work on that, thanks for the feedback.

Edit: By the way, wouldn't into_parts() be enough? You can always do something like let (date, time, _) = DicomDateTime::now_local().into_parts() if you're not interested in the offset.

@Enet4
Copy link
Copy Markdown
Owner

Enet4 commented Mar 2, 2026

Edit: By the way, wouldn't into_parts() be enough? You can always do something like let (date, time, _) = DicomDateTime::now_local().into_parts() if you're not interested in the offset.

Well yes, that would be enough. I was mostly following up on the idea of having a function which returned a date and time, but into_parts seems to make more sense in the end, and does not exclude us from considering into_separated or into_date_and_time in the future if deemed reasonable (which we might never do 🤔).

@pgimeno4d
Copy link
Copy Markdown
Contributor Author

So, the patch was actually pretty simple. My only concern is if the documentation for into_parts() is insufficient.

Copy link
Copy Markdown
Owner

@Enet4 Enet4 left a comment

Choose a reason for hiding this comment

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

The documentation for into_parts does not have to be extensive, given its simplicity.

I say we're good to merge! Thank you for your continued cooperation!

@Enet4 Enet4 merged commit a560858 into Enet4:master Mar 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-lib Area: library C-core Crate: dicom-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Could use DicomDateTime::now

2 participants