@@ -462,3 +462,186 @@ Deno.test("FullDate: does not crash on nonsense date-like input", () => {
462462 }
463463 }
464464} ) ;
465+
466+ // ── ISODate (YYYY-MM-DD) ───────────────────────────────────────────
467+
468+ Deno . test ( "ISODate: basic YYYY-MM-DD" , ( ) => {
469+ const res = Duckling ( [ Time . parser ] ) . extract ( "Published 2024-05-18." ) ;
470+
471+ assertEquals ( res , [
472+ {
473+ end : 20 ,
474+ kind : "time" ,
475+ start : 10 ,
476+ text : "2024-05-18" ,
477+ value : {
478+ era : "CE" ,
479+ grain : "day" ,
480+ when : "2024-05-18T00:00:00.000Z" ,
481+ } ,
482+ } ,
483+ ] ) ;
484+ } ) ;
485+
486+ Deno . test ( "ISODate: multiple YYYY-MM-DD in sentence" , ( ) => {
487+ const res = Duckling ( [ Time . parser ] ) . extract (
488+ "Born on 1990-03-15, died 2060-01-01." ,
489+ ) ;
490+
491+ assertEquals ( res . length , 2 ) ;
492+ assertEquals ( res [ 0 ] . text , "1990-03-15" ) ;
493+ assertEquals ( res [ 0 ] . value . when , "1990-03-15T00:00:00.000Z" ) ;
494+ assertEquals ( res [ 1 ] . text , "2060-01-01" ) ;
495+ assertEquals ( res [ 1 ] . value . when , "2060-01-01T00:00:00.000Z" ) ;
496+ } ) ;
497+
498+ // ── ISODateTime (with offset / without Z) ──────────────────────────
499+
500+ Deno . test ( "ISODateTime: with positive offset" , ( ) => {
501+ const res = Duckling ( [ Time . parser ] ) . extract (
502+ "Logged at 2024-05-18T10:30:00+02:00." ,
503+ ) ;
504+
505+ assertEquals ( res . length , 1 ) ;
506+ assertEquals ( res [ 0 ] . text , "2024-05-18T10:30:00+02:00" ) ;
507+ assertEquals ( res [ 0 ] . value . when , "2024-05-18T08:30:00.000Z" ) ;
508+ assertEquals ( res [ 0 ] . value . grain , "second" ) ;
509+ } ) ;
510+
511+ Deno . test ( "ISODateTime: with negative offset" , ( ) => {
512+ const res = Duckling ( [ Time . parser ] ) . extract (
513+ "Event: 2024-05-18T10:30:00-05:00." ,
514+ ) ;
515+
516+ assertEquals ( res . length , 1 ) ;
517+ assertEquals ( res [ 0 ] . text , "2024-05-18T10:30:00-05:00" ) ;
518+ assertEquals ( res [ 0 ] . value . when , "2024-05-18T15:30:00.000Z" ) ;
519+ } ) ;
520+
521+ Deno . test ( "ISODateTime: without timezone (local time)" , ( ) => {
522+ const res = Duckling ( [ Time . parser ] ) . extract (
523+ "Timestamp: 2024-05-18T10:30:00." ,
524+ ) ;
525+
526+ assertEquals ( res . length , 1 ) ;
527+ assertEquals ( res [ 0 ] . text , "2024-05-18T10:30:00" ) ;
528+ assertEquals ( res [ 0 ] . value . grain , "second" ) ;
529+ } ) ;
530+
531+ Deno . test ( "ISODateTime: without seconds" , ( ) => {
532+ const res = Duckling ( [ Time . parser ] ) . extract (
533+ "Meeting at 2024-05-18T10:30." ,
534+ ) ;
535+
536+ assertEquals ( res . length , 1 ) ;
537+ assertEquals ( res [ 0 ] . text , "2024-05-18T10:30" ) ;
538+ assertEquals ( res [ 0 ] . value . grain , "second" ) ;
539+ } ) ;
540+
541+ // ── LiteralMonthDayYear ────────────────────────────────────────────
542+
543+ Deno . test ( "LiteralMonthDayYear: with comma" , ( ) => {
544+ const res = Duckling ( [ Time . parser ] ) . extract ( "On July 13, 2016 we met." ) ;
545+
546+ assertEquals ( res . length , 1 ) ;
547+ assertEquals ( res [ 0 ] . text , "July 13, 2016" ) ;
548+ assertEquals ( res [ 0 ] . value . grain , "day" ) ;
549+ } ) ;
550+
551+ Deno . test ( "LiteralMonthDayYear: without comma" , ( ) => {
552+ const res = Duckling ( [ Time . parser ] ) . extract ( "On March 3 1990 we met." ) ;
553+
554+ assertEquals ( res . length , 1 ) ;
555+ assertEquals ( res [ 0 ] . text , "March 3 1990" ) ;
556+ assertEquals ( res [ 0 ] . value . grain , "day" ) ;
557+ } ) ;
558+
559+ Deno . test ( "LiteralMonthDayYear: various months" , ( ) => {
560+ const inputs = [
561+ "January 1, 2000" ,
562+ "December 25, 2024" ,
563+ "September 5, 2019" ,
564+ ] ;
565+ for ( const input of inputs ) {
566+ const res = Duckling ( [ Time . parser ] ) . extract ( `Published: ${ input } .` ) ;
567+ assertEquals (
568+ res . length >= 1 ,
569+ true ,
570+ `Should extract at least one entity from "${ input } ", got ${ res . length } ` ,
571+ ) ;
572+ assertEquals ( res [ 0 ] . text , input ) ;
573+ assertEquals ( res [ 0 ] . value . grain , "day" ) ;
574+ }
575+ } ) ;
576+
577+ // ── ClockTime ──────────────────────────────────────────────────────
578+
579+ Deno . test ( "ClockTime: HH:MM with timezone in parens" , ( ) => {
580+ const res = Duckling ( [ Time . parser ] ) . extract ( "The call is at 23:28 (UTC)." ) ;
581+
582+ assertEquals ( res . length , 1 ) ;
583+ assertEquals ( res [ 0 ] . text , "23:28 (UTC)" ) ;
584+ assertEquals ( res [ 0 ] . value . when , "23:28 (UTC)" ) ;
585+ assertEquals ( res [ 0 ] . value . grain , "minute" ) ;
586+ } ) ;
587+
588+ Deno . test ( "ClockTime: HH:MM:SS" , ( ) => {
589+ const res = Duckling ( [ Time . parser ] ) . extract ( "Logged at 23:28:59." ) ;
590+
591+ assertEquals ( res . length , 1 ) ;
592+ assertEquals ( res [ 0 ] . text , "23:28:59" ) ;
593+ assertEquals ( res [ 0 ] . value . grain , "second" ) ;
594+ } ) ;
595+
596+ Deno . test ( "ClockTime: 12-hour AM/PM" , ( ) => {
597+ const cases = [
598+ { input : "Meet at 3:45 PM." , expected : "3:45 PM" } ,
599+ { input : "Wake up at 6:00 AM." , expected : "6:00 AM" } ,
600+ { input : "Deadline 11:59 pm." , expected : "11:59 pm" } ,
601+ ] ;
602+ for ( const { input, expected } of cases ) {
603+ const res = Duckling ( [ Time . parser ] ) . extract ( input ) ;
604+ assertEquals (
605+ res . length >= 1 ,
606+ true ,
607+ `Should extract at least one entity from "${ input } "` ,
608+ ) ;
609+ assertEquals ( res [ 0 ] . text , expected ) ;
610+ assertEquals ( res [ 0 ] . value . grain , "minute" ) ;
611+ }
612+ } ) ;
613+
614+ Deno . test ( "ClockTime: HH:MM with bare timezone" , ( ) => {
615+ const cases = [
616+ { input : "14:00 UTC" , expected : "14:00 UTC" } ,
617+ { input : "09:30 EST" , expected : "09:30 EST" } ,
618+ { input : "11:15 CET" , expected : "11:15 CET" } ,
619+ ] ;
620+ for ( const { input, expected } of cases ) {
621+ const res = Duckling ( [ Time . parser ] ) . extract ( input ) ;
622+ assertEquals (
623+ res . length >= 1 ,
624+ true ,
625+ `Should extract at least one entity from "${ input } "` ,
626+ ) ;
627+ assertEquals ( res [ 0 ] . text , expected ) ;
628+ }
629+ } ) ;
630+
631+ // ── Noon / Midnight ────────────────────────────────────────────────
632+
633+ Deno . test ( "Common: noon" , ( ) => {
634+ const res = Duckling ( [ Time . parser ] ) . extract ( "We eat lunch at noon." ) ;
635+ assertEquals ( res . length , 1 ) ;
636+ assertEquals ( res [ 0 ] . text , "noon" ) ;
637+ assertEquals ( res [ 0 ] . value . when , "12:00" ) ;
638+ assertEquals ( res [ 0 ] . value . grain , "hour" ) ;
639+ } ) ;
640+
641+ Deno . test ( "Common: midnight" , ( ) => {
642+ const res = Duckling ( [ Time . parser ] ) . extract ( "Come back before midnight." ) ;
643+ assertEquals ( res . length , 1 ) ;
644+ assertEquals ( res [ 0 ] . text , "midnight" ) ;
645+ assertEquals ( res [ 0 ] . value . when , "00:00" ) ;
646+ assertEquals ( res [ 0 ] . value . grain , "hour" ) ;
647+ } ) ;
0 commit comments