Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I disagree about "plus" in the JodaTime example. The month addition with corner cases did exactly what I expected because the whole library has been polished to "do what a normal human would do, most of the time". A normal human would not suspect Jan + 1 month, is any month other than Feb. However I suspect, not even reading the documentation, if it were fed 30 or 31 days, rather than '1 month', it would also do exactly that, and mostly give dates in March.


What is 2021-02-28 + 1 month? Is that 2021-03-28 or 2021-03-31? It is far from obvious what a normal human would do most of the time here.

The lack of associativity is still a problem. If 2021-02-28 + 1 month = 2021-03-28,then (2021-02-28 + 1 month) + 1 month = 2021-03-28 + 1 month = 2021-04-28. While if I ask what is 2021-02-28 + 2 months (given 2021-02-28 + 1 month = 2021-03-31), most people would say 2021-04-30.

While I am not entirely sold on using awkward/"honest" names by default, the author does raise a good point: sometimes concepts are inherently messy or full of important edge cases, and we shouldn't just brush that aside.

I recently ran into a similar problem. I am implementing a distributed lock (https://www.joyfulbikeshedding.com/blog/2021-05-19-robust-di...) -- like a mutex, but works across processes and machines. I try to mimic the language's standard Mutex API as much as possible.

A normal Mutex has a query method named "owned?" to check whether the calling thread owns the mutex. When I tried implementing this method for my distributed lock, it raised a question: owned according to who? Owned according to the local state that represents the lock, or according to the state that lives in the server? Because they can differ (e.g. due to bugs in other clients or because an admin manually messed with the state). So I opted for "honest names" here too and implemented two methods: "owned_according_to_local_state?" and "owned_according_to_server_state?"


The problem is that "What is one month after 2021-02-28?" doesn't have a single meaning in plain human language.

It could mean 30 days in the future. Or the same day of the week 4 weeks in the future (i.e., 28 days in the future). Or the day of the same cardinality in next month. Or any date in the next month. And those are all equally correct.

It's simply not a precise measure of time when spoken from one human to another human in plain language. Indeed, I think we inherently understand it to be an imprecise measure of time just as much as "tomorrow" doesn't mean "exactly 86,400 seconds from this moment". "Next week" doesn't necessarily mean "7 days from now", either. That's why computers don't typically use imprecise terms. They provide feedback and say "this will occur at this time and date".

You always have to check what the operators actually do and what the requirements actually mean when you're working with times and dates.


Plus/Increment/Add 1 month, does have one single meaning, it's that the outcome may be invalid that is the issue.

"2021-01-31".plus(unit="Month", size=1) => "2021-02-31"

But nobody really wants that, because it's not a valid date. So implicitly the library is deciding to return a valid date.

A library could be written to just provide invalid dates, and let the end user handle any errors. That library could also include an explicit validation method that takes a date and returns a valid one.

"2021-02-31".coerceToValid() => "2021-02-28" // Overflow == Max

"2021-02-31".coerceToValid(asDays=True) => "2021-03-03" // Overflow Carries (to the right)

In fact the library, that provides an ignorant response and no contract on validity would hold to the associative property, it just wouldn't be as ergonomic.


FWIW, I believe there are only two ways to build this library correctly: the way you described here (where intermediate date values are allowed to be invalid) and simply making "one month after 2021-01-31" throw an exception.


I agree those are the technically correct way to do it.

But if the reason people are wanting to use the library, is they want something to handle the complexity for them, coercing the data is good for simplication.

There is actually another option, that provides idempotent/associative consistency and implicit coercion to valid values.

This option, which discards some use cases (days beyond 28, when manipulating months), you coerce all values 29..31 to 28. This isn't even as technically correct, as the original option, but it removes the inconsistency and holds to the simplification contract to users.


> Plus/Increment/Add 1 month, does have one single meaning

No, it doesn't.

You walk in to the doctor's office on February 28. At the desk, you see another patient about to leave. They turn to the desk attendant and say, "I'll see you in a month for my follow-up."

What date is the other patient's next appointment? What if the date you walked in had been January 31?

Also, for what it's worth, in C#:

  DateTime x = new DateTime(2021, 1, 31);
  x.AddMonths(1); // Feb 28
  x.AddMonths(2); // March 31
  x.AddMonths(1).AddMonths(1); // March 28


I explained it pretty clearly what you get when you add a month on January 31st. You get February 31st.

A month is a discrete unit of measure. It is not decomposable into any number of days.

When you increment a month, you get YYYY - (MM+1). Any higher significance is maintained, but irrelevant to the operation. (This applies to the hypothetical statement in the doctor's office, the specific day is indeterminant, but can be assumed the same as current day next month.)

The fact that not all possible days exist is orthogonal to the singular meaning of the operation. It's obviously not greatly valuable to an end-user, but the method of addressing the ambiguity involves a second operation that ensures validity.

End-users want an method that does both the addition and coercion, but you can create consistency if you follow the simple path I laid out in GP.

I'll use your syntax but with the strictly correct definition of the operation.

  Datetime x = new DateTime(2021, 1, 31);
  x.AddMonths(1); // DateTime(2021, 2, 31)
  x.AddMonths(2); // DateTime(2021, 3, 31)
  x.AddMonths(1).AddMonths(1); //DateTime(2021, 3, 31)

  // Ensure Valid, using a coerce to clamp overflows
  DateTime(2021, 2, 31).EnsureValid(); // Feb 28
  DateTime(2021, 3, 31).EnsureValid(); // Mar 31
  DateTime(2021, 4, 31).EnsureValid(); // Apr 30


> The problem is that "What is one month after 2021-02-28?" doesn't have a single meaning in plain human language.

Thank you. That's exactly it. Crazy to see how many developers don't seem to grasp it here. I guess it's the "trap" that we are used to datetimes before the time when we became developers and we have to actually relearn this stuff to get the idea.


I think if you try and confuse folks with this, that is easy to do. But, this isn't special. What happens if you add a meter to a kilometer? For most measurements, you get a kilometer. We teach this with significant digits, but then typically ignore it and assume all sorts of conventions that are not spelled out.

If it is important in the domain you are working in, take extra care to understand the maths that you are built on. And don't be surprised to find special cases everywhere.


You seem to be stuck on: 'what is the value of 1 month' and thinking in terms of days, because that appears to be the precision of the left value.

Programmers exist in a world where things such as leap seconds matter. Normally if you have a timestamp that is just before a leap second, then add exactly a day's worth of seconds, you'd slide back a little in time. This might matter in another context, such as defining the limits of neighboring ranges properly. Also, who's to say the underlying precision is a second?

The intent of the library in question is to behave the way most people would. With imperfect buckets and idealized answers; yet also precision where someone makes the attempt to be specific.

None of the examples in the article use a more vague syntax, such as "0 days before the end of the month". They start with what a human might, a rounded but full date; then apply an interval. So a more clear contrived example might be.

  Jan 31 .plus(1 months) => Feb 28
  Jan 31 .plus(2 months) => Mar 31
  Jan 31 .plus(3 months) => Apr 30
  Jan 31 .plus(4 months) => May 31
  Jan 1 .plus(1 months) => Feb 1
  Jan 1 .plus(2 months) => Mar 1
  Jan 1 .plus(3 months) => Apr 1
  Jan 1 .plus(4 months) => May 1
Note how in the second half there are still variably sized months, but the result is what a human would want.


You’ve ignored your critique and answered only the more obvious anecdotes.

So I’ll repeat:

What’s Feb 28th + 1 month?

Does the human expect the last day of March? Or the 28th day of March?

The API doesn’t make that clear - I think you could reasonably argue for either.


If you're adding 1 month, you're working on the month's location, e.g. 2021 - (02) - 28.

When you increment the month, the result would be 2021-03-28.

The only time you'd modify the day, is if the day became invalid due to an overflow, during that increment. If, when, you overflow the days you'd set the value of days to the maximum in that month.

If I tell someone, I'll get to that in a month, they expect by this day in the next month, the next calendar page, not 30/31 days.


You're setting the same trap as those viral math problems people share on Facebook:

6÷2(1+2) = ?

We could argue about what the _right_ answer is to that equation, but I call it a trap because it's intentionally confusing and devoid of any context (or the ability to ask a follow up question). There isn't really a situation where you would see that equation and not know the intended way to interpret it... just like your question.

There are a couple of ways that context _could_ be provided though:

I'm writing an automated task that should run once per month, I don't necessarily care what day of the month it runs though since it just cleans up some temp files. If today happens to be Feb 28th, and I say run today, then every month after, I would expect it to run Feb 28th, March 28th, April 28th...

I'm writing an 'end of month' task that needs to run at the end of every month for some bookkeeping reason. If today happens to be Feb 28th, and I say run today, then every month after, I would expect it to run Feb 28th, March 31th, April 30th...

In both of these situations I would program accordingly. Computers don't understand context, that's the job of the human programming it.


The human expects March 28th. Ask any normal person and they’ll answer March 28th.


The article is about naming the method. The fact that dates are messy means a clean API is difficult/impossible.

I think plus() is a name that is good enough. I can't think of a better name that will help the user understand what will happen in the 2/28 + 1 month case. That's asking too much of a method name. That's what docs are for.


Perhaps .dateAfter(1 month) would be more appropriate? I sympathise with the author in finding the violation of associativity of “plus” a bit jarring.


I think that's the easier case—most people would expect Feb 28th + 1 month = Mar 28th. The tricker one is Jan 31 + 1 month, because there is no Feb 31st. I don't think there is a "correct" answer to that.


If I pay you Jan 31, Feb 28, March 31, April 30, etc., don't I pay you monthly? Shouldn't I be able to express this as a repeated addition of a month?

At any rate, these problems have been solved in finance, with proper date and schedule libraries.


No, you should be able to express it the way I mentioned in other examples: Base .plus( N months ) where N is whichever month after the reference you want.

.plus .plus .plus isn't correct because "x months" doesn't have a fixed size. You are NOT saying Base .plus(30 days), NOR are you saying Base .plus(4 weeks) ((which BTW, I'd expect to stay on the same weekday)). You're incrementing by an unstable value.


This doesn't work either as it wouldn't work correctly if started in a month with less than 31 days. To represent "the last day of the month" you really need to be subtracting one day from the first day of the next month, full stop: any solution working off of these weird rounding rules is going to be unreliable.


> Shouldn't I be able to express this as a repeated addition of a month?

I don’t think so, no—more likely, it should be Pattern(Month)… like how would you express “second Tuesday” as a series of additions?


To express this in a sane way you want to express "the last day of the month". You can't claim it is "just keep adding to the hire date one month times the current month minus the hire month" as if I hire you in February that won't work. You really need to be calculating "take the current time, replace the day with 1, add one month, subtract one day" to get the next pay date.


I did cover that:

""" None of the examples in the article use a more vague syntax, such as "0 days before the end of the month". """

As another reply points out, it's incrementing the Month set of buckets. I'll also extend with other results I expect:

  2020-12-31 .plus(1 months) => 2021-01-31
  2020-12-31 .plus(2 months) => 2021-02-28
  2020-12-31 .plus(3 months) => 2021-03-31
  2020-12-31 .plus(4 months) => 2021-04-30
A normal human has several options, and truncating to stay within the month makes the most sense to the most people most of the time. It's perfectly reasonable to take that step when resolving the indicated date to a representable value.

I'll go further: JodaTime probably isn't focused on Precision Date Calculations; it behaves very much the way I expect someone working with forms and fields, general CRUD enterprisy software stuff, would want auto-filled dates to work.


Maybe I'm just being thick (it's usually the case), but for the life of me I still don't know what your answer to the parent's question is, and I can't tell how your quoted part is supposed to answer it.


My 2 higher levels post post included an alternate phrasing of the test case they specified:

""" None of the examples in the article use a more vague syntax, such as "0 days before the end of the month". """

---

They asked:

""" What’s Feb 28th + 1 month?

Does the human expect the last day of March? Or the 28th day of March? """

---

It's implicit, the human only expects the month to change, because the input isn't a descriptive phrase "the end of the month" adjusted or not, it's a literal date. That's why my other test cases show the same behavior for the end of the month.


It should be the 28th and there should be a ceiling method.


I still think plus is fine in this case. And really, my expectation is, that anybody who thinks about adding a months, knows about their own intention. Because I suspect just adding the month is not what most people are actually doing, I guess in 90% of the cases most developers will add another step, rounding to the last (or first) day, or to the next same weekday, whatever.

The one thing nobody wants, is to add a month and land in the month one over.

So even if the semantic differs between libraries for adding a month, it actually doesn't mather that much. Because for all the other cases one could imagine, most people will add days or weeks, if staying on the same day matters.


I found the argument about lack of associativity convincing: it's very counterintuitive that date + 1 month + 1 month is different from date + 2 months


One way I suggested that we solve in a different domain was a small name change that really helped. To me "plus" is the _operator_ and "add" is the _operation_. So to me date.add(1 month).add(1 month) is actually different than date.add(2 month) because I can read that as taking two one-month steps which _may_ be different.


The unit of 'month' is not fixed. It doesn't even have a value until it's applied to a date.

(It is almost like a quantum state. It can be between 28 and 31 days, depending on what it's being applied to. But as soon as it's applied to an absolute date the ambiguity disappears).

If you expand out the short hand 2000-02-02 + (1 month forward from February) + (1 month forward from March), then we can see associativing is nonsensical.


If I tell someone on January 31st “call me a month from today” then we have a call on February 28th and I tell them “call me one month from today” I would expect them to call on March 28th.

In contrast if on January 31st I told them “call me two months from today” I’d expect them to call on March 31st.

It’s very intuitive.


Disagree.

Jan + 1 month = February, sure.

But, as soon as you add the day, it falls apart for me.

For most dates, if I add a month, in my mental model, the answer is the next month with the same date.

Jan 15 + 1 month = Feb 15, etc

But, at the edges, it gets odd quickly.

Jan 31 + 1 month = ??? Not sure, maybe Feb 28, maybe Feb 29, maybe Mar 2, maybe Mar 3. Depends on the year and who's asking me to solve the problem.

I would expect any reasonable software to fail gracefully when asked to solve this problem. And by fail gracefully, I mean ask for clarification. Or prevent me from asking silly questions in the first place.


The point of the article is that it doesn't have to fail gracefully.

Consider the API:

    data Date = Date { getYear :: Int, getMonth :: Int, getDay :: Int }
        deriving (Eq, Ord, Show)

    addDays :: Date -> Int -> Date
    addMonthsRounded :: Date -> Int -> Date
Someone who does

    d `addMonthsRounded` 3
immediately has a contextual clue that there might be something fishy going on, and has a string they can google to get to the docs to find out that this "Rounded" business is all about "hey, the code

    let y = (x `addMonthsRounded` 1) `addMonthsRounded` (-1)
      in y == x
might sometimes return False because it truncates if your day doesn't fit in the given month."


You expected 2021-03-31 + 1 month + 1 month to be different from 2021-03-31 + (1 month + 1 month)? I find this behavior understandable, but not apparent at first glance.

Also, FYI, this is not how GNU date works:

  $ date -d "Jan 28 next month" 
  Sun Feb 28 00:00:00 CST 2021
  $ date -d "Jan 29 next month" 
  Mon Mar 1 00:00:00 CST 2021
I could see confusion from this, depending on what libraries you are used to.


The inconsistency with GNU date is more visible from Feb:

  $ date -d "jan 31 next month"
  Wed Mar  3 00:00:00 PST 2021
  $ date -d "jan 30 next month"
  Tue Mar  2 00:00:00 PST 2021
  $ date -d "jan 1 next month"
  Mon Feb  1 00:00:00 PST 2021
  $ date -d "feb 28 next month"
  Sun Mar 28 00:00:00 PDT 2021
Edit: This was on my unconscious mind for a bit and I came up with an additional test case to confirm a suspicion I realized.

  $ date -d "2016-1-31 next month"
  Wed Mar  2 00:00:00 PST 2016
  date -d "2016-2-1 next month"
  Tue Mar  1 00:00:00 PST 2016
  date -d "2016-2-1 next year"
  Wed Feb  1 00:00:00 PST 2017
  $ date -d "2016-3-1 next year"
  Wed Mar  1 00:00:00 PST 2017
GNU date will add the duration of the CURRENT interval (ignoring already occurred deviations, like leap years) relative to the specified base date.

The oddity in behavior I observed above is adding the length of the month of Jan to dates in Jan. I suspect only a programmer would find that inference remotely correct.


I suspected this when I was playing with it, but good to have the confirmation. Thanks!


> A normal human would not suspect Jan + 1 month, is any month other than Feb.

Because humans intentionally reduce the precision of their computations to make them easier.

  Today = 2021-08-04

  Next year = 2022
  The exact month, day, hour are all unknown.

  Next month = 2021-09
  The exact day and hour are unknown.

  Tomorrow = 2021-08-05
  The hours and minutes are unknown.
If humans used the same level of precision as computers, they'd run into the same problems. Probable date of birth calculation is an example.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: