diff --git a/src/datetime.js b/src/datetime.js index 9472cb531..d98fd4173 100644 --- a/src/datetime.js +++ b/src/datetime.js @@ -901,9 +901,13 @@ export default class DateTime { numberingSystem, defaultToEN: true, }), - [vals, parsedZone, specificOffset, invalid] = parseFromTokens(localeToUse, text, fmt); + [vals, parsedZone, specificOffset, invalid, explanation] = parseFromTokens( + localeToUse, + text, + fmt + ); if (invalid) { - return DateTime.invalid(invalid); + return DateTime.invalid(invalid, explanation); } else { return parseDataToDateTime(vals, parsedZone, opts, `format ${fmt}`, text, specificOffset); } diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js index 8dd38f37f..5ef410d71 100644 --- a/src/impl/tokenParser.js +++ b/src/impl/tokenParser.js @@ -450,14 +450,30 @@ export function explainFromTokens(locale, input, format) { throw new ConflictingSpecificationError( "Can't include meridiem when specifying 24-hour format" ); + } else if ( + hasOwnProperty(matches, "h") && + matches["h"] > 12 && + tokens.find((t) => t.val === "h" || t.val === "hh") + ) { + const hourValue = matches["h"]; + return { + input, + tokens, + invalidReason: "unit out of range", + invalidExplanation: `you specified ${hourValue} (of type ${typeof hourValue}) as an hour along with a meridiem, which is invalid`, + }; } return { input, tokens, regex, rawMatches, matches, result, zone, specificOffset }; } } export function parseFromTokens(locale, input, format) { - const { result, zone, specificOffset, invalidReason } = explainFromTokens(locale, input, format); - return [result, zone, specificOffset, invalidReason]; + const { result, zone, specificOffset, invalidReason, invalidExplanation } = explainFromTokens( + locale, + input, + format + ); + return [result, zone, specificOffset, invalidReason, invalidExplanation]; } export function formatOptsToTokens(formatOpts, locale) { diff --git a/test/datetime/tokenParse.test.js b/test/datetime/tokenParse.test.js index 4025821d8..58ad04175 100644 --- a/test/datetime/tokenParse.test.js +++ b/test/datetime/tokenParse.test.js @@ -18,10 +18,40 @@ test("DateTime.fromFormat() parses basic times", () => { expect(i.millisecond).toBe(445); }); +test("DateTime.fromFormat() yields Invalid reason for invalid 12-hour time", () => { + const i = DateTime.fromFormat("11-08-2023 15:00 AM", "MM-dd-yyyy h:mm a"); + expect(i.invalid).not.toBeNull(); + expect(i.invalid.reason).toEqual("unit out of range"); + expect(i.invalid.explanation).toEqual( + "you specified 15 (of type number) as an hour along with a meridiem, which is invalid" + ); +}); + +test("DateTime.fromFormat() yields Invalid reason for invalid 12-hour time with padding", () => { + const i = DateTime.fromFormat("11-08-2023 15:00 AM", "MM-dd-yyyy hh:mm a"); + expect(i.invalid).not.toBeNull(); + expect(i.invalid.reason).toEqual("unit out of range"); + expect(i.invalid.explanation).toEqual( + "you specified 15 (of type number) as an hour along with a meridiem, which is invalid" + ); +}); + +test("DateTime.fromFormat() throws ConflictingSpecificationError for invalid format", () => { + expect(() => { + DateTime.fromFormat("11-08-2023 12:00 AM", "MM-dd-yyyy H:mm a"); + }).toThrowError( + ConflictingSpecificationError, + "Can't include meridiem when specifying 24-hour format" + ); +}); + test("DateTime.fromFormat() yields Invalid reason 'unparseable' for incompatible formats", () => { const i = DateTime.fromFormat("Mar 3, 2020", "MMM dd, yyyy"); - expect(i.invalid).not.toBeNull; + expect(i.invalid).not.toBeNull(); expect(i.invalid.reason).toEqual("unparsable"); + expect(i.invalid.explanation).toEqual( + 'the input "Mar 3, 2020" can\'t be parsed as format MMM dd, yyyy' + ); }); test("DateTime.fromFormat() parses with variable-length input", () => {