Commit 0a22ef7e34b for woocommerce
commit 0a22ef7e34b1ad92b8594907d005a4f909cfdcbe
Author: Mayur Dhavan <118877752+mayur-dhavan@users.noreply.github.com>
Date: Thu Nov 5 13:56:44 2026 +0530
Fix `containsLeapYear` for date-only strings in timezones west of UTC (#70211)
* Fix containsLeapYear for date-only strings in timezones west of UTC
Date-only ISO 8601 strings (YYYY, YYYY-MM, YYYY-MM-DD) parse as UTC midnight per ECMA-262, so getFullYear() returned the previous year west of UTC for January 1 dates. Use the written calendar year for date-only strings and keep the existing behavior for date-time strings, Date objects, and timestamps. Refs #70210.
* Add timezone regression tests for containsLeapYear
Cover date-only strings under a simulated UTC-5 timezone so the tests fail without the fix in any ambient timezone, plus Date-object/timestamp input parity. Refs #70210.
* Add changelog entry for containsLeapYear timezone fix
Significance: patch, type: fix. Refs #70210.
* Replace date-only regex with split and digit checks
Behavior-equivalent: split on '-', component length checks, and ASCII digit checks identify the same YYYY / YYYY-MM / YYYY-MM-DD forms. Addresses review feedback. Refs #70210.
* Simplify date-only year extraction in containsLeapYear
* Simplify date-only year extraction in containsLeapYear
diff --git a/packages/js/date/changelog/fix-containsleapyear-timezone b/packages/js/date/changelog/fix-containsleapyear-timezone
new file mode 100644
index 00000000000..d2511d5dee1
--- /dev/null
+++ b/packages/js/date/changelog/fix-containsleapyear-timezone
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix `containsLeapYear` returning the wrong answer for date-only strings (e.g. `2024-01-01`) in timezones west of UTC by using the written calendar year instead of the locally parsed one. (#70210)
diff --git a/packages/js/date/src/index.ts b/packages/js/date/src/index.ts
index af9716d363b..4887d985a66 100644
--- a/packages/js/date/src/index.ts
+++ b/packages/js/date/src/index.ts
@@ -1393,6 +1393,25 @@ export function isLeapYear( year: number ) {
return ( year % 4 === 0 && year % 100 !== 0 ) || year % 400 === 0;
}
+/**
+ * Get the calendar year of a date string, or NaN if the date is invalid.
+ *
+ * Date-only ISO strings (YYYY, YYYY-MM, YYYY-MM-DD) are parsed as UTC
+ * midnight, so getFullYear() returns the previous year west of UTC on Jan 1.
+ * For those, use the year as written.
+ *
+ * @param {string} date Date string.
+ * @return {number} The year.
+ */
+function getYear( date: string ) {
+ const parsed = new Date( date );
+ if ( isNaN( parsed.getTime() ) ) {
+ return NaN;
+ }
+ const dateOnly = /^(\d{4})(?:-\d{2}(?:-\d{2})?)?$/.exec( date );
+ return dateOnly ? Number( dateOnly[ 1 ] ) : parsed.getFullYear();
+}
+
/**
* Checks whether a date range contains leap year.
*
@@ -1401,9 +1420,8 @@ export function isLeapYear( year: number ) {
* @return {boolean} True if date range contains a leap year
*/
export function containsLeapYear( startDate: string, endDate: string ) {
- // Parse the input dates to get the years
- const startYear = new Date( startDate ).getFullYear();
- const endYear = new Date( endDate ).getFullYear();
+ const startYear = getYear( startDate );
+ const endYear = getYear( endDate );
if ( ! isNaN( startYear ) && ! isNaN( endYear ) ) {
// Check each year in the range
diff --git a/packages/js/date/src/test/index.ts b/packages/js/date/src/test/index.ts
index 86f50c67377..ed095d50136 100644
--- a/packages/js/date/src/test/index.ts
+++ b/packages/js/date/src/test/index.ts
@@ -2231,5 +2231,24 @@ describe( 'getDateFormatsForIntervalPhp', () => {
false
);
} );
+
+ test( 'uses the written year of date-only strings west of UTC', () => {
+ // Date-only strings parse as UTC midnight, so west of UTC the
+ // local year of a Jan 1 date is the previous year. Simulate UTC-5.
+ jest.spyOn( Date.prototype, 'getFullYear' ).mockImplementation(
+ function ( this: Date ) {
+ return new Date(
+ this.getTime() - 5 * 60 * 60 * 1000
+ ).getUTCFullYear();
+ }
+ );
+
+ expect( containsLeapYear( '2024-01-01', '2024-01-01' ) ).toBe(
+ true
+ );
+ expect( containsLeapYear( '2025-01-01', '2025-12-31' ) ).toBe(
+ false
+ );
+ } );
} );
} );