Skip to content

Commit

Permalink
Normative: Fix date difference for end-of-month edge cases
Browse files Browse the repository at this point in the history
This adjusts the difference algorithm for Gregorian-year dates so that
when an intermediate date occurs past the end of a month, it is not
shifted to the end of that month.

Previously, in some edge cases where taking the difference in months or
years would return a number of months and zero days, we now return one
month less and 28, 29, or 30 days instead.

Example: 1970-01-29 until 1971-02-28, largestUnit years
Old result: 1 year, 1 month
New result: 1 year, 30 days

Note that largestUnit weeks and largestUnit days, the latter of which is
the default, are not affected.

TODO: Make the algorithm in the reference code faster, to help show how
it could be implemented.

Closes: #2535
  • Loading branch information
ptomato committed Jan 27, 2024
1 parent 67c89bc commit 2da6047
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 83 deletions.
127 changes: 46 additions & 81 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3763,95 +3763,60 @@ export function RejectDuration(y, mon, w, d, h, min, s, ms, µs, ns) {
}
}

function ISODateSurpasses(sign, y1, m1, d1, y2, m2, d2) {
const cmp = CompareISODate(y1, m1, d1, y2, m2, d2);
return sign * cmp === 1;
}

export function DifferenceISODate(y1, m1, d1, y2, m2, d2, largestUnit = 'days') {
const sign = -CompareISODate(y1, m1, d1, y2, m2, d2);
if (sign === 0) return { years: 0, months: 0, weeks: 0, days: 0 };

switch (largestUnit) {
case 'year':
case 'month': {
const start = { year: y1, month: m1, day: d1 };
const end = { year: y2, month: m2, day: d2 };

let years = end.year - start.year;
let mid = AddISODate(y1, m1, d1, years, 0, 0, 0, 'constrain');
let midSign = -CompareISODate(mid.year, mid.month, mid.day, y2, m2, d2);
if (midSign === 0) {
return largestUnit === 'year'
? { years, months: 0, weeks: 0, days: 0 }
: { years: 0, months: years * 12, weeks: 0, days: 0 };
}
let months = end.month - start.month;
if (midSign !== sign) {
years -= sign;
months += sign * 12;
}
mid = AddISODate(y1, m1, d1, years, months, 0, 0, 'constrain');
midSign = -CompareISODate(mid.year, mid.month, mid.day, y2, m2, d2);
if (midSign === 0) {
return largestUnit === 'year'
? { years, months, weeks: 0, days: 0 }
: { years: 0, months: months + years * 12, weeks: 0, days: 0 };
}
if (midSign !== sign) {
// The end date is later in the month than mid date (or earlier for
// negative durations). Back up one month.
months -= sign;
mid = AddISODate(y1, m1, d1, years, months, 0, 0, 'constrain');
}

let days = 0;
// If we get here, months and years are correct (no overflow), and `mid`
// is within the range from `start` to `end`. To count the days between
// `mid` and `end`, there are 3 cases:
// 1) same month: use simple subtraction
// 2) end is previous month from intermediate (negative duration)
// 3) end is next month from intermediate (positive duration)
if (mid.month === end.month) {
// 1) same month: use simple subtraction
days = end.day - mid.day;
} else if (sign < 0) {
// 2) end is previous month from intermediate (negative duration)
// Example: intermediate: Feb 1, end: Jan 30, DaysInMonth = 31, days = -2
days = -mid.day - (ISODaysInMonth(end.year, end.month) - end.day);
} else {
// 3) end is next month from intermediate (positive duration)
// Example: intermediate: Jan 29, end: Feb 1, DaysInMonth = 31, days = 3
days = end.day + (ISODaysInMonth(mid.year, mid.month) - mid.day);
}
let years = 0;
if (largestUnit === 'year') {
let candidateYears = sign;
while (!ISODateSurpasses(sign, y1 + candidateYears, m1, d1, y2, m2, d2)) {
years = candidateYears;
candidateYears += sign;
}
}

if (largestUnit === 'month') {
months += years * 12;
years = 0;
}
return { years, months, weeks: 0, days };
let months = 0;
let intermediate;
if (largestUnit === 'year' || largestUnit === 'month') {
let candidateMonths = sign;
intermediate = BalanceISOYearMonth(y1 + years, m1 + candidateMonths);
while (!ISODateSurpasses(sign, intermediate.year, intermediate.month, d1, y2, m2, d2)) {
months = candidateMonths;
candidateMonths += sign;
intermediate = BalanceISOYearMonth(intermediate.year, intermediate.month + sign);
}
case 'week':
case 'day': {
let larger, smaller;
if (CompareISODate(y1, m1, d1, y2, m2, d2) < 0) {
smaller = { year: y1, month: m1, day: d1 };
larger = { year: y2, month: m2, day: d2 };
} else {
smaller = { year: y2, month: m2, day: d2 };
larger = { year: y1, month: m1, day: d1 };
}
let days = DayOfYear(larger.year, larger.month, larger.day) - DayOfYear(smaller.year, smaller.month, smaller.day);
for (let year = smaller.year; year < larger.year; ++year) {
days += LeapYear(year) ? 366 : 365;
}
let weeks = 0;
if (largestUnit === 'week') {
weeks = MathFloor(days / 7);
days %= 7;
}
weeks *= sign;
days *= sign;
return { years: 0, months: 0, weeks, days };
}

intermediate = BalanceISOYearMonth(y1 + years, m1 + months);
const constrained = ConstrainISODate(intermediate.year, intermediate.month, d1);

let weeks = 0;
if (largestUnit === 'week') {
let candidateWeeks = sign;
intermediate = BalanceISODate(constrained.year, constrained.month, constrained.day + 7 * candidateWeeks);
while (!ISODateSurpasses(sign, intermediate.year, intermediate.month, intermediate.day, y2, m2, d2)) {
weeks = candidateWeeks;
candidateWeeks += sign;
intermediate = BalanceISODate(intermediate.year, intermediate.month, intermediate.day + 7 * sign);
}
default:
throw new Error('assert not reached');
}

let days = 0;
let candidateDays = sign;
intermediate = BalanceISODate(constrained.year, constrained.month, constrained.day + 7 * weeks + candidateDays);
while (!ISODateSurpasses(sign, intermediate.year, intermediate.month, intermediate.day, y2, m2, d2)) {
days = candidateDays;
candidateDays += sign;
intermediate = BalanceISODate(intermediate.year, intermediate.month, intermediate.day + sign);
}

return { years, months, weeks, days };
}

export function DifferenceTime(h1, min1, s1, ms1, µs1, ns1, h2, min2, s2, ms2, µs2, ns2) {
Expand Down
3 changes: 1 addition & 2 deletions spec/plaindate.html
Original file line number Diff line number Diff line change
Expand Up @@ -799,8 +799,7 @@ <h1>
</dd>
</dl>
<emu-alg>
1. Let _constrained_ be ! RegulateISODate(_y1_, _m1_, _d1_, *"constrain"*).
1. Let _comparison_ be CompareISODate(_constrained_.[[Year]], _constrained_.[[Month]], _constrained_.[[Day]], _y2_, _m2_, _d2_).
1. Let _comparison_ be CompareISODate(_y1_, _m1_, _d1_, _y2_, _m2_, _d2_).
1. If _sign_ &times; _comparison_ is 1, return *true*.
1. Return *false*.
</emu-alg>
Expand Down

0 comments on commit 2da6047

Please sign in to comment.