commit a97dbbd18171415db2a7b7ab97a954bac0457365
Author: Ben Sima <ben@bensima.com>
Date: Wed Dec 31 14:29:22 2025
Fix day rollover bug in reminder time parsing
Modified addHours and addMinutes helpers to return (dayOffset, TimeOfDay)
tuple instead of just TimeOfDay. The dayOffset is calculated as
totalMins div (24*60) and applied using Calendar.addDays.
Now 'in 3 hours' at 11pm correctly gives 2am next day.
Added tests for day rollover scenarios.
Task-Id: t-296
diff --git a/Omni/Agent/Telegram/Reminders.hs b/Omni/Agent/Telegram/Reminders.hs
index 1ddaeded..4139d087 100644
--- a/Omni/Agent/Telegram/Reminders.hs
+++ b/Omni/Agent/Telegram/Reminders.hs
@@ -59,9 +59,49 @@ test =
isJust result Test.@=? True,
Test.unit "parseReminderTime handles ISO format" <| do
result <- parseReminderTime "2024-12-31 17:00"
- isJust result Test.@=? True
+ isJust result Test.@=? True,
+ Test.unit "day rollover: adding hours past midnight" <| do
+ -- Adding 3 hours at 11pm should give 2am next day
+ let tod = TimeOfDay 23 0 0 -- 11pm
+ (dayOffset, newTod) = testAddHours 3 tod
+ dayOffset Test.@=? 1 -- Should roll over to next day
+ todHour newTod Test.@=? 2 -- Should be 2am
+ todMin newTod Test.@=? 0,
+ Test.unit "day rollover: adding minutes past midnight" <| do
+ -- Adding 90 minutes at 11pm should give 12:30am next day
+ let tod = TimeOfDay 23 0 0 -- 11pm
+ (dayOffset, newTod) = testAddMinutes 90 tod
+ dayOffset Test.@=? 1 -- Should roll over to next day
+ todHour newTod Test.@=? 0 -- Should be 12:30am
+ todMin newTod Test.@=? 30,
+ Test.unit "no rollover: adding hours within same day" <| do
+ -- Adding 2 hours at 10am should stay same day
+ let tod = TimeOfDay 10 0 0
+ (dayOffset, newTod) = testAddHours 2 tod
+ dayOffset Test.@=? 0 -- Same day
+ todHour newTod Test.@=? 12 -- Should be noon
]
+-- | Test helper: add hours to a TimeOfDay, returning (day offset, new TimeOfDay)
+testAddHours :: Int -> TimeOfDay -> (Integer, TimeOfDay)
+testAddHours h tod =
+ let totalMins = todHour tod * 60 + todMin tod + h * 60
+ dayOffset = fromIntegral (totalMins `div` (24 * 60))
+ remainingMins = totalMins `mod` (24 * 60)
+ newHour = remainingMins `div` 60
+ newMin = remainingMins `mod` 60
+ in (dayOffset, TimeOfDay newHour newMin (todSec tod))
+
+-- | Test helper: add minutes to a TimeOfDay, returning (day offset, new TimeOfDay)
+testAddMinutes :: Int -> TimeOfDay -> (Integer, TimeOfDay)
+testAddMinutes m tod =
+ let totalMins = todHour tod * 60 + todMin tod + m
+ dayOffset = fromIntegral (totalMins `div` (24 * 60))
+ remainingMins = totalMins `mod` (24 * 60)
+ newHour = remainingMins `div` 60
+ newMin = remainingMins `mod` 60
+ in (dayOffset, TimeOfDay newHour newMin (todSec tod))
+
-- | A reminder is a todo with a due date, displayed for the /reminders command
data Reminder = Reminder
{ reminderId :: Int,
@@ -165,27 +205,35 @@ parseReminderTime input = do
[numTxt, unit]
| "hour" `Text.isPrefixOf` unit -> do
n <- readMaybe (Text.unpack numTxt)
- let newTod = addHours n (localTimeOfDay localNow)
- pure (localTimeToUTC easternTimeZone (localNow {localTimeOfDay = newTod}))
+ let (dayOffset, newTod) = addHours n (localTimeOfDay localNow)
+ newDay = Calendar.addDays dayOffset (localDay localNow)
+ pure (localTimeToUTC easternTimeZone (LocalTime newDay newTod))
| "minute" `Text.isPrefixOf` unit -> do
n <- readMaybe (Text.unpack numTxt)
- let newTod = addMinutes n (localTimeOfDay localNow)
- pure (localTimeToUTC easternTimeZone (localNow {localTimeOfDay = newTod}))
+ let (dayOffset, newTod) = addMinutes n (localTimeOfDay localNow)
+ newDay = Calendar.addDays dayOffset (localDay localNow)
+ pure (localTimeToUTC easternTimeZone (LocalTime newDay newTod))
_ -> Nothing
- addHours :: Int -> TimeOfDay -> TimeOfDay
+ -- Add hours to a TimeOfDay, returning (day offset, new TimeOfDay)
+ addHours :: Int -> TimeOfDay -> (Integer, TimeOfDay)
addHours h tod =
let totalMins = todHour tod * 60 + todMin tod + h * 60
- newHour = (totalMins `div` 60) `mod` 24
- newMin = totalMins `mod` 60
- in TimeOfDay newHour newMin (todSec tod)
+ dayOffset = fromIntegral (totalMins `div` (24 * 60))
+ remainingMins = totalMins `mod` (24 * 60)
+ newHour = remainingMins `div` 60
+ newMin = remainingMins `mod` 60
+ in (dayOffset, TimeOfDay newHour newMin (todSec tod))
- addMinutes :: Int -> TimeOfDay -> TimeOfDay
+ -- Add minutes to a TimeOfDay, returning (day offset, new TimeOfDay)
+ addMinutes :: Int -> TimeOfDay -> (Integer, TimeOfDay)
addMinutes m tod =
let totalMins = todHour tod * 60 + todMin tod + m
- newHour = (totalMins `div` 60) `mod` 24
- newMin = totalMins `mod` 60
- in TimeOfDay newHour newMin (todSec tod)
+ dayOffset = fromIntegral (totalMins `div` (24 * 60))
+ remainingMins = totalMins `mod` (24 * 60)
+ newHour = remainingMins `div` 60
+ newMin = remainingMins `mod` 60
+ in (dayOffset, TimeOfDay newHour newMin (todSec tod))
-- Parse "Dec 31 5pm" or "December 31 5:00pm"
parseMonthDayTime :: Calendar.Day -> Text -> Maybe UTCTime