Summary
Range
applied to non-integers can be reduced to an expression involving an integral range -- i.e. Range[min, max, di]
is notionally equivalent to
min + di * Range[0, Floor[(max - min) / di]]
A problem arises when the min
and max
are dates and di
is a duration. In min + di * ...
, the duration di * ...
is evaluated from the base date min
and entails exact month lengths. On the contrary, (max - min) / di
resolves to a ratio of two durations with no base date for reference. In this case, di
is interpreted using a "generic" month length of 365/12 days. It is the mismatch of these two interpretations of di
that gives rise to an unexpected number of entries in the result.
Furthermore, when a duration of n months is added to a date, the computation attempts to preserve the day number of the initial date if possible. This is why the generated dates do not always fall at month-end even though the initial date does.
Work-around
DateRange
operates upon dates as dates-proper (as @swish notes in a comment). It respects actual month lengths instead of generic ones and will produce the expected results:
DateRange[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 1}], "Month"]
(* DateObject[{2016, 2, 1}], ..., DateObject[{2016, 7, 1}] *)
It can also pin the generated dates to month-end, if desired:
DateRange[DateObject[{2015, 12, 31}], DateObject[{2016, 6, 30}], "EndOfMonth"]
(* {DateObject[{2015, 12, 31}], ..., DateObject[{2016, 5, 31}], ...} *)
Analysis
A trace of the original expression is very lengthy, but we can inspect certain substeps to see what is going on:
We can see a subexpression which is performing the interval division (max - min) / di
, which ultimately resolves to:
Quantity[151, "Day"] * Quantity[1, Power["Months", -1]]
(* 1812 / 365 *)
Simple arithmetic operations like this can be applied to date objects, but it entails a conversion to a common unit. The trace does not reveal the choice made by Mathematica, but all choices lead to the same result:
UnitConvert[Quantity[151, "Day"], #]& /@ {"Days", "Months", "Years"}
(* {Quantity[151, "Days"], Quantity[1812/365, "Months"], Quantity[151/365, "Years"]} *)
UnitConvert[Quantity[1, "Month"], #]& /@ {"Days", "Months", "Years"}
(* {Quantity[365/12, "Days"], Quantity[1, "Months"], Quantity[1/12, "Years"]} *)
% / %%
(* {365/1812, 365/1812, 365/1812} *)
So when Mathematica attempts to convert these interval sizes into a common unit, it is using a generic duration for a month, i.e. 365/12 days. It is this generic month length that ultimately causes the behaviour under discussion.
The floor of this interval division value is 4, and indeed the trace contains another subexpression which is generating a list of five dates (i.e. initial value plus four more), namely:
{ DateObject[{2016, 2, 1}]
, DateObject[{2016, 2, 1}] + Quantity[1, "Month"]
, DateObject[{2016, 2, 1}] + 2 * Quantity[1, "Month"]
, DateObject[{2016, 2, 1}] + 3 * Quantity[1, "Month"]
, DateObject[{2016, 2, 1}] + 4 * Quantity[1, "Month"]
}
Once again, the trace shows evidence that a generic algorithm is being used to create the range.
Now that we have seen how Range
generates its result, we should be able to get a list with six entries just by adding enough days to the interval so that the floor-plus-one is six instead of five:
{##, #2 - #1, N[(#2 - #1) / Quantity[1, "Month"]]}&[
DateObject[{2016, 2, 1}], DateObject[{2016, 7, #}]]& /@
{1, 2, 3} // Grid
The rollover point is July 3:
Range[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 2}], Quantity[1, "Months"]]
(* {DateObject[{2016, 2, 1}], ..., DateObject[{2016, 6, 1}] *)
Range[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 3}], Quantity[1, "Months"]]
(* {DateObject[{2016, 2, 1}], ..., DateObject[{2016, 7, 1}] *)
DateRange
does not suffer from these problems because it uses an algorithm that is specifically designed to work upon (and only upon) date objects:
DateRange[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 2}], Quantity[1, "Months"]]
(* {DateObject[{2016, 2, 1}], ..., DateObject[{2016, 7, 1}] *)
DateRange[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 3}], Quantity[1, "Months"]]
(* {DateObject[{2016, 2, 1}], ..., DateObject[{2016, 7, 1}] *)
DateRange[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 31}], Quantity[1, "Months"]]
(* {DateObject[{2016, 2, 1}], ..., DateObject[{2016, 7, 1}] *)
DateRange[DateObject[{2016, 2, 1}], DateObject[{2016, 8, 1}], Quantity[1, "Months"]]
(* {DateObject[{2016, 2, 1}], ..., DateObject[{2016, 8, 1}] *)
Month Ends
The final two expressions in the question show that adding a duration of one or more months to a month-end date does not always yield a month-end result. More directly:
DateObject[{2015, 3, 31}] + Quantity[2, "Months"]
(* DateObject[{2015, 5, 31}] *)
yet:
DateObject[{2015, 4, 30}] + Quantity[1, "Months"]
(* DateObject[{2015, 5, 30}] *)
In fact, such additions attempt to preserve the day number of the initial date, only changing it if the day does not exist in the month:
DateObject[{2015, 9, 30}] + Quantity[1, "Month"] * Range[0, 11]
Notice how all dates fall on the 30th, except for February which only has 29 days.
DateRange
can be used to force all dates to lie at month-end:
DateRange[DateObject[{2015, 9, 30}], DateObject[{2016, 8, 31}], "EndOfMonth"]
Design Choice?
This behaviour appears to be a design choice made by WRI. Had Range
been written to use exact date arithmetic instead of generic period sizes, there would be no need for DateRange
to exist. But this would also mean that we would lose the ability, for example, to expressly choose between preserving the day number or pinning to month-end when adding durations to dates.
The Range
documentation is virtually silent about being able to operate upon dates. It states that "the arguments to Range need not be integers" and an example shows that Quantity
objects are acceptable. It may very well be that the ability to apply Range
to dates is merely a side-effect of its ability to operate upon Quantity
objects. The design consequences in the context of dates might not have been explicitly considered.
DateRange
is more appropriate here. $\endgroup$DateRange[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 1}], "Month"]
is not as readable asRange[DateObject[{2016, 2, 1}], DateObject[{2016, 7, 1}], Quantity[1, "Month"]]
? Not only are the parameters near identical but you can make them identical by usingQuantity[1, "Month"]
inDateRange
. It is the exact same parameter list; with the added benefit thatDateRange
returns the expected list of dates. $\endgroup$DateRange
with more parameters. I should reword to say "less orthogonal" rather than less readable, in that ifRange
worked as intended,DateRange
- a more restricted function - could be deprecated. $\endgroup$Range
does work as intended and so doesDateRange
. $\endgroup$