`Type(val)` vs `convert(Type, val)` (Converting more than 365 days to months and years)

I was wondering how many years were between two events. So I define the timepoints, and find their difference:

julia> using Dates

julia> d0 = Date(1980, 6, 17)
1980-06-17

julia> d1 = Date(2015, 1, 4)
2015-01-04

julia> Δd = d1-d0
12619 days

I now wonder how many years and months this is, and try to convert it to a date:

julia> Date(Δd)
ERROR: ArgumentError: Day: 12619 out of range (1:31)
Stacktrace:
 [1] Date(y::Int64, m::Int64, d::Int64)
   @ Dates ~/.julia/juliaup/julia-1.10.0-beta3+0.x64.linux.gnu/share/julia/stdlib/v1.10/Dates/src/types.jl:257
 [2] Date
   @ Dates ~/.julia/juliaup/julia-1.10.0-beta3+0.x64.linux.gnu/share/julia/stdlib/v1.10/Dates/src/types.jl:305 [inlined]
 [3] Date(::Day)
   @ Dates ~/.julia/juliaup/julia-1.10.0-beta3+0.x64.linux.gnu/share/julia/stdlib/v1.10/Dates/src/types.jl:349
 [4] top-level scope
   @ REPL[33]:1

Okay, so I can not convert directly to date (this turns out to be false). So I google, and find this stackoverflow question. To convert more than 60 seconds to hours and minutes (similar problem), it reccomends an approach along the lines of

julia> Time(0) + Second(4000)
01:06:40

So I give that a shot:

julia> Δd + Date(0)
0034-07-20

Okay, so that works. But what happens here is just that Day(12619) is converted to Date:

julia> Δd + Date(0) |> typeof
Date

But that is what I attempted initially, with Date(Δd)! Or, that is what I thought. Apparently the two are different:

julia> convert(Date, Δd)
0035-07-20

I can see from using @code_lowered that Type(val) produces quite different code from convert(Type, val). Notably, the convert version produces significantly less code. My mental model was Type(val) essentially lowered to convert(Type, val), or the other way around. The examples with dates and times are the only cases where I have seen the effects from the difference.

An example of why I thought that they lowered to essentially the same is how the expressions below have similar stack traces and identical errors:

julia> Int(1.1)
ERROR: InexactError: Int64(1.1)
Stacktrace:
 [1] Int64(x::Float64)
   @ Base ./float.jl:909
 [2] top-level scope
   @ REPL[53]:1

julia> convert(Int, 1.1)
ERROR: InexactError: Int64(1.1)
Stacktrace:
 [1] Int64
   @ Base ./float.jl:909 [inlined]
 [2] convert(::Type{Int64}, x::Float64)
   @ Base ./number.jl:7
 [3] top-level scope
   @ REPL[52]:1

Could someone give me some interpretation as to why the two are different? Should they generally not be used interchangeably?

IMHO, this shouldn’t be allowed. A Date corresponds to a specific point in time (in the proleptic Gregorian calendar) while Day(12619) refers to any period of 12619 days, regardless of when they took place (if they even refer to an event that happened in reality at all).

Why should a period of 12619 days be the same as any specific point in time (in this case some day in the year 35 AD)? To me, this seems wrong, and == seems to agree:

julia> Δd == convert(Date, Δd)
false

(although it’s just hitting the === fallback, so this might be an oversight). I would argue that convert should error here.

As for converting Δd to a number of years and months: Since not all years/months have the same number of days, this is not well-defined, so canonicalize returns a number of weeks and days:

julia> canonicalize(Δd)
1802 weeks, 5 days

Maybe there should be a CompoundPeriod(d1, d0) method for returning the difference of two dates in Years, months, etc.? Or is there already something like this?

1 Like

EDIT: Ah, nevermind, I didn’t see @sostock already raised the same points :sweat_smile:

I think it’s usually the latter (convert(T, val) will in most cases call the consructor T at some point, since it is supposed to return something of type T – that’s also what your example with Int(1.1) shows), but I’m sure there are cases where the constructor tries to convert something. It seems to happen for example when trying to construct a Year out of your time difference:

julia> Year(Δd)
ERROR: MethodError: Cannot `convert` an object of type Day to an object of type Year
Closest candidates are:
  convert(::Type{T}, ::Dates.CompoundPeriod) where T<:Period at ~/Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/periods.jl:363
  convert(::Type{Year}, ::Month) at ~/Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/periods.jl:455
  convert(::Type{Year}, ::Quarter) at ~/Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/periods.jl:464
  ...
Stacktrace:
 [1] Year(p::Day)
   @ Dates ~/Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Dates/src/periods.jl:420
 [2] top-level scope
   @ REPL[12]:1

This doesn’t really answer your questions, but is converting the difference between two dates again to a date really the right thing to do to get to the duration expressed in years? There is e.g. the canonicalize function, but it doesn’t seem to go beyond weeks by default…

julia> canonicalize(Δd)
1802 weeks, 5 days
2 Likes

Well how would it go beyond weeks? Month and Year are both variable length, so you can’t in general say e.g. how many months 61 days are or how many years are in 1802 weeks.

3 Likes

My bad, I forgot people are still using the Gregorian calendar :roll_eyes: :stuck_out_tongue_winking_eye:

(I couldn’t find an “official” English version of this, but this is the reference: Der Migokalender It’s a pretty interesting idea, but I doubt that it will stick…)

1 Like

This is true, but I still find it very useful to be able to express the difference between two timepoints in years. Especially when they are years appart (see how natural language uses the unit of years?).

oh gosh, yet another one on the Dates pile

1 Like

So being able to do convert(Date, Day(1000)) is bad, because it is inherently an ill-defined operation. It makes the mistake of conflating a point and a period/duration, and also it is ill-defined due to irregularities in the duration of months and years. Making this operation error would require Julia 2.0 though, right?

But if we ever make conversion of a <:Dates.Period to a <:Dates.TimeType throw an error, it would be awesome if the error-text was along the lines of

That operation is ill-defined, because it is converting a period of time to a point in time. Concider finding a point in time away from a reference-point, as in Dates(0) + Day(1000).

As opposed to some obscure method error.

1 Like

no, but it might require Dates 2.0

1 Like

To the general point about convert vs. the constructor, the difference is described in the manual: Conversion and Promotion · The Julia Language

3 Likes

Oh wow, that is spot on. Thanks!

If the Dates supported floats, it would look like Year(Day(1234)) == Year(3.38) (:
It doesn’t though, and you may find external packages useful and convenient for that:

julia> using Dates, DateFormats

julia> Day(1234) /ₜ Year
3.3785772466238186
1 Like

Here is a solution to this problem that calls Python packages.

At the time I looked at this, I could not find equivalent code in Julia.

1 Like

If you just want the number of years, you can use Unitful, because it can convert Dates.Day and other periods to Unitful quantities:

julia> using Dates, Unitful

julia> Δd = Day(12619)
12619 days

julia> uconvert(u"yr", Δd) # a `yr` equals 365.25 days
50476//1461 yr

julia> float(ans)
34.548939082819984 yr
1 Like

Among other things, the Python code takes into account leap years.