This is a proposal to change the behavior of the DateLike.prototype.with() methods which accept a month argument (e.g. OffsetDateTime.prototype.with(arg: DateTimeLike)).
Currently, the spec does not appear to specify what happens in the following scenerio:
const date = new CivilDate(2010,10,31);
date.with({month: 2})
Here, the month associated with the new CivilDate, February, only has 28 days in it but the existing CivilDate has 31. How is this scenerio handled?
Using the existing javascript date object, this operation results in:
const date = new Date(2019,9,31)
date.setMonth(1) // "2019/3/3"
_the browser sets the month to February 1st plus 31 days -> March 3rd_
I argue that this behavior is surprising and (almost) never desirable. The one use case (I've seen) for the existing Date behavior is to perform addition/subtraction on a date. However, in the temporal API we have .plus() and .minus() for that.
I propose that, when setting the month value, if the old day value is greater than the number of days in the new month, the day value is changed to be the last day of the new month (which, I argue, is a more intuitive/desired result than the existing javascript Date object's behavior).
.with({month: 2}) on it, the new CivilDate returned would represent 2010/2/28 (or on a leap year such as 2012 it would be 2012/2/29)When calling new CivilDate(2010, 2, 28).with({month: 10}), the result would be 2010/10/28.
If a developer moving from 2010/2/28 to a new month wished to ensure that they moved to the last day of the new month, they'd need to manually do that as an extra step.
For example:
let date = new CivilDate(2010, 2, 28)
date = date.with({month: 10, day: 1}).plus({month: 1}).minus({day: 1})
In general, if you go to the trouble of setting the month to a specific value (e.g. February), you want the returned DateLike object to reflect the specified month.
Alternatively, I could see an argument for throwing an error if you attempt to create an invalid DateLike object (e.g. new CivilDate(2019, 10, 31).with({month: 2})). However, I think users will generally want to set the day to the last day of the new month. With this understanding, having this be the behavior makes sense.
Actually, the more I think about this the more I think throwing an error makes the most sense. Even though I believe users in this scenerio will generally want to transform the date to the be the last day of the new month, forcing people to be explicit will ensure that there are no overlooked surprises and probably prevent a few bugs.
A developer could always make a function to handle things:
function setMonth(date: DateLike, month: number) {
// separately, I realize `DateLike#lengthOfMonth` doesn't exist, but it should be added
const day = date.with({month, day: 1}).lengthOfMonth;
return date.with({month, day});
}
The answer is the same as for plus() and minus():
Beginning with the largest unit going to the smallest do:
Example: Date,fromString('2019-10-31').with({ month: 2 }) == '2019-02-28'
Example: Date.from('1976-12-31').with({ year: 2019, month:2, day: 15 }) == '2019-02-15'
This is already reflected to be correct in the current polyfill. I'm in the process of putting this into the specification as well.
I don't think we should be performing hidden arithmetic alterations in with, and _especially_ not intermediate ones (your step-6 logic above would have Date.fromString("2016-02-29").with({ year: 2017, month: 4 }) result in 2017-04-28 rather than 2017-04-29 because there was no 2017-02-29 leap day).
I changed my stance on this issue (see edit above) and agree with @gibson042. I think with() should throw an error if it would result in an invalid date-time. Hidden arithmetic will be surprising to most, and will, I think, inevitably lead to some bugs. Conversely, forcing the developer to be explicit in this area is, at worst, a minor inconvenience at times.
Agreed, and it was just a matter of bad explanation.
const dt = Temporal.Date.fromString('2019-01-31');
dt.with({ month: 2 }); // 2019-02-28
What I was illustrating was the algorithm for addition / subtraction. My Bad
With does not do arithmetic. The algorithm for with is roughly:
Which also means you can get the last day of any month by doing dt.with{ day: 32 });
Most helpful comment
I don't think we should be performing hidden arithmetic alterations in
with, and _especially_ not intermediate ones (your step-6 logic above would haveDate.fromString("2016-02-29").with({ year: 2017, month: 4 })result in 2017-04-28 rather than 2017-04-29 because there was no 2017-02-29 leap day).