❔ Converting my project to NodaTime
Hi there, after some conversation in my previous partially related thread #Accounting for Daylight Savings Time skips at runtime, I'm planning on converting existing functionality in my project (a Discord bot) to NodaTime's types instead of the BCL types, to avoid issues that might occur from constantly converting to and from different time-related types.
There are a lot of classes in NodaTime that can be used to represent a "moment in time" so to speak, so I'd like to know which ones I should use for different situations.
Disclaimer: I am using EFCore with PostgreSQL, and will now be using
Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime
for storing appropriate types.
- Most of my database entities utilize DateTimeOffset
for their creation date and time - always stored in UTC as I believe EFCore cannot store DateTimeOffset
s with a non-UTC offset. I'm unsure whether to use OffsetDateTime
, ZonedDateTime
, or Instant
.
- Many classes implement a timer interface, and utilize a DateTimeOffset
for when the timer "expires". See the linked thread for issues related to Daylight Savings Time - I believe I will need to store a LocalTime
or Instant
and convert it to the user's timezone at runtime (see next point)
- Users can set their timezone via a command - allowing using https://github.com/robbell/nChronic.Core for natural date parsing in their local time, instead of using UTC for everything. I will likely store their timezone as TimeZoneInfo
instead of TimeSpan
which I previously used.
Any suggestions for how to handle these types of situations? I'm trying to make sure I can make this transition as clean and bug-free as possible, since I have a lot of logic implemented that relies on the BCL time/date types.35 Replies
these links should answer most of what you're asking
https://nodatime.org/3.1.x/userguide/type-choices
https://www.npgsql.org/doc/types/nodatime.html?tabs=datasource
I can say for the first one, you want an
Instant
a creation time is an unambiguous point on the global timeline, which is what Instant is for
also, you rarely want OffsetDateTime
it's like BCL's DateTimeOffset, it doesn't store a timezone, just a UTC offsetAnother concern of mine is related to what the best NodaTime equivalent is to
DateTimeOffset.UtcNow
. There doesn't seem to be an Instant.Now
, and https://stackoverflow.com/questions/14531627/instant-now-for-nodatime's answers are...not filling me with confidence. Especially when it comes to storing creation times in a "universal" state, what happens if I construct an Instant
in my testing environment, where my computer's time is in US Central Time, but my production environment is on a device or container in UTC? "Now" would be way off.Instant doesn't have a timezone
it's always in UTC effectively
as for why there's no Instant.Now, that's because they come from clocks
so per that SO thread,
SystemClock.Instance.GetCurrentInstant()
would return "now" as an instant in UTC and not my system's local timezone/offset?NodaTime provides two versions of
IClock
SystemClock
(accessible through SystemClock.Instance
) always uses real world time
and FakeClock
is for testing, and lets you control exactly what it returnsGuess since I have testing commands I could TIAS
yes
it's an instant on the global timeline
it doesn't interact with timezones at all
for that you need LocalDate/LocalTime/LocalDateTime (which has no timezone info, and is assumed to be local) or the various Zoned types
gotcha. I assumed Instant was sorta like doing
DateTimeOffset.Now
and was always going to get messed up with timezonesno
naming was a little confusing, it's an "instant" in time obviously, but to me this instant is time in 1:55 PM, and to my friend in the UK it's several hours later, so it wasn't straightforward to me, but I get it now
it explicitly does not interact with timezones at all
@jcotton42 this might be a complicated or very simple question, I'm not sure. I'm converting a TimeSpan parser to utilize NT's
Period
class, as it has proper support for adding years/months/weeks/etc.
My current logic is like so (abbreviated for clarity):
What do I do with differencePeriod
to determine that it is non-zero? Period
doesn't seem to implement equality operators.
Do I just compare the result of differencePeriod.ToDuration()
?it does though?
but note this
Period equality is implemented by comparing each property's values individually, without any normalization. (For example, a period of "24 hours" is not considered equal to a period of "1 day".) The static NormalizingEqualityComparer comparer provides an equality comparer which performs normalization before comparisons.
er...by equality I meant greater than, my bad
ah
what I did was
period = differencePeriod > Period.Zero ? differencePeriod : null;
then yeah, I'd convert to Duration first
but it doesn't have that operator implemented,
I think I'll use the duration method, as long as it works out. I think the comparer is overkill since it looks like it's intended for comparing "1 month" to "30 days" to see if they are equivalent. I'm just comparing to a zero value 😛
Now I need to find a way to add a Period to either an Instant or a ZonedDateTime...
I also had a (command) parser for DateTimeOffset, which I am now repurposing for either Instant or ZonedDateTime, I think the latter makes more sense given the context (a user specifying a place in time like
tomorrow at noon
, which needs to be timezone aware)the raw input would be LocalDateTime, I think
you would then combine that with the timezone setting to make a ZonedDateTime or Instant
That makes sense, that's actually what I was starting to do, but I'm unsure how to "combine" it properly.
Wouldn't this result in an issue somewhere? I'm getting the LocalDateTime from a ZonedDateTime, adding a Period, and then...converting it back to a ZonedDateTime?
I assumed because LocalDateTime isn't timezone aware, I don't know what happens when I apply
InZoneLeniently()
you can add durations to a zoned datetime
it's to handle DST and similar shenanigans https://nodatime.org/3.0.x/api/NodaTime.LocalDateTime.html#NodaTime_LocalDateTime_InZoneLeniently_NodaTime_DateTimeZone_
so this would suffice?
The good and bad of NodaTime is that there's so many ways to express similar human concepts of time, but I'm always unsure which is more applicable sometimes. I wasn't sure if converting to a Duration would make it lose track of things like differing amounts of days in a month, leap years, etc...
yeah, that was what I learned in the previous DST thread you helped me with 😅 - that much makes sense
a Duration is a fixed length of time
so a number of seconds I guess
ZonedDateTime is aware of both the time zone and the calendar system in use for it, so it will handle all that for you
Yeah, more similar to TimeSpan. Just wasn't sure since converting
1 month
to a number of seconds would vary depending on what month it is know for sure once I'm done with my conversion to NT's types what's working and what's not, all my paranoia is purely speculative since I haven't gotten around to actually testing yetactually, that might not work? https://nodatime.org/3.0.x/api/NodaTime.Period.html#NodaTime_Period_ToDuration
ugh, months are annoying
you're probably gonna have to sit down and give the docs are thorough read
Periods operate on calendar-related types such as LocalDateTime whereas Duration operates on instants on the time line. (Note that although ZonedDateTime includes both concepts, it only supports duration-based arithmetic.)i'm gonna do some more digging
Yeah, I can do it with LocalDateTime it seems. My only hangup is making sure the result is in the correct timezone after I'm done doing the math. Hence my earlier message:
Wouldn't this result in an issue somewhere? I'm getting the LocalDateTime from a ZonedDateTime, adding a Period, and then...converting it back to a ZonedDateTime? I assumed because LocalDateTime isn't timezone aware, I don't know what happens when I apply InZoneLeniently()TIAS, as always
I'm developing a headache, and I'm not even the one writing this code
The funny part is, all of this headache-inducing code could be avoided if Discord would just add a damn time/date picker for slash commands
then bots could just be sent a UTC-relative timestamp or duration or whatever and we can just all be happy and hold hands and dance in circles
that would require Discord to do work
like, there's still no multi-line input for slash commands
no, modals don't count
or dropdowns in modals even though they WERE implemented but undocumented and then removed, or radio buttons in modals, or ANYTHING in modals
Was this issue resolved? If so, run
/close
- otherwise I will mark this as stale and this post will be archived until there is new activity.