feat(common): throw error for suspicious date patterns (#59798)

Adjusts the date pipe and formatDate function to detect suspicious usages of the week-numbering year formatter without including the week number, as this is often confused for the calendar year and likely to result in incorrect results near New Years, meaning that those issues aren't typically caught during development. This commit starts throwing a development-only error to reveal this issue right away.

BREAKING CHANGE:

Using the `Y` formatter (week-numbering year) without also including `w` (week number) is now detected as suspicious date pattern, as `y` is typically intended.

PR Close #59798
This commit is contained in:
Stephen Hicks
2025-01-29 18:33:23 -05:00
committed by kirjs
parent e4ef74dbde
commit 74cceba587
4 changed files with 65 additions and 13 deletions
+3
View File
@@ -21,6 +21,9 @@ export const enum RuntimeErrorCode {
// NgForOf errors
NG_FOR_MISSING_DIFFER = -2200,
// I18n errors
SUSPICIOUS_DATE_FORMAT = 2300,
// Keep 2800 - 2900 for Http Errors.
// Image directive errors
+33
View File
@@ -6,6 +6,11 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {
ɵRuntimeError as RuntimeError,
ɵformatRuntimeError as formatRuntimeError,
} from '@angular/core';
import {
FormatWidth,
FormStyle,
@@ -24,6 +29,7 @@ import {
Time,
TranslationWidth,
} from './locale_data_api';
import {RuntimeErrorCode} from '../errors';
export const ISO8601_DATE_REGEX =
/^(\d{4,})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
@@ -104,6 +110,10 @@ export function formatDate(
}
}
if (typeof ngDevMode === 'undefined' || ngDevMode) {
assertValidDateFormat(parts);
}
let dateTimezoneOffset = date.getTimezoneOffset();
if (timezone) {
dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
@@ -123,6 +133,29 @@ export function formatDate(
return text;
}
/**
* Asserts that the given date format is free from common mistakes. Throws an
* error if one is found (except for the case of all "Y", in which case we just
* log a warning). This should only be called in development mode.
*/
function assertValidDateFormat(parts: string[]) {
if (parts.some((part) => /^Y+$/.test(part)) && !parts.some((part) => /^w+$/.test(part))) {
// "Y" indicates "week-based year", which differs from the actual calendar
// year for a few days around Jan 1 most years. Unless "w" is also
// present (e.g. a date like "2024-W52") this is likely a mistake. Users
// probably meant "y" instead.
const message = `Suspicious use of week-based year "Y" in date pattern "${parts.join(
'',
)}". Did you mean to use calendar year "y" instead?`;
if (parts.length === 1) {
// NOTE: allow "YYYY" with just a warning, since it's used in tests.
console.error(formatRuntimeError(RuntimeErrorCode.SUSPICIOUS_DATE_FORMAT, message));
} else {
throw new RuntimeError(RuntimeErrorCode.SUSPICIOUS_DATE_FORMAT, message);
}
}
}
/**
* Create a new Date object with the given date value, and the time set to midnight.
*