Why do time quantities have to be integers?

What specific problem are you trying to prevent? If someone wants an exact period, they can use Millisecond(1.0) or Millisecond(1). If they don’t care about this, they can also use Second(1e-3).

Disallowing Second(1e-3) on the grounds that the inexactness is unexpected sounds like an argument for disallowing inexact floating-point literals entirely. We aren’t going to do that in general, so why should we do that in a time-period context?

OK, now there’s a PR in Unitful.jl dealing with this. @JeffreySarnoff, I used your code, hope that’s OK.

I posted it for use

1 Like

@stevengj All your points are well-taken. And, in most cases, I agree.
All of my comments in this thread are just for time stuff and should not be read as anything more general. For applications like determining the labels on an axis, where relatively coarse proportional division is needed, then the kind of floating-point multiply I sketched above is available.

The difficulty with float input to time algorithms is that it becomes easier to do something inadvertently that one may care about. Time stuff is so broadly used that third parties who are not being so careful are the ones who get burned first. One may implement the floating point versions of some temporal logic in a well-guarded manner, but that carries more overhead. For widely disseminated code that “looks” to be appropriate for use in coarse and finely resolved contexts, playing it quite safe seems preferable.

The current DateTime CompoundPeriod implementation does not do everything you may want of it. The following is not well tested, and omits handling for Inf, NaN. It is a reasonable approach where inexactness is ok and floating-point scaling is desired.

using Base.Dates
import Base.Dates: CompoundPeriod

function Base.sum(x::CompoundPeriod)
    return sum(typeof(x.periods[end]).(x.periods))
end

function Base.:(*)(x::AbstractFloat, y::CompoundPeriod)
    return x * sum(y)
end

function Base.:(*)(x::AbstractFloat, p::Period)            
    n = Nanosecond(p)                                      
    n_scaled = x * n.value                                 
    nanos = trunc(Int64, round(n_scaled))
    micros, nanos    = divrem(nanos, 1000)                 
    millis, micros   = divrem(micros, 1000)                
    seconds, millis  = divrem(millis, 1000)                
    minutes, seconds = divrem(seconds, 60)                 
    hours, minutes   = divrem(minutes, 60)                 
    days, hours      = divrem(hours, 24)                   
    result = Nanosecond(nanos) + Microsecond(micros) +     
             Millisecond(millis) + Second(seconds) +   
             Minute(minutes) + Hour(hours) + Day(days) 
    return result                                          
end                                                        

function Base.:(/)(p::CompoundPeriod, x::AbstractFloat)
    return sum(p) / x
end

function Base.:(/)(p::Period, x::AbstractFloat)
    x == 0.0 && throw(DomainError())
    return inv(x) * p
end

const AkoPeriod = Union{Period, CompoundPeriod}
Base.:(*)(x::Real, p::P) where P<:AkoPeriod = Float64(x) * p
Base.:(*)(p::P, x::Real) where P<:AkoPeriod = Float64(x) * p
Base.:(/)(p::P, x::Real) where P<:AkoPeriod = p / Float64(x)

Time ultimately is continuous, just represented in some bizarre base 24/60/365.25 way.

1 Like

I find it hard to be persuaded by vague FUD about “people getting burned”. What is a specific example of a use case in which someone needs exact time periods, and is happy working with Int64 values, but will get inadvertently “burned” if Period supports Float64?

Is there functionality that I overlooked which would provide better Float64 scaling?

I don’t just want Float64 * Period, I want Period types to actually store their data as a Float64.

Reading this thread I keep thinking about special and general relativity and the fact that there is no such thing as universal time or universal simultaneity. If you’re trying to coordinate events at in different reference frames or gravitational levels, it doesn’t make sense to go beyond a certain level of precision, unless you take relativity into account. And then doing it right is very complicated, way beyond the scope of time & clock library. So floating-point is the right model for time.

I’m not sure what the limit is on precision of the naive model of universal time for the surface of the earth, but this should be well-known in astrophysics.

I’ve been burnt by Rs POSIXct class (=double) by using them as a indices for some data structures. Not a bright idea to start off, but I’m convinced, that having integer types for dates/time and even periods would have made my life easier.

What is a specific example of a use case in which someone needs exact time periods, and is happy working with Int64 values, but will get inadvertently “burned” if Period supports Float64?

Float64 is likely to be a convenient format for math calculation in programming(eg. not easily overflow, and can be easily divided), but it is not a natural format to represent time(either date or period) to human.

A similar example is the binary/hexadecimal vs decimal format for general numbers. The only reason I can think of preferring decimal number in programming is the friendliness to human style.

Then for examples with similar reason, “please wait 0.0166667 minutes or wait 2.777778e-4 hours” is much obscure to average people than “please wait 1 second”. And even it is such a verbose format in following example, it is a natural to human format than just a single Float64 number.

julia> DateTime(2017, 9, 22)
2017-09-22T00:00:00

There are cases like “half a day” and “one and half an hour” that can be thought of float examples, but still just very limited use cases.

Pure mathematically, of course, I don’t think there is any disadvantages with Float64 number.

I have to admit that I can appreciate the accuracy we have thanks to the integer based time in Julia. But we have to be able to at least allow for a lossy conversion from a floating point <some time unit> to DateTime.

I am not sure I understand your argument here. Simply labelling something “natural” or “unnatural” contributes little to a technical discussion.

My understanding is that the issue in this topic is about representation, not printing/user interface. Currently, dates/times don’t print the representation directly either, ie 2017-01-01 is not printed as 736330 days since the epoch (which would indeed be obscure to most people).

The conclusion I get from this whole thread is there there is no convincing technical argument to restrict the representation to integers. I understand that some use cases are nicer with integers, and maybe those functions could restrict a type parameter to <: Integer or similar, but this does not mean that the type itself should enforce this restriction.

3 Likes

My understanding is that the issue in this topic is about representation, not printing/user interface.

Aren’t representation here highly coupled with the user interface(method signature, API)? Printing is not important. I think you were not talking about representation about the internal design without exposing to users if I understand correctly.

Apparently, with integer representation, the interfaces have to deal with multiple time units from nanoseconds to day/month/year.
And with Float64 representation, the interfaces are inclined to deal with a single time unit(could be a new one or one of above).
The later one does not reflect the traditional form of the practical world where people talk about time with different scales of unit.

Matlab uses Float64 “day numbers” Having used them quite a bit, I can say:

Their problem is that their precision is insufficient for the present time (+/- some years), but an overkill for historical times. Nobody needs millisecond precision for times around A.D. 0, but there is a lot of “contemporary” data being measured with higher precision (astronomy, astrophysics, and other mainly science stuff) than available in a Float64, that indicates 63641780541276 or so milliseconds from the birth of Christ.

Therefore, IF Float64 is used for timestamps, the epoch needs to be close to present, perhaps like 2020-01-01 00:00:00

What is the epoch of Matlab dates? Surely they don’t use A.D. 0 as their epoch?

They do, here the Matlab REPL:

>> datestr(0)                                                                                                                                         
                                                                                                                                                      
ans =

00-Jan-0000
                                                                                                                                                      
>> now                                                                                                                                                
                                                                                                                                                      
ans =

   7.3696e+05

>> now/(365)                                                                                                                                          
                                                                                                                                                      
ans =

   2.0191e+03

:open_mouth:

1 Like

It’s perfectly valid to think of a date as a position or angle, as it’s ultimately intended to be a measure of the Earth’s motion about the sun. The time of day is a measure of the rotation of the Earth. One can think of both as having the same units as angles (in fact, that’s literally where the units of minute, second, etc. came from). Clearly, it would be silly if we couldn’t convert a floating point angle to a measurement of angle that included integer representations of degrees, minutes, arcseconds, milliarcseconds, etc. To say that time should only be represented as a set of integers is to say that angles should be represented this way too.

Viewing time as a geophysical quantity, it’s clear that a floating point representation is natural, and a conversion to people-oriented things like months, days, hours, etc. is a nice thing to have.

Consider that many folks will use Julia to integrate something over time numerically, where the integration begins at some start date. Clearly, we’d like a convenient way to turn the (floating point) integration step times into dates without a lot of fuss. That is, I expect that lots of applications will have and will practically need floating point times.

It sounds to me like @yakir12 is doing the right thing to add a floating point conversion.

This really points to the difference between elapsed time (how many times my cesium source “ticked”) vs dates (how many times the earth went around the sun) and times of day (how many times the earth rotated). It sounds like maybe the integer-focused folks are more interested in the latter (dates and times of day), while the float-focused folks are talking more about the former (elapsed time).

Speaking of accuracy, it’s perhaps important to note that elapsed time cannot be turned into UTC dates in the future with a resolution of 1s beyond about the next 6 months, because we don’t know well enough how much the Earth will rotate. This is why we need leap seconds. That is to say, we do not know how much time will elapse between 15:04:23 of September 22nd in 2017 (UTC) with what will eventually be called 15:04:23 of September 22nd in 2018 (UTC). Also, the Julia astro package includes utilities for these types of calculations (the UT1-UTC offset calculated via published IERS data) – super cool!

While on the subject, happy equinox everyone. :slight_smile:

2 Likes

Oh no. That’s such an unfortunate choice. If they’d used the UNIX epoch it would have exactly the precision/range tradeoff you actually want.

1 Like