DateTime Curiosity: Dates.epochms2datetime

I’m new to Julia but not new to programming, and I’m wondering why Dates.epochms2datetime returned a negative value.

using Dates

unix_0 = unix2datetime(0)
# 1970-01-01T00:00:00
# So far, so good.

Dates.value(unix_0)
# 62135683200000
# OK.

epoch_0 = Dates.epochms2datetime(0)
# 0000-01-01T00:00:00
# Alright.

Dates.value(epoch_0)
# -31536000000
# Whoa, why is this negative?

Why is Dates.value(epoch_0) negative?

I can work with this, but I’m still wondering why Dates.value(epoch_0) returned a negative value. If anyone could enlighten me, I would appreciate it.

Context

I have a lot of timestamps that are stored as milliseconds since the Unix epoch, and I wanted to create DateTime instances out of them. That led to experimenting with epochms2datetime and being surprised by its behavior.

epoch_unix_fail = Dates.epochms2datetime(Dates.value(unix_0))
# 1969-01-01T00:00:00
# This is 1 year before the real Unix epoch.

epoch_unix = Dates.epochms2datetime(
	Dates.value(unix_0) + abs(Dates.value(epoch_0))
)
# 1970-01-01T00:00:00
# It became correct after I added abs(Dates.value(epoch_0)) .

epoch_unix == unix_0
# true

Because it is.

fyi, often there is a better (safer, more robust) way than calling Dates.value directly – Dates.value exists for internal support and its use requires very careful implementation, it throws away units and with dates, times that can be trouble.

using Dates
# the zero for the unix calendarclock
# remember unix timepoints are floating point values
#    for timestamping, integer quantities are much preferred
unixtime0 = unix2datetime(0.0)   # 1970-01-01T00:00:00

# Dates.epochms2datetime is an internal (non-exported) function
#    again, often there is a better (safer, more robust) way than using internals
#=
   Dates.epochms2datetime
      take a number, one that must be a raw count of milliseconds,
      implicitly, this number is an offset from this time point rata2datetime(1),
=#
ratatime0 = rata2datetime(0)
ratatime1 = rata2datetime(1)
canonicalize(ratatime1 - ratatime0) # 1 day
# rata values are calendric, sure the underlying implementation uses millis
#    and that is another good reason not to use Dates.value.
#=
   meanwhile the unixtime0 offset is a floating point value, sure
   the integer part counts seconds and the fractional part counts milliseconds.
   however it does not count them as (second::Int, millisecond::Int),
   so with expansive ranges or unexpected corner cases,
   mixing integer valued temporal offsets with float value offsets
   is strongly disrecommended.
=#

For accurate timestamping and other nice capabilities,
I wrote NanoDates.jl

1 Like

@JeffreySarnoff - Thank you for your detailed answer.

unix2datetime

  • I didn’t realize it took a floating point parameter.
  • I assumed it took integer seconds.
  • I would prefer to stay integer-only if possible like you said.

Dates.epochms2datetime

  • Although it wasn’t exported, it showed up in the documentation, so I thought it was fair game. (I wasn’t trying to be hacky.)
  • At first glance, it looked like what I needed.
    • I wanted to turn milliseconds into DateTimes.
  • It surprised me though.

NanoDates.jl

  • I looked at your library, and I think I understand some of your motivations for writing it.
    • It looks like you correctly unified Date and Time without losing any precision on the time side.
  • The Regulatory Note was interesting.
    • I’m not a high frequency trader, but it’s good to know about those regulations.
  • Is there a way to create a NanoDate from milliseconds?
    • I didn’t see it in the docs, but I may have missed it.

you’ll like this

using Dates, NanoDates

const Time0 = NanoDate(DateTime(1970,1,1)) # UNIX 

millis2nanodate(millis::Millisecond) = Time0 + millis

millis_today     = Millisecond( now() - Time0)
millis_lastyear = Millisecond( (now() - Year(1)) - Time0)

today     = millis2nanodate(millis_today)
lastyear = millis2nanodate(millis_lastyear)

typeof(today), typeof(lastyear)
2 Likes

That’s exactly what I needed. I’ll give NanoDates.jl a spin.

@JeffreySarnoff - I wonder if that’s a useful enough function to include in your library. You were able to easily implement millis2nanodate, because you’re familiar with your own library, but it would have took me a little while to figure out how to do it. Before you posted your code, I had actually spent some time writing a similar unixms2datetime.

_unix_epoch_ms =
    Dates.value(unix2datetime(0)) +
    abs(Dates.value(Dates.epochms2datetime(0)))

function _unixms2datetime(ms)
    Dates.epochms2datetime(_unix_epoch_ms + ms)
end

The function may be simple, but arriving at the right values to add wasn’t obvious and took some time. Including millis2nanodate may save future users of your library a lot of time too.

My use case is dealing with timestamps coming from crypto exchanges, and they often come in milliseconds since the Unix epoch. It’s actually the only kind of timestamp I’ve encountered so far.

1 Like

Thank you for the suggestion. I am adding it to the docs as an example.
And may export it once I have good tests for the function.

2 Likes