diff --git a/src/isValid.ts b/src/isValid.ts index 67481d5..7516833 100644 --- a/src/isValid.ts +++ b/src/isValid.ts @@ -48,7 +48,8 @@ export function validatePreparseResult(pr: PreparseResult, options?: ParserOptio const year = toGregorianYear(pr.Y, options) ?? base.Y; const month = pr.M ?? base.M; - return pr._index > 0 + return !pr._error + && pr._index > 0 && pr._length > 0 && pr._index === pr._length && pr._match > 0 diff --git a/src/preparse.ts b/src/preparse.ts index db56443..3604f0a 100644 --- a/src/preparse.ts +++ b/src/preparse.ts @@ -21,6 +21,11 @@ export interface PreparseResult extends ParsedComponents { * Number of matched tokens */ _match: number; + + /** + * True when a required format token could not be matched. + */ + _error?: true; } const wildcard = ' '; @@ -70,6 +75,7 @@ export function preparse(dateString: string, arg: string | CompiledObject, optio if (result) { if (!result.length) { + pr._error = true; break; } if (result.token && validateToken(result.token)) { @@ -77,7 +83,7 @@ export function preparse(dateString: string, arg: string | CompiledObject, optio } pr._index += result.length; pr._match++; - } else if (token === str[0] || token === wildcard) { + } else if (token === str[0] || token === wildcard && str.length > 0) { pr._index++; } else if (COMMENT.test(token) && !str.indexOf(token.replace(COMMENT, '$1'))) { pr._index += token.length - 2; @@ -85,6 +91,7 @@ export function preparse(dateString: string, arg: string | CompiledObject, optio pr._index = dateString.length; break; } else { + pr._error = true; break; } } diff --git a/tests/incomplete-format.spec.ts b/tests/incomplete-format.spec.ts new file mode 100644 index 0000000..00ead39 --- /dev/null +++ b/tests/incomplete-format.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from 'vitest'; +import { compile, isValid, parse, preparse } from '@/index.ts'; +import { parser } from '@/plugins/day-of-week.ts'; + +describe('incomplete format matching', () => { + test.each([ + ['2025', 'YYYY-MM-DD'], + ['2025-', 'YYYY-MM-DD'], + ['2025-01', 'YYYY-MM-DD'], + ['2025-01-', 'YYYY-MM-DD'], + ['2025-01-01', 'YYYY-MM-DD HH:mm'], + ['2025', 'YYYY[ required]'], + ['2025', 'YYYY '], + ['2025', 'YYYY ...'], + ['2025', 'YYYY ...'], + ['2025 ', 'YYYY ...'] + ])('rejects %s when %s still requires input', (input, format) => { + for (const pattern of [format, compile(format)]) { + expect(isValid(input, pattern)).toBe(false); + expect(Number.isNaN(parse(input, pattern).getTime())).toBe(true); + } + }); + + test('rejects an absent plugin token', () => { + const options = { plugins: [parser] }; + expect(isValid('2025-01-01 ', 'YYYY-MM-DD dddd', options)).toBe(false); + expect(Number.isNaN(parse('2025-01-01 ', 'YYYY-MM-DD dddd', options).getTime())).toBe(true); + }); + + test('preserves parsed components when matching fails', () => { + expect(preparse('2025-01', 'YYYY-MM-DD')).toMatchObject({ + Y: 2025, M: 1, _error: true, _index: 7, _length: 7, _match: 2 + }); + }); + + test.each([ + ['2025', 'YYYY'], + ['2025-01', 'YYYY-MM'], + ['2025-01-01', 'YYYY-MM-DD'], + ['2025', 'YYYY...'], + ['2025 additional text', 'YYYY...'], + ['2025 ', 'YYYY '], + ['2025 ', 'YYYY ...'], + ['2025 ', 'YYYY ...'] + ])('accepts %s when %s is fully matched', (input, pattern) => { + expect(isValid(input, pattern)).toBe(true); + expect(Number.isNaN(parse(input, pattern).getTime())).toBe(false); + }); +}); diff --git a/tests/preparse.spec.ts b/tests/preparse.spec.ts index 7d9be04..c9813ac 100644 --- a/tests/preparse.spec.ts +++ b/tests/preparse.spec.ts @@ -130,7 +130,7 @@ describe('MMMM', () => { }); test('Zero', () => { - const dt = { _index: 0, _length: 4, _match: 0 }; + const dt = { _error: true, _index: 0, _length: 4, _match: 0 }; expect(preparse('Zero', 'MMMM')).toEqual(dt); }); }); @@ -147,7 +147,7 @@ describe('MMM', () => { }); test('Zero', () => { - const dt = { _index: 0, _length: 4, _match: 0 }; + const dt = { _error: true, _index: 0, _length: 4, _match: 0 }; expect(preparse('Zero', 'MMM')).toEqual(dt); }); }); @@ -300,7 +300,7 @@ describe('AA', () => { }); test('M.M.', () => { - const dt = { _index: 0, _length: 4, _match: 0 }; + const dt = { _error: true, _index: 0, _length: 4, _match: 0 }; expect(preparse('M.M.', 'AA')).toEqual(dt); }); }); @@ -317,7 +317,7 @@ describe('A', () => { }); test('MM', () => { - const dt = { _index: 0, _length: 2, _match: 0 }; + const dt = { _error: true, _index: 0, _length: 2, _match: 0 }; expect(preparse('MM', 'A')).toEqual(dt); }); }); @@ -334,7 +334,7 @@ describe('aa', () => { }); test('m.m.', () => { - const dt = { _index: 0, _length: 4, _match: 0 }; + const dt = { _error: true, _index: 0, _length: 4, _match: 0 }; expect(preparse('m.m.', 'aa')).toEqual(dt); }); }); @@ -351,7 +351,7 @@ describe('a', () => { }); test('mm', () => { - const dt = { _index: 0, _length: 2, _match: 0 }; + const dt = { _error: true, _index: 0, _length: 2, _match: 0 }; expect(preparse('mm', 'a')).toEqual(dt); }); });