packages feed

fuzzy-time-0.3.0.0: src/Data/FuzzyTime/Resolve.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}

module Data.FuzzyTime.Resolve
  ( -- * Local time
    resolveLocalTimeForwards,
    resolveLocalTimeBackwards,

    -- * Time Of Day
    resolveTimeOfDayForwards,
    resolveTimeOfDayBackwards,
    normaliseTimeOfDay,
    morning,
    evening,

    -- * Day
    resolveDayForwards,
    resolveDayBackwards,

    -- ** Resolution helpers
    nextDayOfMonth,
    previousDayOfMonth,
    nextDayOfMonthOfYear,
    previousDayOfMonthOfYear,
    nextDayOfWeek,
    previousDayOfWeek,
  )
where

import Data.Fixed (Pico, mod')
import Data.FuzzyTime.Types (AmbiguousLocalTime (BothTimeAndDay, OnlyDaySpecified), FuzzyDay (..), FuzzyLocalTime (..), FuzzyTimeOfDay (AtExact, AtHour, AtMinute, Evening, HoursDiff, Midnight, MinutesDiff, Morning, Noon, SameTime, SecondsDiff))
import Data.Time (Day, DayOfWeek, LocalTime (LocalTime), TimeOfDay (TimeOfDay), addDays, midday, midnight, toGregorian)
import Data.Time.Calendar.Month (Month, fromMonthDayValid, fromYearMonthValid, pattern MonthDay)
import Data.Time.Calendar.WeekDate (fromWeekDate, toWeekDate)
import Data.Word (Word8)

resolveLocalTimeForwards :: LocalTime -> FuzzyLocalTime -> Maybe AmbiguousLocalTime
resolveLocalTimeForwards (LocalTime ld ltod) = \case
  FuzzyLocalTimeDay fd -> OnlyDaySpecified <$> resolveDayForwards ld fd
  FuzzyLocalTimeTimeOfDay ftod -> do
    (d, tod) <- resolveTimeOfDayForwardsWithDiff ltod ftod
    pure $ BothTimeAndDay $ LocalTime (addDays d ld) tod
  FuzzyLocalTimeBoth fd ftod -> do
    let withDiff = resolveTimeOfDayForwardsWithDiff ltod ftod
        withoutDiff = (,) 0 <$> resolveTimeOfDayForwards ltod ftod
    (d, tod) <-
      case fd of
        Now -> withDiff
        Today -> withDiff
        _ -> withoutDiff
    day <- addDays d <$> resolveDayForwards ld fd
    pure $ BothTimeAndDay $ LocalTime day tod

resolveLocalTimeBackwards :: LocalTime -> FuzzyLocalTime -> Maybe AmbiguousLocalTime
resolveLocalTimeBackwards (LocalTime ld ltod) = \case
  FuzzyLocalTimeDay fd -> OnlyDaySpecified <$> resolveDayBackwards ld fd
  FuzzyLocalTimeTimeOfDay ftod -> do
    (d, tod) <- resolveTimeOfDayBackwardsWithDiff ltod ftod
    pure $ BothTimeAndDay $ LocalTime (addDays d ld) tod
  FuzzyLocalTimeBoth fd ftod -> do
    let withDiff = resolveTimeOfDayBackwardsWithDiff ltod ftod
        withoutDiff = (,) 0 <$> resolveTimeOfDayBackwards ltod ftod
    (d, tod) <-
      case fd of
        Now -> withDiff
        Today -> withDiff
        _ -> withoutDiff
    day <- addDays d <$> resolveDayBackwards ld fd
    pure $ BothTimeAndDay $ LocalTime day tod

resolveTimeOfDayForwards :: TimeOfDay -> FuzzyTimeOfDay -> Maybe TimeOfDay
resolveTimeOfDayForwards tod ftod = snd <$> resolveTimeOfDayForwardsWithDiff tod ftod

resolveTimeOfDayBackwards :: TimeOfDay -> FuzzyTimeOfDay -> Maybe TimeOfDay
resolveTimeOfDayBackwards tod ftod = snd <$> resolveTimeOfDayBackwardsWithDiff tod ftod

resolveTimeOfDayForwardsWithDiff :: TimeOfDay -> FuzzyTimeOfDay -> Maybe (Integer, TimeOfDay)
resolveTimeOfDayForwardsWithDiff tod@(TimeOfDay h m s) ftod =
  case ftod of
    SameTime -> Just (0, tod)
    Noon -> Just $ next midday
    Midnight -> Just $ next midnight
    Morning -> Just $ next morning
    Evening -> Just $ next evening
    AtHour h_ -> Just $ next $ TimeOfDay h_ 0 0
    AtMinute h_ m_ -> Just $ next $ TimeOfDay h_ m_ 0
    AtExact tod_ -> Just $ next tod_
    HoursDiff hd -> Just $ normaliseTimeOfDay (h + hd) m s
    MinutesDiff md -> Just $ normaliseTimeOfDay h (m + md) s
    SecondsDiff sd -> Just $ normaliseTimeOfDay h m (s + sd)
  where
    next tod_ = (skipIf (>= tod_), tod_)
    skipIf p =
      if p tod
        then 1
        else 0

resolveTimeOfDayBackwardsWithDiff :: TimeOfDay -> FuzzyTimeOfDay -> Maybe (Integer, TimeOfDay)
resolveTimeOfDayBackwardsWithDiff tod@(TimeOfDay h m s) ftod =
  case ftod of
    SameTime -> Just (0, tod)
    Noon -> Just $ previous midday
    Midnight -> Just $ previous midnight
    Morning -> Just $ previous morning
    Evening -> Just $ previous evening
    AtHour h_ -> Just $ previous $ TimeOfDay h_ 0 0
    AtMinute h_ m_ -> Just $ previous $ TimeOfDay h_ m_ 0
    AtExact tod_ -> Just $ previous tod_
    HoursDiff hd -> Just $ normaliseTimeOfDay (h + hd) m s
    MinutesDiff md -> Just $ normaliseTimeOfDay h (m + md) s
    SecondsDiff sd -> Just $ normaliseTimeOfDay h m (s + sd)
  where
    previous tod_ = (skipIf (<= tod_), tod_)
    skipIf p =
      if p tod
        then (-1)
        else 0

normaliseTimeOfDay :: Int -> Int -> Pico -> (Integer, TimeOfDay)
normaliseTimeOfDay h m s =
  let s' = s `mod'` 60
      totalM = m + round (s - s') `div` 60
      m' = totalM `mod` 60
      totalH = h + (totalM - m') `div` 60
      h' = totalH `mod` 24
      totalD = (totalH - h') `div` 24
   in (fromIntegral totalD, TimeOfDay h' m' s')

morning :: TimeOfDay
morning = TimeOfDay 6 0 0

evening :: TimeOfDay
evening = TimeOfDay 18 0 0

resolveDayForwards :: Day -> FuzzyDay -> Maybe Day
resolveDayForwards d fd =
  case fd of
    Yesterday -> Just $ addDays (-1) d
    Now -> Just d
    Today -> Just d
    Tomorrow -> Just $ addDays 1 d
    OnlyDay di -> nextDayOfMonth d di
    DayInMonth mi di -> nextDayOfMonthOfYear d mi di
    DiffDays ds -> Just $ addDays (fromIntegral ds) d
    DiffWeeks ws -> Just $ addDays (7 * fromIntegral ws) d
    DiffMonths ms -> Just $ addDays (30 * fromIntegral ms) d
    DayOfTheWeek dow diff -> Just $ addDays (7 * fromIntegral diff) (nextDayOfWeek d dow)
    ExactDay d_ -> Just d_

resolveDayBackwards :: Day -> FuzzyDay -> Maybe Day
resolveDayBackwards d fd =
  case fd of
    Yesterday -> Just $ addDays (-1) d
    Now -> Just d
    Today -> Just d
    Tomorrow -> Just $ addDays 1 d
    OnlyDay di -> previousDayOfMonth d di
    DayInMonth mi di -> previousDayOfMonthOfYear d mi di
    DiffDays ds -> Just $ addDays (fromIntegral ds) d
    DiffWeeks ws -> Just $ addDays (7 * fromIntegral ws) d
    DiffMonths ms -> Just $ addDays (30 * fromIntegral ms) d
    DayOfTheWeek dow diff -> Just $ addDays (7 * fromIntegral diff) (previousDayOfWeek d dow)
    ExactDay d_ -> Just d_

nextDayOfMonth :: Day -> Word8 -> Maybe Day
nextDayOfMonth = dayOfMonthHelper nextAfterDay succ

previousDayOfMonth :: Day -> Word8 -> Maybe Day
previousDayOfMonth = dayOfMonthHelper previousBeforeDay pred

dayOfMonthHelper ::
  (Day -> Maybe Day -> Maybe Day -> Maybe Day) ->
  (Month -> Month) ->
  Day ->
  Word8 ->
  Maybe Day
dayOfMonthHelper chooser changer d wi =
  let di :: Int
      di = fromIntegral wi
      MonthDay thisMonth _ = d
      guessThisMonth = fromMonthDayValid thisMonth (fromIntegral wi)
      guessOtherMonth = fromMonthDayValid (changer thisMonth) di
   in chooser d guessThisMonth guessOtherMonth

nextDayOfMonthOfYear :: Day -> Word8 -> Word8 -> Maybe Day
nextDayOfMonthOfYear = dayOfMonthOfYearHelper nextAfterDay succ

previousDayOfMonthOfYear :: Day -> Word8 -> Word8 -> Maybe Day
previousDayOfMonthOfYear = dayOfMonthOfYearHelper previousBeforeDay pred

dayOfMonthOfYearHelper ::
  (Day -> Maybe Day -> Maybe Day -> Maybe Day) ->
  (Integer -> Integer) ->
  Day ->
  Word8 ->
  Word8 ->
  Maybe Day
dayOfMonthOfYearHelper chooser changer d mw dw =
  let mi = fromIntegral mw
      di = fromIntegral dw
      (y, _, _) = toGregorian d
      current =
        fromYearMonthValid y mi >>= \m ->
          fromMonthDayValid m di
      other =
        fromYearMonthValid (changer y) mi >>= \m ->
          fromMonthDayValid m di
   in chooser d current other

nextDayOfWeek :: Day -> DayOfWeek -> Day
nextDayOfWeek = dayOfWeekHelper (\d current after -> if current > d then current else after) (addDays 7)

previousDayOfWeek :: Day -> DayOfWeek -> Day
previousDayOfWeek = dayOfWeekHelper (\d current before -> if current < d then current else before) (addDays (-7))

dayOfWeekHelper ::
  (Day -> Day -> Day -> Day) ->
  (Day -> Day) ->
  Day ->
  DayOfWeek ->
  Day
dayOfWeekHelper chooser changer day dow =
  let (y, woy, _) = toWeekDate day
      currentGuess = fromWeekDate y woy (fromEnum dow)
      otherGuess = changer currentGuess
   in chooser day currentGuess otherGuess

nextAfterDay :: Day -> Maybe Day -> Maybe Day -> Maybe Day
nextAfterDay today beforeGuess afterGuess =
  case beforeGuess of
    Just d ->
      if d > today
        then beforeGuess
        else afterGuess
    Nothing -> afterGuess

previousBeforeDay :: Day -> Maybe Day -> Maybe Day -> Maybe Day
previousBeforeDay today afterGuess beforeGuess =
  case afterGuess of
    Just d ->
      if d < today
        then afterGuess
        else beforeGuess
    Nothing -> beforeGuess