

When a+b+c does not equal c+b+a - meesterdude
http://ruru.name/entries/when-a-plus-b-plus-c-equals-c-plus-b-plus-a/

======
mattmalin
I also imagined floating point arithmetic problems when seeing the title. What
is really happening here is that a, b and c may themselves have different
values depending on the operation - as adding a month here isn't a fixed
number of days as month lengths differ.

The article states it perfectly at the end though: "When doing math that deals
with time, specifically different units of time, it can lead to unexpected
results if you are not careful with the ordering". I think this can generally
be extended to "when doing any math that deals with time, it can lead to
unexpected results if you are not careful with everything"!

~~~
JadeNB
> "When doing math that deals with time, specifically different units of time,
> it can lead to unexpected results if you are not careful with the ordering"

Not arguing with you, but with the poster: I think that a deeper lesson might
be the important realisation that, unlike 'day' or 'hour', 'month' is _not_ a
unit of time! (At least, not any more than 'moment' is. (I know, I know,
[https://en.wikipedia.org/wiki/Moment_(time)](https://en.wikipedia.org/wiki/Moment_\(time\)),
but you know what I mean.))

~~~
cies
> unlike 'day' or 'hour', 'month' is not a unit of time!

Not a 'fixed unit' of time. Where I define 'fixed' as being always of the same
size (like gram or meter).

My next thought is that day is also not fixed in case of daylight saving and
leap seconds. A day has many definitions: astronomical, 24 hours, calendar,
etc.

Maybe we can also see this problem with 'hour'; but in that case it would be
more contrived.

Bottom line: programming time is hard, be aware of that.

See this post for some common pitfalls:

[http://infiniteundo.com/post/25326999628/falsehoods-
programm...](http://infiniteundo.com/post/25326999628/falsehoods-programmers-
believe-about-time)

~~~
JadeNB
> Not a 'fixed unit' of time. Where I define 'fixed' as being always of the
> same size (like gram or meter).

I would regard that as part of what 'unit' means
([https://en.wikipedia.org/wiki/Units_of_measurement](https://en.wikipedia.org/wiki/Units_of_measurement)),
though I'm not sure that there's any universal agreement.

I think that it's fair to say that even, or perhaps especially, the most
obvious concepts, when sufficiently well understood, exhibit subtleties. (I
always tell students in my mathematics classes that the correct answer to
every question is "it depends", and, though it's somewhat facetious, I
honestly can't think of a mathematical statement so basic and obvious that a
sufficiently sophisticated mathematician couldn't correctly raise this
objection.)

With that said, while I agree that there may be different _colloquial_
definitions of 'day' and, as you mention, of 'hour', and even of 'second'
([https://en.wikipedia.org/wiki/Second#Other_current_definitio...](https://en.wikipedia.org/wiki/Second#Other_current_definitions)),
they also have unambiguous scientific definitions that _can_ be pinned down by
sufficiently precise reference; whereas 'month' simply does not.

EDIT: I should clarify that I'm not attempting by any of this to say that
programming with times isn't hard—it absolutely is. I just think that a large
part of it can be traced to confusion about what is _not_ a unit (which, as
observed by cies above
([https://news.ycombinator.com/item?id=9665366](https://news.ycombinator.com/item?id=9665366))
and DannoHung below
([https://news.ycombinator.com/item?id=9665472](https://news.ycombinator.com/item?id=9665472)),
can include such seemingly innocuous concepts as 'day'), and that it might be
profitable explicitly to state this unifying idea as one way to guide attempts
to program successfully with time.

~~~
DannoHung
As long as we're posting wikpedia links:
[http://en.wikipedia.org/wiki/Leap_second](http://en.wikipedia.org/wiki/Leap_second)

I wouldn't trust any time unit larger than seconds if I was concerned about my
units staying consistent. Hopefully time-keeping bodies don't decide we need
leap-millis at some point.

Which is only to say that this is a problem that can't be solved _simply_ by
discarding some concepts that you find problematic. There's a lot of shit
about timekeeping that's problematic. I'd advise tossing all the problematic
shit when trying to figure out some internal representation for the underlying
time that more or less always makes sense and then building out more human-
usable representations with the uglyness on top of it.

~~~
JadeNB
> Which is only to say that this is a problem that can't be solved simply by
> discarding some concepts that you find problematic.

> I'd advise tossing all the problematic shit when trying to figure out some
> internal representation for the underlying time that more or less always
> makes sense and then building out more human-usable representations with the
> uglyness on top of it.

I don't mean to be snarky, but these two sentences seem contradictory—you say
that the problem can't be solved by discarding problematic concepts, then
advise to discard problematic concepts. Are you making the point that, even if
internally we work with an un-problematic representation of time (if such a
thing exists!), at some point we have to convert to and fro? Certainly I agree
with that. Though I seem to have said it badly, that's really what I meant to
say above: trying to work directly with months (and, perhaps less
dramatically, with days), which are not units of time, allows the problematic
real-world notion of time to contaminate what should be the clean internal
representation of it.

~~~
DannoHung
I guess? I'm saying that you can't realistically avoid working with the
problematic units. Like, if you want to have a monthly payment system, at
_some_ point, you have to bake in the human concept of months into your
system, warts and all.

~~~
LukeShu
When my cell plan was with T-mobile, I was on a pre-paid plan. My bill was due
once a month, so I set up monthly automatic billing. Well, the system that
enabled or disabled my service depending on if I had paid considered that to
be "every 30 days". The system that handled the automatic payments considered
that to be the same day every month (IIRC, it charged my card on the 18th of
every month; when I set it up my bill was due on the 20th). A couple of 31-day
months later, and I lose service for a day because the automatic payments
didn't keep up with the bill being due.

------
rockdoe
Being able to add timedeltas of a month seems like a spectacularly bad idea.

It's a rails extension.

[http://stackoverflow.com/questions/2699902/get-next-
previous...](http://stackoverflow.com/questions/2699902/get-next-previous-
month-from-a-time-object/2699925#2699925)

~~~
zubspace
It's actually quite useful, because for us human beings thinking in months is
natural. Think about a user setting for some kind of web notification service:

How often do you want to be notified:

* Once per month

* Once each quarter of a year

* Twice a year

It's straightforward if you can add the number of months to the sign up date.
You simply need to define the edge conditions. Example in C#:

    
    
      The AddMonths method calculates the resulting month and year,
      taking into account leap years and the number of days in a month,
      then adjusts the day part of the resulting DateTime object. If the
      resulting day is not a valid day in the resulting month, the last
      valid day of the resulting month is used.
      For example, March 31st + 1 month = April 30th.
    

[https://msdn.microsoft.com/en-
us/library/system.datetime.add...](https://msdn.microsoft.com/en-
us/library/system.datetime.addmonths%28v=vs.110%29.aspx)

I think ruby does the same.

~~~
jerf
In typed languages, I'd suggest a case could be made that once you add a
"month" to a time, you should get a different type out that should either
support a grossly reduced set of operations, or one that would require formal
unpacking to produce a "real" datetime that would once again support normal
operations. Months and years are dangerous things. (Days technically aren't
constant either, but there's a lot of applications like billing where a leap
second won't bother you at all if your month & year code just deal with them.)

~~~
agentultra
Add in timezones and you have a whole barrel full of monkeys to deal with.
Especially if you have two integrated systems with databases in different
timezone formats. Trying to teach computers how to interpret time the way we
do intuitively is a peanut butter sandwich experiment. Tedious, frustrating,
error prone, and messy.

------
quesera
Was hoping for something more surprising. Dates and times are fertile ground.

Rephrased, this problem is that "unit quantity + container quantity yields
different results when container quantity varies".

On Feb 26th:

    
    
      > Time.now + 1.month + 5.days => Mar 31st
      > Time.now + 5.days + 1.month => Apr 3rd
    

...

IIRC, #month is a Rails-ism, not part of Ruby.

------
ColinWright
I thought this was going to be another example of floating point arithmetic
"gotchas" similar to this:

    
    
      #!/usr/bin/python
    
      x = 1.
      a = 1.
      while a+x > a:
        x = x/2
    
      print x
    
      b = x
      c = x
    
      print a+b+c == c+b+a
      print (a+b)+c == a+(b+c)
    

Always interesting when the arithmetic system you use is non-commutative and
non-associative.

*Edited to add the non-associative bit)

~~~
kuschku
Which is the case for all vector × vector, vector × matrix and matrix × vector
multiplications, for example.

(Taking math at university right now as part of studying compsci, non-
commutative rings are annoying)

~~~
ColinWright
Yes, but at least they are all still associative. (Original comment edited to
deal with that).

And non-commutivity is important. You can put on your socks then shoes, but
get a different result if you put on your shoes then socks. In general you get
different results in rotations if you change the order you do them. Similarly
permutations.

Non-commutivity is important, it's just that most people don't expect addition
to exhibit it. (And by definition, addition is still commutative in non-
commutative rings)

------
tim333
tl;dr

Time.now + 5.days + 1.month == Time.now + 1.month + 5.days

Isn't always true because months have different lengths

~~~
bazzargh
That expression wouldn't always be true even if months _did_ have the same
lengths. Two calls to the clock in the same expression are not guaranteed to
return the same time.

In the article, that problem is papered over by using string equality. That
makes the problem much less likely (because it only occurs at rollovers of
seconds) but it will still happen. Tests like this one - has a subscription
expired? - should use inequalities, since that's all that's needed and it
won't contain heisenbugs like the above.

Edited to add - just to demonstrate:

    
    
       irb(main):045:0> i=0; loop { i=i+1; raise "#{i}" unless Time.now.to_s == Time.now.to_s; }
       RuntimeError: 61473

------
noobie
I had a physics teacher that used to tell us:

    
    
        vache + mouton = la honte
    

which translates to

    
    
        cow + sheep = shame
    

point being, if you add up things that don't match, i.e. do not have the same
unit/dimension, you should expect some weird mistakes.

------
thebouv
Date math, calendars and all that is lumped in there together: my most hated
of programming tasks.

Second most hated: anything to do with maps.

Seems like every new client or job I take on, I'm fixing or building a
calendar or map.

------
amelius
Operator overloading sucks. Especially when the expected properties such as
commutativity do not hold.

~~~
DannoHung
Is it really overloading that's the problem here? Even if you used method
names to do this, you'd still have to deal with the fact that the operation
being performed is non-commutative.

I don't know that there is a way of expressing such a restriction even if you
were using dependent types in such a way as to prevent shooting yourself in
the foot here. Possibly you could not implement the month or year span
elements altogether and only allow addition of days or seconds?

~~~
erdeszt
But nested method calls don't imply commutativity while addition does. So the
code is misleading.

~~~
DannoHung
I don't follow. How do nested method calls fail to imply commutativity?

For example, using psuedo-Haskell notation:

    
    
        (+):: Int -> Int -> Int
    
        ; ncc short for non commutative combination
        ncc:: Time -> Time -> Time
    
        1 + 2 + 3
    
        ncc 03:00 (ncc 10:00 02:00)

~~~
erdeszt
It could be just me but I would expect something that looks like an addition
to be commutative but not arbitrary function calls. But I see what you mean
that the types are not much of a help here.

------
lucb1e
So if I sign up in February I get 28 days to enjoy the service; if I sign up
in March I get 31? That seems like the fundamental problem here.

------
cies
It all stems from that `+` in Ruby is not something mathematical. It is merely
a method that needs to be defined on the expression that it follows:

    
    
        Time.now.+(1.month).+(5.days)
    

Or:

    
    
        Time.now.send(:"+", 1.month).send(:"+", 5.days)
    

`Time.now` evaluates to an object of the class `Time`, which implements the
method `+`.

If you are looking for a language basis itself more on "laws" then on "message
passing", I found this is Haskell.

In Haskell `+` is a function that need to be implemented for all types in the
`Num` type-class. `+` can only add two `Num`s of the same type (and evaluates
to a value of that specific type), thus explicit conversion is needed. See the
docs here:

[http://hackage.haskell.org/package/base-4.8.0.0/docs/Prelude...](http://hackage.haskell.org/package/base-4.8.0.0/docs/Prelude.html#v:-43-)

------
nathanvanfleet
This was a bit misleading. It's about date handling, which is a complex
scenario well outside 1 + 1.

~~~
Moru
I was directly thinking dates when I saw the headline :-)

I too have had my share of problems with date handling. One year is not always
52 weeks, A month is never 30 days, most days have 24 hours (but not all of
them) and so on...

Not to mention the famous D/M/Y, M/D/Y, Y-M-D ordering problem...

~~~
mauricemir
Me to I recall fixing a bug with a major uk company BACS transfer tape where
the accounts receivable system was instant that you could have more than 365
days in a year.

Which meant that we where unable to collect any Direct debits for 3/4 of year!

------
jbert
A related one which I found is A + B - B != A

In pretty much the same case, where A==some time and B == 1 month

This came up in calculations for monthly subscriptions, trying to reach back
in time to work out pro-rata charges for changing subscription details part
way through a month.

------
rmah
Hah, you think normal date math is bad, try dealing with the various calendars
used in finance. US 30/360, Euro 30/360, Actual/365, Actual/360, etc, etc.
That stuff will make you tear your hair out.

------
wz1000
It really depends on how you expect '+' to behave.

When seeing '+', I immediately think of a semigroup operation. I would expect
it to be associative but not necessarily commutative.

~~~
monochromatic
Is it even associative? I don't use Ruby, but it seems like this might not
even be defined:

    
    
        5.days + 1.month

~~~
IceyEC
> 5.days + 1.month => 1 month and 5 days

~~~
monochromatic
Well, right... but "1 month and 5 days" isn't particularly well-defined on its
own, unless you have some starting date that you're measuring from.

~~~
IceyEC
Agreed, but technically it _is_ defined.

------
anirudt
On a first glance at the link, my guess was string concatenation.

------
meesterdude
Maybe worth mentioning: this was a bug that existed for ~2 years before I
started on the project. It's easy for this kinda subtlety to slip past review
and debugging.

~~~
jobigoud
That's interesting because if I understand the bug correctly, it should have
failed as soon as you performed the test less than 5 days before the end of
the current month. (Except in December and July for which the next month has
the same number of days in it.).

~~~
meesterdude
if i recall, it was a bug that would come and go from our tests depending on
when you ran them. Sometimes the math worked out, other times it was off. We
knew the code worked, but couldn't figure out why the specs kept failing for
it randomly.

~~~
bazzargh
eek. Time tests should be using something like
[https://github.com/travisjeffery/timecop](https://github.com/travisjeffery/timecop)
for repeatability. If you do mean to allow random starting values like that,
you should look into property-based testing
[http://www.troikatech.com/blog/2014/04/02/property-based-
tes...](http://www.troikatech.com/blog/2014/04/02/property-based-testing-in-
ruby) \- test hundreds of possible values at a time, instead of just the one
time you ran the tests at.

------
dsugarman
Tldr; a month is not a constant amount of days, it depends on the month of the
year

~~~
M2Ys4U
And the year the month is in.

------
unmagnet
Definitely happens with natural language judgementalistically.

------
broken_symlink
I was thinking quaternions when I saw the title.

~~~
ColinWright
But adding quaternions is both associative and commutative.

------
mauricemir
yes your point is? Date math is not the same as arithmetic

~~~
meesterdude
and arithmetic is not the same thing as sending + to different objects in ruby
:)

