@@ -84,6 +84,7 @@ describe('parseNumberCell', () => {
8484describe ( 'parseDateCell' , ( ) => {
8585 it ( 'normalises bare calendar dates without timezone drift' , ( ) => {
8686 expect ( parseDateCell ( '2026-06-30' , 'date' ) ) . toBe ( '2026-06-30' ) ;
87+ expect ( parseDateCell ( '2026/6/3' , 'date' ) ) . toBe ( '2026-06-03' ) ;
8788 } ) ;
8889 it ( 'emits full ISO for datetime' , ( ) => {
8990 expect ( parseDateCell ( '2026-06-30' , 'datetime' ) ) . toBe ( '2026-06-30T00:00:00.000Z' ) ;
@@ -99,16 +100,17 @@ describe('parseDateCell', () => {
99100} ) ;
100101
101102/**
102- * [#20534] A text cell is read only in ISO 8601 or the export's own
103- * `YYYY-MM-DD HH:mm:ss`, on a calendar day that exists; everything else is
104- * refused (`undefined`, so the row's `invalid_date`), never rolled over, never
103+ * [#20534] A text cell is read only in ISO 8601, the export's own
104+ * `YYYY-MM-DD HH:mm:ss` or a year-first date (`2026/7/15`, `2026/7/15 9:00`,
105+ * by the maintainer ruling on the card), on a calendar day that exists;
106+ * everything else is refused (`undefined`, so the row's `invalid_date`), never rolled over, never
105107 * read in the host's zone and never read month-first. A `date`'s year keeps
106108 * four digits. Every case runs under two host zones that disagree by twelve
107109 * hours, and must answer the same under both — the host-zone reading this
108110 * removes answered differently (`07/15/2026 10:00` was `…T14:00Z` in New York
109111 * and `…T02:00Z` in Shanghai).
110112 */
111- describe ( '[#20534] parseDateCell — ISO 8601 or the export shape, on a real day' , ( ) => {
113+ describe ( '[#20534] parseDateCell — ISO 8601, the export shape or a year-first date , on a real day' , ( ) => {
112114 const HOST_ZONES = [ 'America/New_York' , 'Asia/Shanghai' ] ;
113115 const originalTz = process . env . TZ ;
114116 afterEach ( ( ) => {
@@ -140,9 +142,12 @@ describe('[#20534] parseDateCell — ISO 8601 or the export shape, on a real day
140142 [ '07/08/2026' , 'datetime' ] , [ '07/08/2026' , 'date' ] ,
141143 [ '07/15/2026' , 'date' ] , [ '15 July 2026' , 'date' ] , [ '15 July 2026' , 'datetime' ] ,
142144 [ 'Jul 15 2026 10:00' , 'time' ] , [ 'Wed, 15 Jul 2026 10:00:00 GMT' , 'datetime' ] ,
143- // Year-first but not ISO 8601.
144- [ '2026/6/3' , 'date' ] , [ '2026/07/15' , 'date' ] , [ '2026-7-15' , 'date' ] ,
145- [ '2026/08/01 06:00:00' , 'datetime' ] , [ '2026-07-15 9:00' , 'datetime' ] ,
145+ // Year-first, outside its one form: an impossible day, a mixed separator,
146+ // a clock out of range, a `T`, a zone, a fraction, a two-digit year.
147+ [ '2026/2/30' , 'date' ] , [ '2026/2/30' , 'datetime' ] , [ '2026/7-15' , 'date' ] ,
148+ [ '2026/7/15 24:00' , 'datetime' ] , [ '2026/7/15 9:60' , 'datetime' ] ,
149+ [ '2026/7/15T9:00' , 'datetime' ] , [ '2026/7/15 9:00Z' , 'datetime' ] ,
150+ [ '2026/7/15 9:00:00.5' , 'datetime' ] , [ '26/7/15' , 'date' ] , [ '07/15/2026' , 'datetime' ] ,
146151 // Reduced / expanded forms, a zone after a space, lower-case `t` / `z`.
147152 [ '2026' , 'date' ] , [ '2026-07' , 'datetime' ] , [ '+002026-07-15' , 'date' ] ,
148153 [ '2026-07-15 10:00Z' , 'datetime' ] , [ '2026-07-15 10:00:00+08:00' , 'datetime' ] ,
@@ -176,6 +181,19 @@ describe('[#20534] parseDateCell — ISO 8601 or the export shape, on a real day
176181 [ '2026-07-15T24:00:00Z' , 'datetime' , '2026-07-16T00:00:00.000Z' ] ,
177182 [ ' 2026-07-15 ' , 'date' , '2026-07-15' ] ,
178183 [ '10:00' , 'time' , '10:00:00' ] ,
184+ // A year-first date (Excel's zh-CN / ja-JP short date): the padded ISO day,
185+ // and a clock read as a wall clock exactly as the export shape's is.
186+ [ '2026/6/3' , 'date' , '2026-06-03' ] ,
187+ [ '2026/07/15' , 'date' , '2026-07-15' ] ,
188+ [ '2026-7-15' , 'date' , '2026-07-15' ] ,
189+ [ '2026/7/15' , 'datetime' , '2026-07-15T00:00:00.000Z' ] ,
190+ [ '2026/7/15 9:00' , 'datetime' , '2026-07-15T09:00:00.000Z' ] ,
191+ [ '2026/7/15 9:00' , 'date' , '2026-07-15' ] ,
192+ [ '2026/7/15 9:00' , 'time' , '09:00:00' ] ,
193+ [ '2026/08/01 06:00:00' , 'datetime' , '2026-08-01T06:00:00.000Z' ] ,
194+ [ '2026-07-15 9:00' , 'datetime' , '2026-07-15T09:00:00.000Z' ] ,
195+ [ '2028/2/29' , 'date' , '2028-02-29' ] ,
196+ [ '0500/1/1' , 'date' , '0500-01-01' ] ,
179197 // The year keeps four digits on every `date` branch.
180198 [ '0500-01-01' , 'date' , '0500-01-01' ] ,
181199 [ '0001-01-01' , 'date' , '0001-01-01' ] ,
@@ -234,7 +252,7 @@ describe('coerceRow', () => {
234252 note : { type : 'text' } ,
235253 } ) ;
236254 const { data, errors } = await coerceRow (
237- { done : '是' , amount : '$1,200.50' , priority : '高' , tags : 'A, B' , due : '2026-07- 01' , note : ' hi ' } ,
255+ { done : '是' , amount : '$1,200.50' , priority : '高' , tags : 'A, B' , due : '2026/07/ 01' , note : ' hi ' } ,
238256 metaMap ,
239257 { } ,
240258 ) ;
0 commit comments