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
+			);
+		} );
 	} );
 } );