@@ -387,3 +387,200 @@ def test_this_week_day_after_day(self):
387387 result = parsing .this_week_day (base_date , weekday )
388388
389389 self .assertEqual (result , datetime (2016 , 12 , 14 , 10 , 10 , 52 , 85280 ))
390+
391+ def test_today_at_time_uses_base_date (self ):
392+ """
393+ The string 'today at [time]' should respect the base_date parameter.
394+ """
395+ base_date = datetime (2018 , 7 , 14 , 8 , 52 , 21 ) # July 14, 2018 at 8:52:21 AM
396+ input_text = 'Your dental appointment is scheduled today at 9:00pm.'
397+ parser = parsing .datetime_parsing (input_text , base_date )
398+
399+ self .assertEqual (len (parser ), 1 )
400+ self .assertIn ('today at 9:00pm' , parser [0 ])
401+
402+ parsed_datetime = parser [0 ][1 ]
403+ # Should be July 14, 2018 at 9:00 PM
404+ self .assertEqual (parsed_datetime .year , 2018 )
405+ self .assertEqual (parsed_datetime .month , 7 )
406+ self .assertEqual (parsed_datetime .day , 14 )
407+ self .assertEqual (parsed_datetime .hour , 21 ) # 9 PM
408+ self .assertEqual (parsed_datetime .minute , 0 )
409+
410+ def test_next_month_from_january_31st (self ):
411+ """
412+ Test 'next month' from Jan 31 handles February correctly
413+ """
414+ base_date = datetime (2025 , 1 , 31 , 12 , 0 , 0 )
415+ input_text = 'next month'
416+ parser = parsing .datetime_parsing (input_text , base_date )
417+
418+ self .assertEqual (len (parser ), 1 )
419+ # February only has 28 days in 2025, should clamp to last valid day
420+ self .assertEqual (parser [0 ][1 ].year , 2025 )
421+ self .assertEqual (parser [0 ][1 ].month , 2 )
422+ self .assertEqual (parser [0 ][1 ].day , 28 )
423+
424+ def test_next_3_months_crosses_year_boundary (self ):
425+ """
426+ Test 'next 3 months' crossing year boundary
427+ """
428+ base_date = datetime (2025 , 11 , 15 , 12 , 0 , 0 )
429+ input_text = 'next 3 months'
430+ parser = parsing .datetime_parsing (input_text , base_date )
431+
432+ self .assertEqual (len (parser ), 1 )
433+ self .assertEqual (parser [0 ][1 ].year , 2026 )
434+ self .assertEqual (parser [0 ][1 ].month , 2 )
435+ self .assertEqual (parser [0 ][1 ].day , 15 )
436+
437+ def test_next_month_from_march_31st (self ):
438+ """
439+ Test 'next month' from March 31 handles April correctly
440+ """
441+ base_date = datetime (2025 , 3 , 31 , 12 , 0 , 0 )
442+ input_text = 'next month'
443+ parser = parsing .datetime_parsing (input_text , base_date )
444+
445+ self .assertEqual (len (parser ), 1 )
446+ # April only has 30 days, should pick the last valid day
447+ self .assertEqual (parser [0 ][1 ].year , 2025 )
448+ self .assertEqual (parser [0 ][1 ].month , 4 )
449+ self .assertEqual (parser [0 ][1 ].day , 30 )
450+
451+ def test_next_month_from_may_31st (self ):
452+ """
453+ Test 'next month' from May 31 handles June correctly
454+ """
455+ base_date = datetime (2025 , 5 , 31 , 12 , 0 , 0 )
456+ input_text = 'next month'
457+ parser = parsing .datetime_parsing (input_text , base_date )
458+
459+ self .assertEqual (len (parser ), 1 )
460+ # June only has 30 days, should pick the last valid day
461+ self .assertEqual (parser [0 ][1 ].year , 2025 )
462+ self .assertEqual (parser [0 ][1 ].month , 6 )
463+ self .assertEqual (parser [0 ][1 ].day , 30 )
464+
465+ def test_multiple_datetime_expressions (self ):
466+ """
467+ Test parsing text with multiple date/time references
468+ """
469+ base_date = datetime (2025 , 10 , 18 , 12 , 0 , 0 )
470+ input_text = 'Meeting today at 2pm and tomorrow at 3pm'
471+ parser = parsing .datetime_parsing (input_text , base_date )
472+
473+ self .assertEqual (len (parser ), 2 )
474+ # First: today at 2pm
475+ self .assertEqual (parser [0 ][1 ].year , 2025 )
476+ self .assertEqual (parser [0 ][1 ].month , 10 )
477+ self .assertEqual (parser [0 ][1 ].day , 18 )
478+ self .assertEqual (parser [0 ][1 ].hour , 14 )
479+ # Second: tomorrow at 3pm
480+ self .assertEqual (parser [1 ][1 ].year , 2025 )
481+ self .assertEqual (parser [1 ][1 ].month , 10 )
482+ self .assertEqual (parser [1 ][1 ].day , 19 )
483+ self .assertEqual (parser [1 ][1 ].hour , 15 )
484+
485+ def test_duration_from_yesterday (self ):
486+ """
487+ Test '2 days after yesterday' using base_time
488+ """
489+ base_date = datetime (2025 , 10 , 18 , 12 , 0 , 0 )
490+ input_text = '2 days after yesterday'
491+ parser = parsing .datetime_parsing (input_text , base_date )
492+
493+ self .assertEqual (len (parser ), 1 )
494+ # Yesterday = Oct 17, + 2 days = Oct 19
495+ self .assertEqual (parser [0 ][1 ].year , 2025 )
496+ self .assertEqual (parser [0 ][1 ].month , 10 )
497+ self .assertEqual (parser [0 ][1 ].day , 19 )
498+
499+ def test_duration_from_tomorrow (self ):
500+ """
501+ Test '3 days after tomorrow'
502+ """
503+ base_date = datetime (2025 , 10 , 18 , 12 , 0 , 0 )
504+ input_text = '3 days after tomorrow'
505+ parser = parsing .datetime_parsing (input_text , base_date )
506+
507+ self .assertEqual (len (parser ), 1 )
508+ # Tomorrow = Oct 19, + 3 days = Oct 22
509+ self .assertEqual (parser [0 ][1 ].year , 2025 )
510+ self .assertEqual (parser [0 ][1 ].month , 10 )
511+ self .assertEqual (parser [0 ][1 ].day , 22 )
512+
513+ def test_duration_from_today (self ):
514+ """
515+ Test '5 days before today'
516+ """
517+ base_date = datetime (2025 , 10 , 18 , 12 , 0 , 0 )
518+ input_text = '5 days before today'
519+ parser = parsing .datetime_parsing (input_text , base_date )
520+
521+ self .assertEqual (len (parser ), 1 )
522+ # Today = Oct 18, - 5 days = Oct 13
523+ self .assertEqual (parser [0 ][1 ].year , 2025 )
524+ self .assertEqual (parser [0 ][1 ].month , 10 )
525+ self .assertEqual (parser [0 ][1 ].day , 13 )
526+
527+ def test_noon_without_convention (self ):
528+ """
529+ Test '12:00' without AM/PM defaults to AM convention (midnight = 0)
530+ """
531+ base_date = datetime (2025 , 10 , 18 , 0 , 0 , 0 )
532+ input_text = 'Meeting at 12:00'
533+ parser = parsing .datetime_parsing (input_text , base_date )
534+
535+ self .assertEqual (len (parser ), 1 )
536+ # No convention defaults to 'am', so 12:00 becomes 0 (midnight)
537+ self .assertEqual (parser [0 ][1 ].hour , 0 )
538+ self .assertEqual (parser [0 ][1 ].minute , 0 )
539+
540+ def test_twelve_pm (self ):
541+ """
542+ Test '12:00 pm' is noon (stays as 12)
543+ """
544+ base_date = datetime (2025 , 10 , 18 , 0 , 0 , 0 )
545+ input_text = 'Meeting at 12:00 pm'
546+ parser = parsing .datetime_parsing (input_text , base_date )
547+
548+ self .assertEqual (len (parser ), 1 )
549+ self .assertEqual (parser [0 ][1 ].hour , 12 )
550+ self .assertEqual (parser [0 ][1 ].minute , 0 )
551+
552+ def test_twelve_am (self ):
553+ """
554+ Test '12:00 am' is midnight (converted to 0)
555+ """
556+ base_date = datetime (2025 , 10 , 18 , 0 , 0 , 0 )
557+ input_text = 'Meeting at 12:00 am'
558+ parser = parsing .datetime_parsing (input_text , base_date )
559+
560+ self .assertEqual (len (parser ), 1 )
561+ self .assertEqual (parser [0 ][1 ].hour , 0 )
562+ self .assertEqual (parser [0 ][1 ].minute , 0 )
563+
564+ def test_one_am (self ):
565+ """
566+ Test '1:00 am' is 1:00
567+ """
568+ base_date = datetime (2025 , 10 , 18 , 0 , 0 , 0 )
569+ input_text = 'Meeting at 1:00 am'
570+ parser = parsing .datetime_parsing (input_text , base_date )
571+
572+ self .assertEqual (len (parser ), 1 )
573+ self .assertEqual (parser [0 ][1 ].hour , 1 )
574+ self .assertEqual (parser [0 ][1 ].minute , 0 )
575+
576+ def test_one_pm (self ):
577+ """
578+ Test '1:00 pm' is 13:00
579+ """
580+ base_date = datetime (2025 , 10 , 18 , 0 , 0 , 0 )
581+ input_text = 'Meeting at 1:00 pm'
582+ parser = parsing .datetime_parsing (input_text , base_date )
583+
584+ self .assertEqual (len (parser ), 1 )
585+ self .assertEqual (parser [0 ][1 ].hour , 13 )
586+ self .assertEqual (parser [0 ][1 ].minute , 0 )
0 commit comments