3

I am reading Phobos docs. Sometimes I can't understand the logic of some methods.

Date roll

Adds the given number of years or months to this Date. A negative number will subtract. The difference between rolling and adding is that rolling does not affect larger units.

Maybe is Phobos is not good considered, maybe I do not understand where it's can be helpful.

If I am adding to 2013-07-01 for example 200 days I am expecting to get 2014 year, but not 2013.

Could anybody explain the logic?

Dmitry Bubnenkov
  • 9,415
  • 19
  • 85
  • 145
Suliman
  • 1,469
  • 3
  • 13
  • 19
  • I don't understand the question... If you want to add to a date, why don't you use the "add" method instead of rolling? – cym13 Aug 02 '15 at 14:25
  • I am trying to understand logic. For example why I can't "add" days? – Suliman Aug 02 '15 at 14:32

2 Answers2

5

roll is also present in Java.... the only time I remember ever using it in either language though was implementing a little date picker widget. There's separate fields for each thing: month, day, year, and you want the user to be able to spin through days without it incrementing month, since they are separate fields. (Suppose it is set to the 30th and they want to select the 1st. The quickest way might be to just hit the up arrow and let it roll over.)

There might be other uses, I just don't remember them right now. Even when I was implementing ical support a while ago (sadly, proprietary), I never used the roll method, but there might be potential for it there. ical is a standard for recurring events on calendars and there's some tricky things in there, and a few different ways you might implement it.

In the comment, you also ask why you can't add days. The reason is that adding days is pretty trivial and covered with the opBinary:

datetime += 5.days; // works

That does NOT work for months though, because months have a variable duration. That's where the add method comes in to help: suppose it is January 30 and you add one month. You might just add 31 days, since January has 31 days in it. That would land you on March 2 (I think, doing this in my head)... but Jan 30 + 1 month might also need to be Feb 28. Or maybe Feb 29. Suppose you are paying a monthly bill due at the end of the month, the number of days between those due dates will change.

The add method handles this and gives you the option to fall on the last day or to spill over into the next month.

Though, I'd kinda argue days don't have a static length either.... consider the DST transition. (Or leap seconds, but handling them would be nuts). Maybe we can ask JMD.

Adam D. Ruppe
  • 25,382
  • 4
  • 41
  • 60
  • 1
    Because of the rollover, `add!"years"` and `add!"months"` does take the time zone into account, but `+=` does not. It only deals with the time in UTC. So, yes, if you hit a DST transition and you were expecting it to take the time zone into account, then that might throw you off slightly, but it's adding the actual amount of time that's passed (in hnsecs), not explicitly incrementing the day or the hour or whatnot. `roll` on the other hand does take the time zone into account, because it's specifically operating on the units. And yes, it's there for spin widgets for date pickers. – Jonathan M Davis Aug 02 '15 at 20:57
  • 1
    If you specifically wanted `+=` to be done in the local time zone, you'd have to convert the `SysTime` to `DateTime`, add to it, and then convert back, which is awkward, but really, `SysTime` was designed to avoid having DST issues screw with time operations, and you really don't want to take DST into account like that normally. I supposed that `add` could be extended to add units smaller than months and take the time zone into account, but I expect that that would lead to a lot of accidental misuse for folks who really wanted `+=` but didn't understand. – Jonathan M Davis Aug 02 '15 at 21:02
5

So, the short answer is that Adam is right, and roll is there specifically for handling date pickers and similar use cases, and add is there specifically to handle months and years, because Duration can't (since months and years don't convert to hecto-nanoseconds without knowing a starting point). But I'll take a stab at it a more in depth answer in case that clarifies things.

DateTime, Date, and TimeOfDay were designed for calendar-based operations. They have no relation to the system clock, they have no concept of time zone, and they hold the parts of the date/time as separate units (year, month, day, hour, etc.) internally. So, when you set the hour field on a DateTime, you are literally just adjusting one of DateTime's member variables. And when you add a Duration to them, that has to be split up to add to each unit individually and thus doing something like adding a Duration of 7 hours could increment more than just the hour field. e.g.

auto dt = DateTime(2015, 8, 2, 20, 0, 0);
dt += hours(7);
assert(dt == DateTime(2015, 8, 3, 3, 0, 0));

And since Duration holds its value internally in hecto-nanoseconds, you're not necessarily adding to a specific unit anyway. i.e. It's not like += hours(7) is specifically adding to the hour field. It's adding some number of hecto-nanoseconds to the whole DateTime and has to figure out how that affects each of the units rather than it affecting a specific unit. e.g.

auto dt = DateTime(2015, 8, 2, 20, 0, 0);
dt += hours(7) + minutes(5) + seconds(22);
assert(dt == DateTime(2015, 8, 3, 3, 5, 22));

roll on the other hand is specifically for adding to a specific unit without affecting any of the others. As Adam correctly determined, its primary use case was for spin controls and the like in date picker dialogs, where you end up adjusting a particular time unit by some amount, and you don't want to affect the other units, just the one that you're tweaking. So, if you add 7 hours, you don't end up with the day incrementing, just the hours.

auto dt = DateTime(2015, 8, 2, 20, 0, 0);
dt.roll!"hours"(7);
assert(dt == DateTime(2015, 8, 2, 3, 0, 0));

Now, things do get a bit funnier with months and years, because they don't have a fixed length. And that's why Duration does not support them. You can't say that x hecto-nanoseconds is equivalent to y months or z years. But while Duration doesn't make sense with regards to months or years, we still want to be able to add months and years to a Date or DateTime. So, we have add!"years" and add!"months" to handle that, and it allows you to control some of the oddities like whether adding 1 month to January 31st should end up at the end of February or the beginning of March (since there is no February 31st).

None of this takes any kind of time zone into account. It's purely dealing with calendar time. add and roll are purely calendar-based operations, and += as implemented on DateTime, Date, and TimeOfDay is a calendar-based operation. It is taking the years, months, days, etc. into account when it does its calculations (and can't do otherwise in their case, because the units are split out among their member variables).

And then we have SysTime. It's intended to represent the system's time (which does mean taking the time zone into account). However, the only way to avoid DST-related issues and the like is to always keep the time in UTC. So, SysTime always keeps its time internally in UTC (in hecto-nanoseconds) and uses a TimeZone object (LocalTime by default) to convert that to a particular time zone only when it needs to. This avoids all kinds of annoying time-related bugs. And anyone operating on the system's time should be using SysTime rather than DateTime, Date, or TimeOfDay.

So, when you add to a SysTime using +=, you're really just adding the hecto-nanoseconds from the Duration to the hecto-nanoseconds that SysTime uses internally. No calendar operations occur whatsoever (and if they did, you'd end up with lots of DST-related bugs). SysTime is not inherently calendar-based at all. In order to do any calendar operations, a SysTime's hecto-nanoseconds that it holds internally must be converted to whatever that represents in terms of a date on the Gregorian calendar (using the TimeZone object to take the time zone into account).

However, in order to be user-friendly, SysTime provides many of the calendar-based properties and functions that DateTime does (e.g. the year, month, day, hour, etc. properties and the add and roll functions). To support that, it basically ends up converting to Date or DateTime internally to do them, which means taking the time zone into account and which potentially means introducing DST bugs if they're not used correctly. And in some cases, it's annoyingly inefficient. For example, if you have a SysTime variable called st

auto year = st.year;
auto month = st.month;
auto day = st.day;

converts st to Date three times, whereas it would have been more efficient to just convert to a Date explicitly and get the units from there. So, that user-friendliness is a bit of a double-edged sword.

Also, it raises the question that Adam raised about how adding and rolling deal with DST. roll and add on SysTime both deal with a Date or DateTime internally, and that takes DST into account, because converting from SysTime to Date or DateTime converts it from UTC to whatever its TimeZone object represents. So, roll and add operate on SysTime exactly how they'd operate on DateTime - because they're converting to DateTime and back again.

But as I already said, += on SysTime just adds the Duration to its UTC time. So, there is no way on a SysTime to add units smaller than months to it and take DST into account. You'd have explicitly convert to a DateTime, use += on that, and then convert it back to a SysTime, which is a bit ugly, but it's not something that you'd normally want. I suppose that we could make SysTime.add work with units smaller than months, but then you'd just end up with folks using that and ending up with DST-related bugs when they should have been using += but didn't understand the difference.

Really, at this point, I question that adding those calendar functions to SysTime was wise and that it's actually ultimately harming usability rather than helping as was intended. But I doubt that we could change it at this point - or more accurately, while we could change it, I question that deprecating those functions on SysTime and forcing folks to change their code would be worth the pain that it would cause. If I could redo it though, I'd seriously consider not putting any calendar-based operations on SysTime.

In any case, hopefully that wall of text was enlightening enough to be worth your time.

Community
  • 1
  • 1
Jonathan M Davis
  • 37,181
  • 17
  • 72
  • 102
  • @Adam D. Ruppe @JonathanMDavis is there any rational problems to implement all like C# do: `DateTime nextMonth = date.AddDays(1).AddMonths(1).AddDays(-1);` I mean is there any hopes that in future I would able to write in D in such way? – Dmitry Bubnenkov Aug 06 '15 at 07:23
  • @user1432751 I don't know what you're even trying to do there. I am not particularly familiar with C#'s DateTime, and your example is not clear. I've looked at C#'s DateTime before but not recently, I don't think that I've ever used it, since I've barely ever used C#. D's SysTime and DateTime's `add` and `+=` return their `this` by `ref`, so you can chain those calls if you want to. You can also chain `+` calls, though that would result in a new object rather than mutating an existing one. There are no `const` variants of `add` which return a new object if that's what you're looking for. – Jonathan M Davis Aug 06 '15 at 08:03
  • yes I mean chain calls, but thanks for detail answer. But still D realization looks for me too complex. Maybe problem in every day examples in docs... – Dmitry Bubnenkov Aug 06 '15 at 08:21