[Pitch] Extending Date with methods to get specific new dates

Operations on DateComponents, like adding one day or one month, are missing. Moreover, DateComponents specifies a point-in-time, it contains also fields for time. This is more details than wanted, which means DateComponents is not the same as "day without time".

@sveinhal
I had a similar surprise in my first trial to add one day.

let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd"
let d1 = fmt.date(from: "2024-12-06")!
let cal = Calendar(identifier: .gregorian)
let d2 = cal.date(byAdding: .day, value: 1, to: d1)!
print(d2)
print(fmt.string(from: d2))

The output is:

2024-12-06 23:00:00 +0000
2024-12-07

My TZ is +1. I didn't specify any time and I didn't ask for any conversion to/from my locate. The problem is that one must be careful with time conversions when doing date calculations with Date.

They are deliberately missing because they would be traps. Imagine if DateComponents.nextYear existed. What would this print?

let components = DateComponents(year: 2024, month: 2, day: 29)
print(components.nextYear) // There is no 29th of February, 2025
1 Like

Exactly, this means DateComponents does not cover the needs of "date without time". It is not only a type which can represent a "date without time", it is mostly the operations on this type that are needed.

To cover this need, DateComponents could be extended to give a correct result in your example (which needs also a calendar as parameter), or another type could be added for this purpose.

What is the "correct" result of adding 1 month to January 29th? Would it be...

  • February 28th (30 days later)?
  • March 1 (31 days later)?
  • February 29 (day "number" stays the same, but month increases by one)?
    • This is only possible on leap years, as there's for example no February 29th, 2025. Would this return nil on non-leap years?

All of the above are distinct operations one might want to perform on dates, and all three can be interpreted as computing the "next month".

4 Likes

It is a matter of convention, but this question is orthogonal to which type can handle this operation.

To my knowledge, the behavior of adding one month in Swift's Date, as well as in other languages, is: increase the month by 1 (and shift to next year if needed), then truncate the day if needed. Since the operation asks for a month, month gets priority, i.e., it is preserved during truncation.

  • 2024-01-29 plus 1 month = 2024-02-29
  • 2025-01-29 plus 1 month = 2025-02-28
  • (2025-01-29 plus 1 month) minus 1 month = 2025-01-28
  • (2025-01-29 plus 1 month) plus 1 month != 2025-01-29 plus 2 months

This behavior is reasonable when the calculation is considered as a date calculation (in which day/month/year are objects). Addition of objects does not have some of the nice properties of numeric addition. It looks strange when it is considered as a time calculation. Most people think of day/month/year as dates, not as time.

To step through the last day of each month, the update would look like:

d = d.add(month: 1).lastDay
1 Like

I don't think that behavior is reasonable at all. Anyone trying to advance the date by one month would expect the resulting date to be uniquely referenced:

  • January 6th, 2025 → February 6th, 2025
  • January 7th, 2025 → February 7th, 2025
  • January 8th, 2025 → February 8th, 2025

It would be very surprising to discover that this isn't true at all at the end of the month, and "a month from this date" returns the same day for 4 days in a row:

  • January 28th, 2025 → February 28th, 2025
  • January 29th, 2025 → February 28th, 2025
  • January 30th, 2025 → February 28th, 2025
  • January 31st, 2025 → February 28th, 2025

Some operations would work as expected most of the time, like adding and then substracting months:

var date = DateOnly(year: 2025, month: 1, day: 6) // January 6th
date = date.addMonths(2) // <- March 6th
date = date.subtractMonths(2) // <- January 6th

Even at the end of some months:

var date = DateOnly(year: 2025, month: 1, day: 31) // January 31st
date = date.addMonths(2) // <- March 31st
date = date.subtractMonths(2) // <- January 31st

Only to break down for some dates in truly bizarre ways:

var date = DateOnly(year: 2025, month: 1, day: 31) // January 31st
date = date.addMonths(1) // <- February 28th?
date = date.subtractMonths(1) // <- January 28th????

I doubt people would answer with the same day 4 days in a row when asked the specific question "what's the date one month after January 28th/29th/30th/31st?".


I'm genuinely surprised that you'd find this behavior useful. I didn't even think of this as an option in my previous post.

What's the use case for which you need to advance only the "month" part of a date, while keeping the "day" part of the month the same, except at the end of some months, where the "day" part of the month can be changed to match the last day of whatever the target month happens to be?

1 Like

This is how date calculations are done in personal finance apps, I guess also in other apps which deal with dates without time.

They look strange because you expect the properties of numeric addition. These properties hold for time calculations, because time is a linear continuum, they don't hold for dates, because calendars are full of irregularities. The "correct" way to think about dates is that a date is a discrete state (object), and an operation is a jump from one state to another. An operation may be confusingly called "add", but it is not reversible as the usual addition is. It could have another name if this helps.

If you want this kind of date calculus, switching to time and back does not help. If you perform the operation in time the result is different (what in 1 month in time?). If you perform in time something equivalent to a date operation, then switching to time only adds complication. You have to deal with calendar irregularities at the level of day anyway, switching to time doesn't eliminate these irregularities. Actually you face them twice, once when you convert to time, and once when you convert back to date.

See for example:
https://docs.wxwidgets.org/3.2.6/classwx_date_span.html

It's perfectly reasonable in many, many applications.

Any system where dates are more of a abstract thing, like your birthday, rather than a specific moment in time, like a timestamp in a database. Financial systems and loyalty/reward systems are the ones I have the most experience in, where this happens all over the place, frequently with additional constraints like sticking to working days.

But this is well into business logic territory, with lots of possible interpretations for what is correct, and so belongs in domain specific packages or be written by the devs who need it.

That said, I'd very much like a standard type that represents a date without any time or timezone components, and some basic support for less ambiguous operations within a given calendar.

1 Like

Sorry, some clarification: I’m not saying performing the operation is unreasonable, but rather that defaulting to that behavior as the default for performing operations on a “date without time” type is.

This is an example of a “date without time”, but not an example of doing maths using “dates without time”.

Adding a month to a birthday date would be an example of such an operation, but then I’m highly skeptical that returning a date that may be anywhere between 28 to 31 days after the birthday is unambiguously the desired behavior.

Let’s imagine you’re writing a journaling app that works similarly to one of these trending Hobonichi journals, and shows —for each day— what you were doing 1/2/3/4 years ago. This is squarely into the “date without time” territory, and looks like a prime example for using one of the proposed methods:

date.subtract(.year, 1)

But what does the notebook show for Feb 29th? Well, the notebook shows just the leap years, not a duplicated entry for non-leap year’s 28th of February.

A slight variation of the example above would be showing the entry for a month ago. What would a month ago mean in this example? Wouldn’t users find jarring that sometimes the same day is shown for 4 days in a row (March 28/29/30/31 showing the entry for Feb 28th), or that some past days are skipped?

This seems to be a consequence of a “month” being the typical billing period, rather than an intrinsic characteristic of how humans think about dates.

A type that defaults to the Gregorian calendar only, for recent dates only, and with date operations clamped to force alignment with billing periods, seems to be describing a BusinessDate type instead of a “date without time” type.

4 Likes

The calendar logic is already implemented in Calendar's date(byAdding:value:to:wrappingComponents:), it is nothing new.

This Pitch does not try to re-invent calendars or to revise them. The topic is what interface and what types give access to this (already existing) logic.

I raised also the topic of better fit between internal representation and underlying concepts, and also of better efficiency in calculations, which are out of the scope of this Pitch.

This thread is a perfect encapsulation of the fallacies and misconceptions around date and time programming. It also highlights how inaccurate our language is when dealing with dates and times.

Names are made up

Underlying the proposals and ideas tossed around this thread is the fundamental misconception that there are algorithms that can be used to manipulate “wall dates” (words and numbers that you might find written on a calendar hanging on the wall of your home). This is false. All names and numbers used with calendars are entirely made up. They can change. They have superficial patterns but are not required to follow those patterns. The fundamental problem is not the code nor operating system nor ICU version
 the problem is humans. The names we give to segments of time are imagined up by humans, and humans change their mind. All the time. With short notice.

We cannot assume any linearity to dates and times, because of political influences. December 29th is usually followed by December 30th, but wasn’t if you lived in Samoa or Tokelau in 2011. That was a political decision that you can never account for ahead of time. This was a change to the timezone itself, which affected the names that country gave to their perception of their moments in time. So while Daylight Saving Time (a series of rules which affect the names a calendar gives to moments in time) typically do not extend more than an hour or so forward or backward in time, the underlying premise that “timezones themselves affect the name, and timezones are regulated by political bodies” means that date offsetting can never be a wholly algorithmic operation.

Names are ranges

Leading up to the next great flaw in this thread is the idea that “a wall calendar value” is a single concrete thing. This is also false. The true unit of time is an instantaneous, geometrically invisible point called “Planck time”. We measure Planck time using atomic clocks to come up with the SI Base Unit of a “second”, which is not Âč/₈₆₄₀₀th of a day.

Names that we give to human-perceivable time are therefore representative of ranges of time. A “calendrical second” represents all the possible geometrically small moments that happened in that one tick of a second hand. Even nanoseconds or femtoseconds are themselves ranges of Planck time.

So, it is convenient to point to a day on a calendar and say “that is one, isolated thing”, but it is fundamentally inaccurate: calendars break up time into ranges.

Kinds of Named Values

Therefore, when we consider something like “June 2nd, 2014”, there are multiple conflicting meanings for this, and this has also been very evident in this thread. There are two main flavors of calendar values: fixed ones and floating ones, and they have been conflated in this thread to great confusion. A “fixed” value is a one that corresponds to one-and-only-one range of “instants”. An example of this would be something like “June 2nd, 2014 in the Pacific time zone”. That will always represent the unix timestamp range of 1401692400.0 ..< 1401778800.0.

A “floating” value, however, can represent many possible ranges of values. “June 2nd, 2014” is a floating value, because it does not have a time zone. In Samoa (Pacific/Apia), “June 2nd, 2014” would represent the unix timestamp range of 1401620400.0 ..< 1401706800.0. However, a few miles away in American Samoa (Pacific/Pago_Pago), it would be 1401706800.0 ..< 1401793200.0. These ranges do not overlap. A “floating” value cannot be fixed (or anchored) to any single moment in time.

So when we talk about birthdays, it’s important to remember that this is a floating value. One observes the birthday in whatever timezone you happen to find yourself in on the day of the year that happens to have the same month-and-day names as the day on which you were born. You don’t follow necessarily follow the timezone in which you were born.

(And we're not even going to touch the concept of perceived time, which is the idea that someone awake at 12:03 AM on a Saturday will still likely consider it as part of Friday simply because they haven't slept yet)

The Real Answer

The real answer to this is that, since all perceptions and names of times are subject to humanity’s whims and political machinations, you can never reliably come up with a way to algorithmically manipulate dates. You must always use a Calendar. The Calendar type is how we encode the rules we’re aware of regarding time’s names. It relies on a list of exceptions and tweaks that get updated over-the-air by Apple and a small group of very devoted enthusiasts, and even that is incomplete. The list of rules and exceptions gets updated multiple times a year, changing both future names of time and past names, as our understanding of history evolves.

So again, we should not adopt this idea in Foundation, because it is already implemented correctly. We have Date to represent our base “moment in time” concept (despite having a name which we all know is a bad one), and we have DateComponents to represent the various portions of the name for that moment. We have Calendar to move back and forth between the two. We have TimeZone to encode the list of rules and exceptions for how Calendar comes up with those names, and we have Locale to encapsulate the rules for “rendering” those names into human-readable text, done via one of the various formatting APIs.

Could the whole system be better? Absolutely; that’s why I implemented by own Time library; it makes these concepts of fixed-versus-floating values part of the API, so you must always be aware of exactly what you need. But it’s also the kind of domain-level need that not every Swift process needs, and therefore should not be part of Foundation. Foundation provides the building blocks, as it should.

32 Likes

(If this discussion is going to continue it's perhaps worth noting that it would likely be more germane over in Foundation than here in Pitches.)

1 Like

Moved it.

4 Likes

This thread inspired me to open-source Dated, a package we’ve developed to manage floating dates. At its core, Dated introduces a LocalDate type, which combines a date with a timezone for use in the source of truth. Additionally, it provides a suite of floating date types. These include CalendarDate (a wall calendar date with second-level precision), as well as more specialized types like Day, Week, Month, and Year. We persist these types in caches that are invalidated when the user's preferred calendar changes.

I’m curious to hear what everyone thinks about this approach. There seem to be a lot of people in this thread who know a lot more about dates than I do.

@davedelong
Yes there are misconceptions around date and time, and yes calendars are not stable. Nevertheless, we try to make applications for humans, who expect to see dates as they know them. We don't try to predict the next political announcement about calendars.

Could you give your answer to my question above:
What is the next date after 2024-12-02?

Is it a valid question? Is it out of the scope of Foundation? What code would you use to calculate it? Existing functionality in Date and Calendar or a custom library? If I don't want to import any package for this calculation, could you recommend a solution?

[In case your answer is to use Calendar's date(byAdding:to:wrappingComponents:): do I need to bother about time and timezone? If I can give an immediate mental answer to this question, why I cannot do the same with computer calculations? Is the mental answer wrong, because I did not go into the trouble to think about the exact time?]

Regarding "domain-specific": everyone knows how to find the next date. School children also know. Domain-specific means you need an expert to get the correct answer. There is nothing specific in wall calendar dates. It is standard knowledge and it should belong to a standard library.

I'm not Dave, but:

The question is underspecified.

To begin with, like others have noted, 2024-12-02 has to be given context in a specific calendar to have any meaning: both the Gregorian calendar and the Hebrew calendar, for instance, have a date corresponding to a "day 2 of month 12 of year 2024", except in the Hebrew calendar, this date is thousands of years in our past, while in the Gregorian calendar, it's last week.

Following that, you have to specify what "next date" means. Do you mean:

  1. If I look at a calendar on my wall right now and find today's date on it, what number year, month, and day will my calendar show for the next box?
  2. If I stand in place and look at my phone until the same time tomorrow, what date will it say it is? (And if I stick my head out the window and ask someone what day it is, what will they say?)
  3. Something else?

Finding the "next date" has a specific meaning in context that isn't universally applicable.

The answer to this question will define the answer to your following questions.

Most people need the answer to (2), not (1), for which the answers are:

  1. It is in scope for Foundation
  2. date(byAdding:to:wrappingComponents:) is one way to find the answer to this question, yes
  3. Yes, you do need to bother with time and timezone, because the specific way you decide to fine the "next date" depends on those, given things like DST, time zone changes, locales, etc.

No, your mental answer to your own question is valid. The point is that you're fundamentally asking a different question from other people.

Not true. If I ask a school child "I'll be in New York on 2025-03-08 at 11:00 PM. If I wait 24 hours, (a day is 24 hours long, right?) what time will it be?", and I can bet very good money they'll get the answer wrong.

We teach children all sorts of simplifications to complicated models that we later have to unteach them. The simplified model can still be very useful, but part of the learning experience is learning when the model applies, and when you have to use the more complex model.

For almost all questions about date and time, the simplified model is not applicable and is very liable to give subtly incorrect answers. It depends on application whether or not it matters, and how, but it's fundamentally dangerous to offer attractive, easy-to-use tools that make the model appear more applicable than it really is.

7 Likes

Just ask a child "What is the next date after 2024-12-02?". Underspecified. Do not change it and do not give more clarifications. Would you still bet money that the answer will be anything else than "2024-12-03"?

If you add time, it is a totally different question. It is not about dates anymore.

Given that the only child available to me is my 21-month old, I would say "Yes" :smile:

But, less facetiously: if you asked a child in Samoa on 2011-12-29 what the date would be tomorrow and they said 2011-12-30, they would be wrong because the following date was 2011-12-31.


My point is, if you're asking "what (year, month, day) does the Gregorian calendar by definition specify comes after (year, month, day)" then there is an answer that can be given by that definition itself — but this is rarely a useful question, or answer, and is not what most people are asking. It's useful if you're, for instance, laying out and printing physical calendars; but if you were to ask someone "hey, what's tomorrow's date?", the answer does depend on the time and day you're asking, yes.

5 Likes

I mean, if you ask a vague question you can get a vaguely correct answer. Rarely do we want vaguely correct answers though.

2 Likes

None of this pro-timeless-date stuff makes any sense to me. There is no world in which dates-with-no-times have any importance.

Yes, it’s true and I freely admit that if I go to sleep on the night of 2024-12-2, I will believe it to be 2024-12-3 next morning. So what? Ignoring any possibility of geopolitical upheavals changing the calendar while I was sleeping, dates have no meaning whatsoever unless they are at least minimally linked to points in time. When you wake up on 2024-12-3, and you ask me what I think the date is, if I reply that it’s 2024-6-27, how can you call me wrong? You can’t. How do you know you aren’t wrong about December? You don’t.


 Unless we have some sort of system of shared time instants to refer to.

This isn’t mitigated by the “It’s just a label” theory presented upthread. If I get a bank statement that lists my November transactions separately from my December transactions separately from my January transactions, then, yes, I don’t really care whether the last November transaction really took place at the beginning of December, in my view of the calendar. But what if the statement tells me that every cash withdrawal I made in 2024 took place on June 13 (equivalently: is labeled 2024-6-13)? Do I care? Yes, I'd think the bank made a horrible accounting mistake. Are all those dates right or are they wrong? Don’t tell me it doesn’t matter, that it’s "just a label".

There is no understanding of dates without points in time to refer them to. There is no comparison of dates without equatability based on points in time, even in contexts where the points in time aren’t used for anything other than equatability. There is no meaning to dates that cannot transmit information between people, and there is no information transfer without equatability.

The actual premise of this thread is not to ask for “dates without times” or “dates without equatability”, but to ask for “dates with equatability defined reliably but very loosely”, such as “dates that are the same when there is some vaguely plausible relationship to points in time that could differ by ±24 hours or so”.

In fact, such “dates with equatability defined very loosely” are a thing in real life. That’s what’s motivating this entire thread. We don’t need to know the exact conversions to live much of our lives properly. We just need to know that the loose conversions are near enough for practical purposes.

This stops being good enough when we start writing code. For reasons — not the least that the code writer doesn’t have a time-instant-independent understanding of the user’s personal date-intuition — loose equatability in code is simply going to produce perceived incorrect results, to a greater or lesser degree. If you get this wrong, people just aren’t going to buy your app.

When it comes down to it, I don’t want a “simple” "timeless-date + 1” function in any system libraries, because it’s not useful for anything I can think of, and it’s worse than useless for things I do think of.

I do want proper point-in-time-based date functions in system libraries, because time/date conversions are hard to get right, and I really want an expert to write correct implementations so that I don’t have to.

1 Like