content: -- Copyright (c) 2016-present, Facebook, Inc.
  -- All rights reserved.
  --
  -- This source code is licensed under the BSD-style license found in the
  -- LICENSE file in the root directory of this source tree.
  
  
  {-# LANGUAGE GADTs #-}
  {-# LANGUAGE LambdaCase #-}
  {-# LANGUAGE NamedFieldPuns #-}
  {-# LANGUAGE NoRebindableSyntax #-}
  {-# LANGUAGE OverloadedStrings #-}
  
  module Duckling.Time.BG.Rules
    ( rules
    ) where
  
  import Control.Applicative ((<|>))
  import Data.Maybe
  import Data.Text (Text)
  import Prelude
  import qualified Data.Text as Text
  
  import Duckling.Dimensions.Types
  import Duckling.Duration.Helpers (duration)
  import Duckling.Duration.Types (DurationData (..))
  import Duckling.Numeral.Helpers (isNatural, parseInt)
  import Duckling.Numeral.Types (NumeralData (..))
  import Duckling.Ordinal.Types (OrdinalData (..))
  import Duckling.Regex.Types
  import Duckling.Time.Computed
  import Duckling.Time.Helpers
  import Duckling.Time.Types (TimeData (..), TimeIntervalType (..))
  import Duckling.Types
  import qualified Duckling.Duration.Types as TDuration
  import qualified Duckling.Numeral.Types as TNumeral
  import qualified Duckling.Ordinal.Types as TOrdinal
  import qualified Duckling.Time.Types as TTime
  import qualified Duckling.TimeGrain.Types as TG
  
  ruleIntersect :: Rule
  ruleIntersect = Rule
    { name = "intersect"
    , pattern =
      [ Predicate $ and . sequence [isNotLatent, isGrainFinerThan TG.Year]
      , Predicate $ or . sequence [isNotLatent, isGrainOfTime TG.Year]
      ]
    , prod = \case
        (Token Time td1:Token Time td2:_) ->
          Token Time <$> intersect td1 td2
        _ -> Nothing
    }
  
  ruleIntersectOf :: Rule
  ruleIntersectOf = Rule
    { name = "intersect by \",\", \"of\", \"from\", \"'s\""
    , pattern =
      [ Predicate isNotLatent
      , regex "от|за|на|,"
      , Predicate isNotLatent
      ]
    , prod = \case
        (Token Time td1:_:Token Time td2:_) ->
          Token Time . notLatent <$> intersect td1 td2
        _ -> Nothing
    }
  
  ruleIntersectYear :: Rule
  ruleIntersectYear = Rule
    { name = "intersect by \",\", \"of\", \"from\" for year"
    , pattern =
      [ Predicate isNotLatent
      , regex "на|,"
      , Predicate $ isGrainOfTime TG.Year
      ]
    , prod = \case
        (Token Time td1:_:Token Time td2:_) ->
          Token Time . notLatent <$> intersect td1 td2
        _ -> Nothing
    }
  
  ruleAbsorbOnDay :: Rule
  ruleAbsorbOnDay = Rule
    { name = "on <day>"
    , pattern =
      [ regex "на"
      , Predicate $ isGrainOfTime TG.Day
      ]
    , prod = \case
        (_:token:_) -> Just token
        _ -> Nothing
    }
  
  ruleAbsorbOnADOW :: Rule
  ruleAbsorbOnADOW = Rule
    { name = "on a <named-day>"
    , pattern =
      [ regex "в"
      , Predicate isADayOfWeek
      ]
    , prod = \case
        (_:token:_) -> Just token
        _ -> Nothing
    }
  
  ruleAbsorbInMonthYear :: Rule
  ruleAbsorbInMonthYear = Rule
    { name = "in|during <named-month>|year"
    , pattern =
      [ regex "в|през"
      , Predicate $ or . sequence [isAMonth, isGrainOfTime TG.Year]
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ notLatent td
        _ -> Nothing
    }
  
  ruleAbsorbCommaTOD :: Rule
  ruleAbsorbCommaTOD = Rule
    { name = "absorption of , after named day"
    , pattern =
      [ Predicate isADayOfWeek
      , regex ","
      ]
    , prod = \case
        (token:_) -> Just token
        _ -> Nothing
    }
  
  ruleInstants :: [Rule]
  ruleInstants = mkRuleInstants
    [ ("right now"        , TG.Second, 0  , "((точно\\s+)?сега)|веднага")
    , ("today"            , TG.Day   , 0  , "днес|(по това време)"      )
    , ("tomorrow"         , TG.Day   , 1  , "утре"                      )
    , ("yesterday"        , TG.Day   , - 1, "вчера"                     )
    , ( "after tomorrow"  , TG.Day   , 2  , "(в\\s*)?другиден"          )
    , ( "before yesterday", TG.Day   , - 2, "(оня ден)|завчера"         )
    ]
  
  ruleNow :: Rule
  ruleNow = Rule
    { name = "now"
    , pattern =
      [ regex "сега"
      ]
    , prod = \_ -> tt now
    }
  
  ruleNextDOW :: Rule
  ruleNextDOW = Rule
    { name = "this|next <day-of-week>"
    , pattern =
      [ regex "(т(о|а)зи)|следващ((ия(т)?)|ата)"
      , Predicate isADayOfWeek
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ predNth 0 True td
        _ -> Nothing
    }
  
  ruleThisTime :: Rule
  ruleThisTime = Rule
    { name = "this <time>"
    , pattern =
      [ regex "(т(а|о)зи)|това"
      , Predicate isOkWithThisNext
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ predNth 0 False td
        _ -> Nothing
    }
  
  ruleNextTime :: Rule
  ruleNextTime = Rule
    { name = "next <time>"
    , pattern =
      [ regex "следващ((ия(т)?)|ата|ото)"
      , Predicate isOkWithThisNext
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ predNth 0 True td
        _ -> Nothing
    }
  
  ruleLastTime :: Rule
  ruleLastTime = Rule
    { name = "last <time>"
    , pattern =
      [ regex "((пред(н|ишн))|минал)((ия(т)?)|ата|ото)"
      , Predicate isOkWithThisNext
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ predNth (- 1) False td
        _ -> Nothing
    }
  
  ruleLastWeekendOfMonth :: Rule
  ruleLastWeekendOfMonth = Rule
    { name = "last weekend of <named-month>"
    , pattern =
      [ regex "последния(т)? уикенд\\s+(през|на)"
      , Predicate isAMonth
      ]
    , prod = \case
        (_:Token Time td2:_) -> tt $ predLastOf weekend td2
        _ -> Nothing
    }
  
  ruleTimeBeforeLast :: Rule
  ruleTimeBeforeLast = Rule
    { name = "<time> before last"
    , pattern =
      [ regex "по(\\-|\\s+)((пред(н|ишн))|минал)((ия(т)?)|ата|ото)"
      , dimension Time
      ]
    , prod = \case
      (_:Token Time td:_) -> tt $ predNth (- 2) False td
      _ -> Nothing
    }
  
  ruleTimeAfterNext :: Rule
  ruleTimeAfterNext = Rule
    { name = "<time> after next"
    , pattern =
      [ regex "по(\\-|\\s+)следващ((ия(т)?)|ата|ото)"
      , dimension Time
      ]
    , prod = \case
      (_:Token Time td:_) -> tt $ predNth 1 True td
      _ -> Nothing
    }
  
  ruleLastDOWOfTime :: Rule
  ruleLastDOWOfTime = Rule
    { name = "last <day-of-week> of <time>"
    , pattern =
      [ regex "последн((ия(т)?)|ата)"
      , Predicate isADayOfWeek
      , regex "на"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td1:_:Token Time td2:_) ->
          tt $ predLastOf td1 td2
        _ -> Nothing
    }
  
  ruleLastCycleOfTime :: Rule
  ruleLastCycleOfTime = Rule
    { name = "last <cycle> of <time>"
    , pattern =
      [ regex "последн((ия(т)?)|ото|ата)"
      , dimension TimeGrain
      , regex "на|в|през"
      , dimension Time
      ]
    , prod = \case
        (_:Token TimeGrain grain:_:Token Time td:_) ->
          tt $ cycleLastOf grain td
        _ -> Nothing
    }
  
  ruleLastNight :: Rule
  ruleLastNight = Rule
    { name = "last night"
    , pattern =
      [ regex "(късно )?снощи"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) ->
          let hours = if Text.toLower match == "късно " then 3 else 6
              start = durationBefore (DurationData hours TG.Hour) today
          in Token Time . partOfDay . notLatent <$> interval TTime.Open start today
        _ -> Nothing
    }
  
  ruleNthTimeOfTime :: Rule
  ruleNthTimeOfTime = Rule
    { name = "nth <time> of <time>"
    , pattern =
      [ dimension Ordinal
      , dimension Time
      , regex "на|в|през"
      , dimension Time
      ]
    , prod = \case
        (Token Ordinal od:Token Time td1:_:Token Time td2:_) -> Token Time .
          predNth (TOrdinal.value od - 1) False <$> intersect td2 td1
        _ -> Nothing
    }
  
  ruleNthTimeAfterTime :: Rule
  ruleNthTimeAfterTime = Rule
    { name = "nth <time> after <time>"
    , pattern =
      [ dimension Ordinal
      , dimension Time
      , regex "след|от"
      , dimension Time
      ]
    , prod = \case
        (Token Ordinal od:Token Time td1:_:Token Time td2:_) ->
          tt $ predNthAfter (TOrdinal.value od - 1) td1 td2
        _ -> Nothing
    }
  
  ruleNDOWFromTime :: Rule
  ruleNDOWFromTime = Rule
    { name = "<integer> <day-of-week> from <time>"
    , pattern =
      [ dimension Numeral
      , Predicate isADayOfWeek
      , regex "от"
      , dimension Time
      ]
    , prod = \case
        (token:Token Time td1:_:Token Time td2:_) -> do
          n <- getIntValue token
          tt $ predNthAfter (n - 1) td1 td2
        _ -> Nothing
    }
  
  ruleYearLatent :: Rule
  ruleYearLatent = Rule
    { name = "year (latent)"
    , pattern =
        [ Predicate $ isIntegerBetween 25 10000
        ]
    , prod = \case
        (token:_) -> do
          n <- getIntValue token
          tt . mkLatent $ year n
        _ -> Nothing
    }
  
  ruleYearADBC :: Rule
  ruleYearADBC = Rule
    { name = "<year> (bc|ad)"
    , pattern =
      [ Predicate $ isIntegerBetween 1 10000
      , regex "(пр|сл)\\.?\\s+(Хр\\.?|н\\.?\\e.?)"
      ]
    , prod = \case
      (token:Token RegexMatch (GroupMatch (ab:_)):_) -> do
        y <- getIntValue token
        tt . yearADBC $ if Text.head (Text.toLower ab) == 'п' then -y else y
      _ -> Nothing
    }
  
  ruleDOMLatent :: Rule
  ruleDOMLatent = Rule
    { name = "<day-of-month> (ordinal)"
    , pattern = [Predicate isDOMOrdinal]
    , prod = \case
        (token:_) -> do
          n <- getIntValue token
          tt . mkLatent $ dayOfMonth n
        _ -> Nothing
    }
  
  ruleTheDOMNumeral :: Rule
  ruleTheDOMNumeral = Rule
    { name = "the <day-of-month> (number)"
    , pattern =
      [ Predicate isDOMInteger
      ]
    , prod = \case
        (token:_) -> do
          n <- getIntValue token
          tt . mkLatent $ dayOfMonth n
        _ -> Nothing
    }
  
  ruleTheDOMOrdinal :: Rule
  ruleTheDOMOrdinal = Rule
    { name = "the <day-of-month> (ordinal)"
    , pattern =
      [ Predicate isDOMOrdinal
      ]
    , prod = \case
        (Token Ordinal OrdinalData{TOrdinal.value = v}:
         _) -> tt $ dayOfMonth v
        _ -> Nothing
    }
  
  ruleNamedDOMOrdinal :: Rule
  ruleNamedDOMOrdinal = Rule
    { name = "<named-month>|<named-day> <day-of-month> (ordinal)"
    , pattern =
      [ Predicate $ or . sequence [isAMonth, isADayOfWeek]
      , Predicate isDOMOrdinal
      ]
    , prod = \case
        (Token Time td:token:_) -> Token Time <$> intersectDOM td token
        _ -> Nothing
    }
  
  ruleMonthDOMNumeral :: Rule
  ruleMonthDOMNumeral = Rule
    { name = "<named-month> <day-of-month> (non ordinal)"
    , pattern =
      [ Predicate isAMonth
      , Predicate isDOMInteger
      ]
    , prod = \case
        (Token Time td:token:_) -> Token Time <$> intersectDOM td token
        _ -> Nothing
    }
  
  ruleDOMMonth :: Rule
  ruleDOMMonth = Rule
    { name = "<day-of-month> (ordinal or number) <named-month>"
    , pattern =
      [ Predicate isDOMValue
      , Predicate isAMonth
      ]
    , prod = \case
        (token:Token Time td:_) -> Token Time <$> intersectDOM td token
        _ -> Nothing
    }
  
  ruleDOMMonthYear :: Rule
  ruleDOMMonthYear = Rule
    { name = "<day-of-month>(ordinal or number)/<named-month>/year"
    , pattern =
      [ Predicate isDOMValue
      , regex "[-/\\s]"
      , Predicate isAMonth
      , regex "[-/\\s]"
      , regex "(\\d{4})"
      ]
    , prod = \case
        (token:
         _:
         Token Time td:
         _:
         Token RegexMatch (GroupMatch (match:_)):
         _) -> do
           intVal <- parseInt match
           dom <- intersectDOM td token
           Token Time <$> intersect dom (year intVal)
        _ -> Nothing
    }
  
  ruleDOMOrdinalMonthYear :: Rule
  ruleDOMOrdinalMonthYear = Rule
    { name = "<day-of-month>(ordinal) <named-month> year"
    , pattern =
      [ Predicate isDOMOrdinal
      , Predicate isAMonth
      , regex "(\\d{2,4})"
      ]
    , prod = \case
        (token:Token Time td:Token RegexMatch (GroupMatch (match:_)):_) -> do
          intVal <- parseInt match
          dom <- intersectDOM td token
          Token Time <$> intersect dom (year intVal)
        _ -> Nothing
    }
  
  ruleMonthYear :: Rule
  ruleMonthYear = Rule
    { name = "<named-month> year"
    , pattern =
      [ Predicate isAMonth
      , regex "(\\d{2,4})"
      ]
    , prod = \case
        (Token Time td:Token RegexMatch (GroupMatch (match:_)):_) -> do
          intVal <- parseInt match
          Token Time <$> intersect td (year intVal)
        _ -> Nothing
    }
  
  ruleIdesOfMonth :: Rule
  ruleIdesOfMonth = Rule
    { name = "the ides of <named-month>"
    , pattern =
      [ regex "средата на"
      , Predicate isAMonth
      ]
    , prod = \case
        (_:Token Time td@TimeData {TTime.form = Just (TTime.Month m)}:_) ->
          Token Time <$>
            intersect td (dayOfMonth $ if elem m [3, 5, 7, 10] then 15 else 13)
        _ -> Nothing
    }
  
  ruleTODLatent :: Rule
  ruleTODLatent = Rule
    { name = "time-of-day (latent)"
    , pattern =
      [ Predicate $ isIntegerBetween 0 23
      ]
    , prod = \case
        (token:_) -> do
          n <- getIntValue token
          tt . mkLatent $ hour (n < 13) n
        _ -> Nothing
    }
  
  ruleAtTOD :: Rule
  ruleAtTOD = Rule
    { name = "at <time-of-day>"
    , pattern =
      [ regex "в"
      , Predicate isATimeOfDay
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ notLatent td
        _ -> Nothing
    }
  
  ruleTODOClock :: Rule
  ruleTODOClock = Rule
    { name = "<time-of-day> o'clock"
    , pattern =
      [ Predicate isATimeOfDay
      , regex "часа"
      ]
    , prod = \case
        (Token Time td:_) -> tt $ notLatent td
        _ -> Nothing
    }
  
  ruleHHMM :: Rule
  ruleHHMM = Rule
    { name = "hh:mm"
    , pattern =
      [ regex "((?:[01]?\\d)|(?:2[0-3]))[:.]([0-5]\\d)"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (hh:mm:_)):_) -> do
          h <- parseInt hh
          m <- parseInt mm
          tt $ hourMinute False h m
        _ -> Nothing
    }
  
  ruleHHhMM :: Rule
  ruleHHhMM = Rule
    { name = "hhhmm"
    , pattern =
      [ regex "(?<!/)((?:[01]?\\d)|(?:2[0-3]))ч(([0-5]\\d)|(?!\\d))"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (hh:mm:_)):_) -> do
          h <- parseInt hh
          m <- parseInt mm <|> Just 0
          tt $ hourMinute False h m
        _ -> Nothing
    }
  
  ruleHHMMLatent :: Rule
  ruleHHMMLatent = Rule
    { name = "hhmm (latent)"
    , pattern =
      [ regex "((?:[01]?\\d)|(?:2[0-3]))([0-5]\\d)(?!.\\d)"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (hh:mm:_)):_) -> do
          h <- parseInt hh
          m <- parseInt mm
          tt . mkLatent $ hourMinute False h m
        _ -> Nothing
    }
  
  ruleHHMMSS :: Rule
  ruleHHMMSS = Rule
    { name = "hh:mm:ss"
    , pattern =
      [ regex "((?:[01]?\\d)|(?:2[0-3])):([0-5]\\d):([0-5]\\d)"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (hh:mm:ss:_)):_) -> do
          h <- parseInt hh
          m <- parseInt mm
          s <- parseInt ss
          tt $ hourMinuteSecond False h m s
        _ -> Nothing
    }
  
  ruleTODAM :: Rule
  ruleTODAM = Rule
    { name = "<time-of-day> am"
    , pattern =
      [ Predicate isATimeOfDay
      , regex "сутрин(та)?"
      ]
    , prod = \case
        (Token Time td:_) -> tt $ timeOfDayAMPM True td
        _ -> Nothing
    }
  
  ruleTODPM :: Rule
  ruleTODPM = Rule
    { name = "<time-of-day> pm"
    , pattern =
      [ Predicate isATimeOfDay
      , regex "вечер(та)?|след об(е|я)д"
      ]
    , prod = \case
        (Token Time td:_) -> tt $ timeOfDayAMPM False td
        _ -> Nothing
    }
  
  ruleHONumeral :: Rule
  ruleHONumeral = Rule
    { name = "<hour-of-day> <integer>"
    , pattern =
      [ Predicate isAnHourOfDay
      , Predicate $ isIntegerBetween 1 59
      ]
    , prod = \case
        (Token Time TimeData{TTime.form = Just (TTime.TimeOfDay (Just hours) is12H)
                            ,TTime.latent = isLatent}:
         token:
         _) -> do
          n <- getIntValue token
          if isLatent
            then tt . mkLatent $ hourMinute is12H hours n
            else tt $ hourMinute is12H hours n
        _ -> Nothing
    }
  
  ruleHODHalf :: Rule
  ruleHODHalf = Rule
    { name = "<hour-of-day> half"
    , pattern =
      [ Predicate isAnHourOfDay
      , regex "и половина"
      ]
    , prod = \case
        (Token Time TimeData{TTime.form = Just (TTime.TimeOfDay (Just hours) is12H)}:
         _) -> tt $ hourMinute is12H hours 30
        _ -> Nothing
    }
  
  ruleHODQuarter :: Rule
  ruleHODQuarter = Rule
    { name = "<hour-of-day> quarter"
    , pattern =
      [ Predicate isAnHourOfDay
      , regex "и (четвърт|(петнайсе(т)?))"
      ]
    , prod = \case
        (Token Time TimeData{TTime.form = Just (TTime.TimeOfDay (Just hours) is12H)}:
         _) -> tt $ hourMinute is12H hours 15
        _ -> Nothing
    }
  
  ruleNumeralToHOD :: Rule
  ruleNumeralToHOD = Rule
    { name = "<integer> to|till|before <hour-of-day>"
    , pattern =
      [ Predicate isAnHourOfDay
      , regex "без"
      , Predicate $ isIntegerBetween 1 59
      ]
    , prod = \case
        (Token Time td:_:token:_) -> do
          n <- getIntValue token
          Token Time <$> minutesBefore n td
        _ -> Nothing
    }
  
  ruleQuarterToHOD :: Rule
  ruleQuarterToHOD = Rule
    { name = "quarter to|till|before <hour-of-day>"
    , pattern =
      [ Predicate isAnHourOfDay
      , regex "без (четвърт|(петнайсе(т)?))"
      ]
    , prod = \case
        (Token Time td:_:_) -> Token Time <$> minutesBefore 15 td
        _ -> Nothing
    }
  
  ruleNumeralAfterHOD :: Rule
  ruleNumeralAfterHOD = Rule
    { name = "integer after|past <hour-of-day>"
    , pattern =
      [ Predicate isAnHourOfDay
      , regex "и"
      , Predicate $ isIntegerBetween 1 59
      ]
    , prod = \case
        (Token Time td:_:token:_) -> do
          n <- getIntValue token
          Token Time <$> minutesAfter n td
        _ -> Nothing
    }
  
  ruleMMYYYY :: Rule
  ruleMMYYYY = Rule
    { name = "mm/yyyy"
    , pattern =
      [ regex "(0?[1-9]|1[0-2])[/-](\\d{4})"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (mm:yy:_)):_) -> do
          y <- parseInt yy
          m <- parseInt mm
          tt $ yearMonth y m
        _ -> Nothing
    }
  
  ruleYYYYMM :: Rule
  ruleYYYYMM = Rule
    { name = "yyyy-mm"
    , pattern =
      [ regex "(\\d{4})\\s*[/-]\\s*(1[0-2]|0?[1-9])"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (yy:mm:_)):_) -> do
          y <- parseInt yy
          m <- parseInt mm
          tt $ yearMonth y m
        _ -> Nothing
    }
  
  ruleYYYYMMDD :: Rule
  ruleYYYYMMDD = Rule
    { name = "yyyy-mm-dd"
    , pattern =
      [ regex "(\\d{2,4})-(0?[1-9]|1[0-2])-(3[01]|[12]\\d|0?[1-9])"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (yy:mm:dd:_)):_) -> do
          y <- parseInt yy
          m <- parseInt mm
          d <- parseInt dd
          tt $ yearMonthDay y m d
        _ -> Nothing
    }
  
  ruleDDMMYYYYDot :: Rule
  ruleDDMMYYYYDot = Rule
    { name = "dd.mm.yyyy"
    , pattern =
      [ regex "(3[01]|[12]\\d|0?[1-9])\\.(1[0-2]|0?[1-9])\\.(\\d{2,4})"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (dd:mm:yy:_)):_) -> do
          y <- parseInt yy
          m <- parseInt mm
          d <- parseInt dd
          tt $ yearMonthDay y m d
        _ -> Nothing
    }
  
  ruleYYYYQQ :: Rule
  ruleYYYYQQ = Rule
    { name = "yyyyqq"
    , pattern =
      [ regex "(\\d{2,4})q([1-4])"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (yy:qq:_)):_) -> do
          y <- parseInt yy
          q <- parseInt qq
          tt . cycleNthAfter True TG.Quarter (q - 1) $ year y
        _ -> Nothing
    }
  
  ruleNoonMidnightEOD :: Rule
  ruleNoonMidnightEOD = Rule
    { name = "noon|midnight|EOD|end of day"
    , pattern =
      [ regex "(обед|обяд|полунощ|((в )?(края на деня)))"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) -> tt . hour False $
          if (Text.toLower match == "обед" || Text.toLower match == "обяд")  then 12 else 0
        _ -> Nothing
    }
  
  rulePartOfDays :: Rule
  rulePartOfDays = Rule
    { name = "part of days"
    , pattern =
      [ regex "(сутрин(та)?|(по|след) ?об(е|я)д|вечер(та)?|(през )?нощ(та)?)"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) -> do
          let (start, end) = case Text.toLower match of
                "сутрин"  -> (hour False 4, hour False 12)
                "сутринта"  -> (hour False 4, hour False 12)
                "вечер"  -> (hour False 18, hour False 0)
                "вечерта"  -> (hour False 18, hour False 0)
                "нощ"    -> (hour False 0, hour False 4)
                "нощта"    -> (hour False 0, hour False 4)
                "през нощта"    -> (hour False 0, hour False 4)
                "обед"    -> (hour False 12, hour False 14)
                "по обед" -> (hour False 12, hour False 14)
                "обяд"    -> (hour False 12, hour False 14)
                "по обяд" -> (hour False 12, hour False 14)
                _          -> (hour False 12, hour False 19)
          td <- interval TTime.Open start end
          tt . partOfDay $ mkLatent td
        _ -> Nothing
    }
  
  ruleEarlyMorning :: Rule
  ruleEarlyMorning = Rule
    { name = "early morning"
    , pattern =
      [ regex "рано сутрин(та)?"
      ]
    , prod = \_ -> Token Time . partOfDay . mkLatent <$>
        interval TTime.Open (hour False 4) (hour False 9)
    }
  
  rulePODThis :: Rule
  rulePODThis = Rule
    { name = "this <part-of-day>"
    , pattern =
      [ regex "(т(о|а)зи)"
      , Predicate isAPartOfDay
      ]
    , prod = \case
        (_:Token Time td:_) -> Token Time . partOfDay . notLatent <$>
          intersect today td
        _ -> Nothing
    }
  
  ruleTonight :: Rule
  ruleTonight = Rule
    { name = "tonight"
    , pattern =
      [ regex "(късно )?вечер(та)?"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) -> do
          let h = if Text.toLower match == "късно " then 21 else 18
          evening <- interval TTime.Open (hour False h) (hour False 0)
          Token Time . partOfDay . notLatent <$> intersect today evening
        _ -> Nothing
    }
  
  ruleAfterPartofday :: Rule
  ruleAfterPartofday = Rule
    { name = "after lunch/work/school"
    , pattern =
      [ regex "след[\\s*]?((об(е|я)д)|работа|училище)"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) -> do
          (start, end) <- case Text.toLower match of
            "обед"  -> Just (hour False 13, hour False 17)
            "обяд"  -> Just (hour False 13, hour False 17)
            "работа"   -> Just (hour False 17, hour False 21)
            "училище" -> Just (hour False 15, hour False 21)
            _        -> Nothing
          td <- interval TTime.Open start end
          Token Time . partOfDay . notLatent <$> intersect today td
        _ -> Nothing
    }
  
  -- Since part of days are latent, general time intersection is blocked
  ruleTimePOD :: Rule
  ruleTimePOD = Rule
    { name = "<time> <part-of-day>"
    , pattern =
      [ dimension Time
      , Predicate isAPartOfDay
      ]
    , prod = \case
        (Token Time td:Token Time pod:_) -> Token Time <$> intersect pod td
        _ -> Nothing
    }
  
  rulePODofTime :: Rule
  rulePODofTime = Rule
    { name = "<part-of-day> of <time>"
    , pattern =
      [ Predicate isAPartOfDay
      , regex "на"
      , dimension Time
      ]
    , prod = \case
        (Token Time pod:_:Token Time td:_) -> Token Time <$> intersect pod td
        _ -> Nothing
    }
  
  ruleWeekend :: Rule
  ruleWeekend = Rule
    { name = "week-end"
    , pattern =
      [ regex "(края на седмицата)|(уикенд(а)?)"
      ]
    , prod = \_ -> tt $ mkOkForThisNext weekend
    }
  
  ruleWeek :: Rule
  ruleWeek = Rule
   { name = "week"
   , pattern = [regex "(цяла|(остатъка на( тази)?)) седмица(та)?"]
   , prod = \case
       (Token RegexMatch (GroupMatch (match:_)):_) ->
         let end = cycleNthAfter True TG.Day (-2) $ cycleNth TG.Week 1
             period = case Text.toLower match of
                        "цяла" -> interval Closed (cycleNth TG.Week 0) end
                        "остатъка на" -> interval Open today end
                        "остатъка на тази" -> interval Open today end
                        _ -> Nothing
         in case Text.toLower match of
           "тази" -> Token Time . mkLatent <$> period
           _ -> Token Time <$> period
       _ -> Nothing
   }
  
  ruleSeason :: Rule
  ruleSeason = Rule
    { name = "last|this|next <season>"
    , pattern =
      [ regex "(това|тази|(по\\-)?(по\\-)?((настоящ|следващ|друг|последн|минал|предн)(ото|ата))) seasons?"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) -> do
          n <- case Text.toLower match of
                 "това" -> Just 0
                 "тази" -> Just 0
                 "настоящото" -> Just 0
                 "настоящата" -> Just 0
                 "миналото" -> Just (-1)
                 "миналата" -> Just (-1)
                 "последното" -> Just (-1)
                 "последната" -> Just (-1)
                 "предното" -> Just (-1)
                 "предната" -> Just (-1)
                 "следващото" -> Just 1
                 "следващата" -> Just 1
                 "другото" -> Just 1
                 "другата" -> Just 1
                 "по-миналото" -> Just (-2)
                 "по-миналата" -> Just (-2)
                 "по-последното" -> Just (-2)
                 "по-последната" -> Just (-2)
                 "по-предното" -> Just (-2)
                 "по-предната" -> Just (-2)
                 "по-следващото" -> Just 2
                 "по-следващата" -> Just 2
                 "по-другото" -> Just 2
                 "по-другата" -> Just 2
                 "по-по-миналото" -> Just (-3)
                 "по-по-миналата" -> Just (-3)
                 "по-по-последното" -> Just (-3)
                 "по-по-последната" -> Just (-3)
                 "по-по-предното" -> Just (-3)
                 "по-по-предната" -> Just (-3)
                 "по-по-следващото" -> Just 3
                 "по-по-следващата" -> Just 3
                 "по-по-другото" -> Just 3
                 "по-по-другата" -> Just 3
                 _ -> Nothing
          tt $ predNth n False season
        _ -> Nothing
    }
  
  ruleSeasons :: [Rule]
  ruleSeasons = mkRuleSeasons
    [ ( "summer", "лято"     , monthDay  6 21, monthDay  9 23 )
    , ( "fall"  , "есен"     , monthDay  9 23, monthDay 12 21 )
    , ( "winter", "зима"     , monthDay 12 21, monthDay  3 20 )
    , ( "spring", "пролет"   , monthDay  3 20, monthDay  6 21 )
    ]
  
  ruleTODPrecision :: Rule
  ruleTODPrecision = Rule
    { name = "<time-of-day> sharp|exactly"
    , pattern =
      [ regex "(в )?точно( в )?"
      , Predicate isATimeOfDay
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ notLatent td
        _ -> Nothing
    }
  
  rulePrecisionTOD :: Rule
  rulePrecisionTOD = Rule
    { name = "about|exactly <time-of-day>"
    , pattern =
      [ regex "около"
      , Predicate $ isGrainFinerThan TG.Year
      ]
    , prod = \case
        (_:Token Time td:_) -> tt $ notLatent td
        _ -> Nothing
    }
  
  ruleIntervalMonthDDDD :: Rule
  ruleIntervalMonthDDDD = Rule
    { name = "<month> dd-dd (interval)"
    , pattern =
      [ Predicate isAMonth
      , Predicate isDOMValue
      , regex "\\-|до"
      , Predicate isDOMValue
      ]
    , prod = \case
        (Token Time td:
         token1:
         _:
         token2:
         _) -> do
          dom1 <- intersectDOM td token1
          dom2 <- intersectDOM td token2
          Token Time <$> interval TTime.Closed dom1 dom2
        _ -> Nothing
    }
  
  ruleIntervalDDDDMonth :: Rule
  ruleIntervalDDDDMonth = Rule
    { name = "dd-dd <month> (interval)"
    , pattern =
      [ Predicate isDOMValue
      , regex "\\-|до"
      , Predicate isDOMValue
      , Predicate isAMonth
      ]
    , prod = \case
        (token1:
         _:
         token2:
         Token Time td:
         _) -> do
          dom1 <- intersectDOM td token1
          dom2 <- intersectDOM td token2
          Token Time <$> interval TTime.Closed dom1 dom2
        _ -> Nothing
    }
  
  ruleIntervalFromDDDDMonth :: Rule
  ruleIntervalFromDDDDMonth = Rule
    { name = "from the <day-of-month> (ordinal or number) to the <day-of-month> (ordinal or number) <named-month> (interval)"
    , pattern =
      [ regex "от"
      , Predicate isDOMValue
      , regex "\\-|до"
      , Predicate isDOMValue
      , Predicate isAMonth
      ]
    , prod = \case
        (_:
         token1:
         _:
         token2:
         Token Time td:
         _) -> do
          dom1 <- intersectDOM td token1
          dom2 <- intersectDOM td token2
          Token Time <$> interval TTime.Closed dom1 dom2
        _ -> Nothing
    }
  
  -- Blocked for :latent time. May need to accept certain latents only, like hours
  ruleIntervalDash :: Rule
  ruleIntervalDash = Rule
    { name = "<datetime> - <datetime> (interval)"
    , pattern =
      [ Predicate isNotLatent
      , regex "\\-|до"
      , Predicate isNotLatent
      ]
    , prod = \case
        (Token Time td1:_:Token Time td2:_) ->
          Token Time <$> interval TTime.Closed td1 td2
        _ -> Nothing
    }
  
  ruleIntervalSlash :: Rule
  ruleIntervalSlash = Rule
    { name = "<datetime>/<datetime> (interval)"
    , pattern =
      [ Predicate isNotLatent
      , regex "/"
      , Predicate isNotLatent
      ]
    , prod = \case
        (Token Time td1:_:Token Time td2:_) ->
          if sameGrain td1 td2 then
            Token Time <$> interval TTime.Closed td1 td2
          else Nothing
        _ -> Nothing
    }
  
  ruleIntervalFrom :: Rule
  ruleIntervalFrom = Rule
    { name = "from <datetime> - <datetime> (interval)"
    , pattern =
      [ regex "от"
      , dimension Time
      , regex "до"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td1:_:Token Time td2:_) ->
          Token Time <$> interval TTime.Closed td1 td2
        _ -> Nothing
    }
  
  ruleIntervalBetween :: Rule
  ruleIntervalBetween = Rule
    { name = "between <time> and <time>"
    , pattern =
      [ regex "между"
      , dimension Time
      , regex "и"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td1:_:Token Time td2:_) ->
          Token Time <$> interval TTime.Closed td1 td2
        _ -> Nothing
    }
  
  -- Specific for time-of-day, to help resolve ambiguities
  ruleIntervalTODDash :: Rule
  ruleIntervalTODDash = Rule
    { name = "<time-of-day> - <time-of-day> (interval)"
    , pattern =
      [ Predicate $ and . sequence [isNotLatent, isATimeOfDay]
      , regex "\\-|до"
      , Predicate isATimeOfDay
      ]
    , prod = \case
        (Token Time td1:_:Token Time td2:_) ->
          Token Time <$> interval TTime.Closed td1 td2
        _ -> Nothing
    }
  
  ruleIntervalTODBetween :: Rule
  ruleIntervalTODBetween = Rule
    { name = "between <time-of-day> and <time-of-day> (interval)"
    , pattern =
      [ regex "между"
      , Predicate isATimeOfDay
      , regex "и"
      , Predicate isATimeOfDay
      ]
    , prod = \case
        (_:Token Time td1:_:Token Time td2:_) ->
          Token Time <$> interval TTime.Closed td1 td2
        _ -> Nothing
    }
  
  ruleIntervalBy :: Rule
  ruleIntervalBy = Rule
    { name = "by <time>"
    , pattern =
      [ regex "до"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td:_) -> Token Time <$> interval TTime.Open now td
        _ -> Nothing
    }
  
  ruleIntervalByTheEndOf :: Rule
  ruleIntervalByTheEndOf = Rule
    { name = "by the end of <time>"
    , pattern =
      [ regex "до края на"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td:_) -> Token Time <$> interval TTime.Closed now td
        _ -> Nothing
    }
  
  ruleIntervalUntilTime :: Rule
  ruleIntervalUntilTime = Rule
    { name = "until <time>"
    , pattern =
      [ regex "преди|до"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td:_) -> tt . withDirection TTime.Before $ notLatent td
        _ -> Nothing
    }
  
  ruleIntervalAfterFromSinceTime :: Rule
  ruleIntervalAfterFromSinceTime = Rule
    { name = "from|since|after <time>"
    , pattern =
      [ regex "след|от"
      , dimension Time
      ]
    , prod = \case
        (_:Token Time td:_) -> tt . withDirection TTime.After $ notLatent td
        _ -> Nothing
    }
  
  ruleDaysOfWeek :: [Rule]
  ruleDaysOfWeek = mkRuleDaysOfWeek
    [ ( "Monday"   , "понеделник|пон\\.?|пн\\.?" )
    , ( "Tuesday"  , "вторник|вт\\.?"            )
    , ( "Wednesday", "сряда|ср\\.?"              )
    , ( "Thursday" , "четвъртък|четв\\.?|чт\\.?" )
    , ( "Friday"   , "петък|пет\\.?|пт\\.?"      )
    , ( "Saturday" , "събота|съб\\.?|сб\\.?"     )
    , ( "Sunday"   , "неделя|нед\\.?|нд\\.?"     )
    ]
  
  ruleMonths :: [Rule]
  ruleMonths = mkRuleMonths
    [ ( "January"  , "януари|ян\\.?"     )
    , ( "February" , "февруари|февр\\.?" )
    , ( "March"    , "март"              )
    , ( "April"    , "април|апр\\.?"     )
    , ( "May"      , "май"               )
    , ( "June"     , "юни"               )
    , ( "July"     , "юли"               )
    , ( "August"   , "август|авг\\.?"    )
    , ( "September", "септември|септ\\.?")
    , ( "October"  , "октомври|окт\\.?"  )
    , ( "November" , "ноември|ноем\\.?"  )
    , ( "December" , "декември|дек\\.?"  )
    ]
  
  rulePartOfMonth :: Rule
  rulePartOfMonth = Rule
    { name = "part of <named-month>"
    , pattern =
      [ regex "(началото|средата|края) на"
      , Predicate isAMonth
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):Token Time td:_) -> do
          (sd, ed) <- case Text.toLower match of
            "началото" -> Just (1, 10)
            "средата"   -> Just (11, 20)
            "края"  -> Just (21, -1)
            _       -> Nothing
          start <- intersect td $ dayOfMonth sd
          end <- if ed /= -1
            then intersect td $ dayOfMonth ed
            else Just $ cycleLastOf TG.Day td
          Token Time <$> interval TTime.Open start end
        _ -> Nothing
    }
  
  ruleEndOrBeginningOfMonth :: Rule
  ruleEndOrBeginningOfMonth = Rule
    { name = "at the beginning|end of <named-month>"
    , pattern =
      [ regex "(в )?(началото|края) на"
      , Predicate isAMonth
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (_:match:_)):Token Time td:_) -> do
          (sd, ed) <- case Text.toLower match of
            "началото" -> Just (1, 10)
            "края"       -> Just (21, -1)
            _           -> Nothing
          start <- intersect td $ dayOfMonth sd
          end <- if ed /= -1
            then intersect td $ dayOfMonth ed
            else Just $ cycleLastOf TG.Day td
          Token Time <$> interval TTime.Open start end
        _ -> Nothing
    }
  
  ruleEndOfMonth :: Rule
  ruleEndOfMonth = Rule
    { name = "end of month"
    , pattern =
      [ regex "((до|в)\\s+)?края на месеца"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_)
          | (Just start, Just end) <- parsed ->
            Token Time <$> interval TTime.Open start end
          where
            cycleMonth = cycleNth TG.Month
            parsed = if "до" `Text.isPrefixOf` Text.toLower match
              then
                ( Just now
                , intersect (dayOfMonth 1) $ cycleMonth 1)
              else
                ( intersect (dayOfMonth 21) $ cycleMonth 0
                , Just $ cycleLastOf TG.Day $ cycleMonth 0)
        _ -> Nothing
    }
  
  ruleBeginningOfMonth :: Rule
  ruleBeginningOfMonth = Rule
    { name = "beginning of month"
    , pattern = [ regex "(в\\s+ )?началото на месеца" ]
    , prod = \_ -> do
        start <- intersect (dayOfMonth 1) $ cycleNth TG.Month 0
        end <- intersect (dayOfMonth 10) $ cycleNth TG.Month 0
        Token Time <$> interval TTime.Open start end
    }
  
  ruleEndOrBeginningOfYear :: Rule
  ruleEndOrBeginningOfYear = Rule
    { name = "at the beginning|end of <year>"
    , pattern =
      [ regex "(в\\s+)?(началото|края) на"
      , Predicate $ isGrainOfTime TG.Year
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (_:match:_)):Token Time td:_) -> do
          (sd, ed) <- case Text.toLower match of
            "началото" -> Just (1, 4)
            "края"       -> Just (9, -1)
            _           -> Nothing
          start <- intersect td $ month sd
          end <- if ed /= -1
            then intersect td $ cycleLastOf TG.Month $ month ed
            else cycleNthAfter False TG.Year 1 <$> intersect td (month 1)
          Token Time <$> interval TTime.Open start end
        _ -> Nothing
    }
  
  ruleEndOfYear :: Rule
  ruleEndOfYear = Rule
    { name = "end of year"
    , pattern =
      [ regex "((до|в)\\s+)?края на годината"
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):_) -> do
          start <- std
          end <- intersect (month 1) $ cycleYear 1
          Token Time <$> interval TTime.Open start end
            where
              std = if "до" `Text.isPrefixOf` Text.toLower match
                then Just now
                else intersect (month 9) $ cycleYear 0
              cycleYear = cycleNth TG.Year
        _ -> Nothing
    }
  
  ruleBeginningOfYear :: Rule
  ruleBeginningOfYear = Rule
    { name = "beginning of year"
    , pattern = [ regex "(в\\s+)?началото на годината" ]
    , prod = \_ -> do
        start <- intersect (month 1) $ cycleNth TG.Year 0
        end <- intersect (month 4) $ cycleNth TG.Year 0
        Token Time <$> interval TTime.Open start end
    }
  
  ruleEndOrBeginningOfWeek :: Rule
  ruleEndOrBeginningOfWeek = Rule
    { name = "at the beginning|end of <week>"
    , pattern =
      [ regex "(в\\s+)?(началото|края) на"
      , Predicate $ isGrainOfTime TG.Week
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (_:match1:_)):Token Time td:_) -> do
          (sd, ed) <- case Text.toLower match1 of
            "началото" -> Just (1, 3)
            "края"       -> Just (5, 7)
            _           -> Nothing
          start <- intersect td $ dayOfWeek sd
          end <- intersect td $ dayOfWeek ed
          Token Time <$> interval TTime.Open start end
        _ -> Nothing
    }
  
  rulePeriodicHolidays :: [Rule]
  rulePeriodicHolidays = mkRuleHolidays
    -- Fixed dates, year over year
    [ ( "April Fools", "първи април", monthDay 4 1 )
    , ( "Bulgarian Liberation Day", "(трети март)|(ден(я|ят)? на освобождението на българия( от турско робство)?)", monthDay 3 3 )
    , ( "Bulgarian Indenpendence Day", "ден(я|ят)? на (обявянето на)? независимостта на българия", monthDay 9 22 )
    , ( "Bulgarian Unification Day", "ден(я|ят)? на съединението на (княжество )?българия(и източна румелия)?", monthDay 9 6 )
    , ( "Slavonic Litaracy Day", "(ден(я|ят)? на славянската писменост)|(св(\\.|ети)?(св(\\.|ети)?)? кирил и методи(й)?)", monthDay 5 24 )
    , ( "Saint Geroge's Day", "(св(\\.|ети)? георги)|гергьовден", monthDay 5 6 )
    , ( "Assumption of Mary", "голяма богородица", monthDay 8 15 )
    , ( "Christmas", "коледа|(рождество( христово)?)", monthDay 12 25 )
    , ( "Christmas Eve", "бъдни вечер", monthDay 12 24 )
    , ( "Earth Day", "ден(я|ят) на земята", monthDay 4 22 )
    , ( "May Day", "(първи май)|(ден(я|ят)? на труда)", monthDay 5 1 )
    , ( "New Year's Day", "нова година", monthDay  1  1 )
    , ( "St. George's Day", "гергьовден|(св(\\.|ети)? георги)", monthDay 5 6 )
    , ( "Valentine's Day", "св(\\.|ети)? валентин", monthDay 2 14 )
    , ( "International Women's Day", "ден(я|ят)? на (майката|жената)", monthDay 3 8 )
    ]
  
  ruleComputedHolidays :: [Rule]
  ruleComputedHolidays = mkRuleHolidays
    [ ( "Orthodox Easter Monday", "велики понеделник"
      , cycleNthAfter False TG.Day 1 orthodoxEaster )
    , ( "Orthodox Easter Sunday", "великден"
      , orthodoxEaster )
    , ( "Orthodox Holy Saturday", "велика събота"
      , cycleNthAfter False TG.Day (-1) orthodoxEaster )
    , ( "Orthodox Great Friday", "(велики|разпети) петък"
      , cycleNthAfter False TG.Day (-2) orthodoxEaster )
    , ( "Orthodox Palm Sunday", "цветница"
      , cycleNthAfter False TG.Day (-7) orthodoxEaster )
    ]
  
  ruleComputedHolidays' :: [Rule]
  ruleComputedHolidays' = mkRuleHolidays'
    [ ( "Great Lent", "велики(те)? пости"
      , let start = cycleNthAfter False TG.Day (-48) orthodoxEaster
            end = cycleNthAfter False TG.Day (-9) orthodoxEaster
          in interval TTime.Open start end )
  
    -- Other
    -- Last Saturday of March unless it falls on Holy Saturday
    -- In which case it's the Saturday before
    , ( "Earth Hour", "час(а|ът) на земята"
      , let holySaturday = cycleNthAfter False TG.Day (-1) easterSunday
            tentative = predLastOf (dayOfWeek 6) (month 3)
            alternative = cycleNthAfter False TG.Day (-7) tentative
          in do
            day <- intersectWithReplacement holySaturday tentative alternative
            start <- intersect day $ hourMinute True 20 30
            interval TTime.Closed start $ cycleNthAfter False TG.Minute 60 start )
    ]
  
  ruleCycleThisLastNext :: Rule
  ruleCycleThisLastNext = Rule
    { name = "this|last|next <cycle>"
    , pattern =
      [ regex "(т(о|а)зи|това|((по\\-)*(настоящ|идн|следващ|минал|последн|предишн)(ия(т)?|ата|ото)))"
      , dimension TimeGrain
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):Token TimeGrain grain:_) ->
          case Text.toLower match of
            "този"          -> tt $ cycleNth grain 0
            "тази"          -> tt $ cycleNth grain 0
            "това"          -> tt $ cycleNth grain 0
            "настоящия"     -> tt $ cycleNth grain 0
            "настоящият"    -> tt $ cycleNth grain 0
            "настоящата"    -> tt $ cycleNth grain 0
            "настоящото"    -> tt $ cycleNth grain 0
            "последния"     -> tt . cycleNth grain $ - 1
            "последният"    -> tt . cycleNth grain $ - 1
            "последната"    -> tt . cycleNth grain $ - 1
            "последното"    -> tt . cycleNth grain $ - 1
            "миналия"       -> tt . cycleNth grain $ - 1
            "миналият"      -> tt . cycleNth grain $ - 1
            "миналата"      -> tt . cycleNth grain $ - 1
            "миналото"      -> tt . cycleNth grain $ - 1
            "предишния"     -> tt . cycleNth grain $ - 1
            "предишният"    -> tt . cycleNth grain $ - 1
            "предишната"    -> tt . cycleNth grain $ - 1
            "предишното"    -> tt . cycleNth grain $ - 1
            "следващия"     -> tt $ cycleNth grain 1
            "следващият"    -> tt $ cycleNth grain 1
            "следващата"    -> tt $ cycleNth grain 1
            "следващото"    -> tt $ cycleNth grain 1
            "идния"         -> tt $ cycleNth grain 1
            "идният"        -> tt $ cycleNth grain 1
            "идната"        -> tt $ cycleNth grain 1
            "идното"        -> tt $ cycleNth grain 1
            "по-последния"     -> tt . cycleNth grain $ - 2
            "по-последният"    -> tt . cycleNth grain $ - 2
            "по-последната"    -> tt . cycleNth grain $ - 2
            "по-последното"    -> tt . cycleNth grain $ - 2
            "по-миналия"       -> tt . cycleNth grain $ - 2
            "по-миналият"      -> tt . cycleNth grain $ - 2
            "по-миналата"      -> tt . cycleNth grain $ - 2
            "по-миналото"      -> tt . cycleNth grain $ - 2
            "по-предишния"     -> tt . cycleNth grain $ - 2
            "по-предишният"    -> tt . cycleNth grain $ - 2
            "по-предишната"    -> tt . cycleNth grain $ - 2
            "по-предишното"    -> tt . cycleNth grain $ - 2
            "по-следващия"     -> tt $ cycleNth grain 2
            "по-следващият"    -> tt $ cycleNth grain 2
            "по-следващата"    -> tt $ cycleNth grain 2
            "по-следващото"    -> tt $ cycleNth grain 2
            "по-по-последния"     -> tt . cycleNth grain $ - 3
            "по-по-последният"    -> tt . cycleNth grain $ - 3
            "по-по-последната"    -> tt . cycleNth grain $ - 3
            "по-по-последното"    -> tt . cycleNth grain $ - 3
            "по-по-миналия"       -> tt . cycleNth grain $ - 3
            "по-по-миналият"      -> tt . cycleNth grain $ - 3
            "по-по-миналата"      -> tt . cycleNth grain $ - 3
            "по-по-миналото"      -> tt . cycleNth grain $ - 3
            "по-по-предишния"     -> tt . cycleNth grain $ - 3
            "по-по-предишният"    -> tt . cycleNth grain $ - 3
            "по-по-предишната"    -> tt . cycleNth grain $ - 3
            "по-по-предишното"    -> tt . cycleNth grain $ - 3
            "по-по-следващия"     -> tt $ cycleNth grain 3
            "по-по-следващият"    -> tt $ cycleNth grain 3
            "по-по-следващата"    -> tt $ cycleNth grain 3
            "по-по-следващото"    -> tt $ cycleNth grain 3
            _ -> Nothing
        _ -> Nothing
    }
  
  ruleCycleAfterBeforeTime :: Rule
  ruleCycleAfterBeforeTime = Rule
    { name = "<cycle> after|before <time>"
    , pattern =
      [ dimension TimeGrain
      , regex "след|преди"
      , dimension Time
      ]
    , prod = \case
        (Token TimeGrain grain:
         Token RegexMatch (GroupMatch (match:_)):
         Token Time td:
         _) ->
          let n = if Text.toLower match == "след" then 1 else - 1 in
            tt $ cycleNthAfter False grain n td
        _ -> Nothing
    }
  
  ruleDayInDuration :: Rule
  ruleDayInDuration = Rule
    { name = "<day> in <duration>"
    , pattern =
      [ Predicate $ or . sequence [isGrainOfTime TG.Day, isGrainOfTime TG.Month]
      , regex "в|през"
      , Predicate $ isDurationGreaterThan TG.Hour
      ]
    , prod = \case
        (Token Time td:_:Token Duration dd:_) ->
          Token Time <$> intersect td (inDurationInterval dd)
        _ -> Nothing
    }
  
  ruleDurationInWithinAfter :: Rule
  ruleDurationInWithinAfter = Rule
    { name = "in|within|after <duration>"
    , pattern =
      [ regex "(за|в рамките на|след)"
      , dimension Duration
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):
          Token Duration dd:
          _) -> case Text.toLower match of
            "в рамките на" -> Token Time <$> interval TTime.Open now (inDuration dd)
            "след"  -> tt . withDirection TTime.After $ inDuration dd
            "за"     -> tt $ inDuration dd
            _        -> Nothing
        _ -> Nothing
    }
  
  ruleNDOWago :: Rule
  ruleNDOWago = Rule
    { name = "<integer> <named-day> ago|back"
    , pattern =
      [ regex "преди"
      , Predicate isNatural
      , Predicate isADayOfWeek
      ]
    , prod = \case
        (_:Token Numeral NumeralData{TNumeral.value = v}:Token Time td:_) ->
          tt $ predNth (- (floor v)) False td
        _ -> Nothing
    }
  
  ruleDurationHenceAgo :: Rule
  ruleDurationHenceAgo = Rule
    { name = "<duration> hence|ago"
    , pattern =
      [ regex "(от|преди)"
      , dimension Duration
      ]
    , prod = \case
        (Token RegexMatch (GroupMatch (match:_)):Token Duration dd:_) ->
          case Text.toLower match of
            "от" -> tt $ durationAgo dd
            _     -> tt $ inDuration dd
        _ -> Nothing
    }
  
  ruleInNumeral :: Rule
  ruleInNumeral = Rule
    { name = "in <number> (implicit minutes)"
    , pattern =
      [ regex "след"
      , Predicate $ isIntegerBetween 0 60
      ]
    , prod = \case
        (_:Token Numeral NumeralData{TNumeral.value = v}:_) ->
          tt . inDuration . duration TG.Minute $ floor v
        _ -> Nothing
    }
  
  ruleIntervalForDurationFrom :: Rule
  ruleIntervalForDurationFrom = Rule
    { name = "for <duration> from <time>"
    , pattern =
      [ regex "за"
      , dimension Duration
      , regex "от|след"
      , dimension Time
      ]
    , prod = \case
        (_:Token Duration dd:_:Token Time td1:_) ->
          Token Time <$> interval TTime.Closed td1 (durationAfter dd td1)
        _ -> Nothing
  }
  
  ruleIntervalTimeForDuration :: Rule
  ruleIntervalTimeForDuration = Rule
    { name = "<time> for <duration>"
    , pattern =
      [ Predicate isNotLatent
      , regex "за"
      , dimension Duration
      ]
    , prod = \case
        (Token Time td1:_:Token Duration dd:_) ->
          Token Time <$> interval TTime.Closed td1 (durationAfter dd td1)
        _ -> Nothing
  }
  
  ruleIntervalFromTimeForDuration :: Rule
  ruleIntervalFromTimeForDuration = Rule
    { name = "from <time> for <duration>"
    , pattern =
      [ regex "от|след"
      , Predicate isNotLatent
      , regex "за"
      , dimension Duration
      ]
    , prod = \case
        (_:Token Time td1:_:Token Duration dd:_) ->
          Token Time <$> interval TTime.Closed td1 (durationAfter dd td1)
        _ -> Nothing
  }
  
  timezoneName :: String
  timezoneName = "YEKT|YEKST|YAKT|YAKST|WITA|WIT|WIB|WGT|WGST|WFT|WET|WEST|WAT|WAST|VUT|VLAT|VLAST|VET|UZT|UYT|UYST|UTC|ULAT|TVT|TMT|TLT|TKT|TJT|TFT|TAHT|SST|SRT|SGT|SCT|SBT|SAST|SAMT|RET|PYT|PYST|PWT|PST|PONT|PMST|PMDT|PKT|PHT|PHOT|PGT|PETT|PETST|PET|PDT|OMST|OMSST|NZST|NZDT|NUT|NST|NPT|NOVT|NOVST|NFT|NDT|NCT|MYT|MVT|MUT|MST|MSK|MSD|MMT|MHT|MDT|MAWT|MART|MAGT|MAGST|LINT|LHST|LHDT|KUYT|KST|KRAT|KRAST|KGT|JST|IST|IRST|IRKT|IRKST|IRDT|IOT|IDT|ICT|HOVT|HKT|GYT|GST|GMT|GILT|GFT|GET|GAMT|GALT|FNT|FKT|FKST|FJT|FJST|EST|EGT|EGST|EET|EEST|EDT|ECT|EAT|EAST|EASST|DAVT|ChST|CXT|CVT|CST|COT|CLT|CLST|CKT|CHAST|CHADT|CET|CEST|CDT|CCT|CAT|CAST|BTT|BST|BRT|BRST|BOT|BNT|AZT|AZST|AZOT|AZOST|AWST|AWDT|AST|ART|AQTT|ANAT|ANAST|AMT|AMST|ALMT|AKST|AKDT|AFT|AEST|AEDT|ADT|ACST|ACDT"
  
  ruleTimezone :: Rule
  ruleTimezone = Rule
    { name = "<time> timezone"
    , pattern =
      [ Predicate $ and . sequence [isNotLatent, isATimeOfDay, hasNoTimezone]
      , regex $ "\\b(" ++ timezoneName ++ ")\\b"
      ]
    , prod = \case
        (Token Time td:
         Token RegexMatch (GroupMatch (tz:_)):
         _) -> Token Time <$> inTimezone (Text.toUpper tz) td
        _ -> Nothing
    }
  
  ruleTimezoneBracket :: Rule
  ruleTimezoneBracket = Rule
    { name = "<time> (timezone)"
    , pattern =
      [ Predicate $ and . sequence [isNotLatent, isATimeOfDay, hasNoTimezone]
      , regex $ "\\((" ++ timezoneName ++ ")\\)"
      ]
    , prod = \case
        (Token Time td:
         Token RegexMatch (GroupMatch (tz:_)):
         _) -> Token Time <$> inTimezone (Text.toUpper tz) td
        _ -> Nothing
    }
  
  ruleIntervalDashTimezone :: Rule
  ruleIntervalDashTimezone = Rule
    { name = "<datetime> - <datetime> (interval) timezone"
    , pattern =
      [ Predicate $ and . sequence [isATimeOfDay, hasNoTimezone]
      , regex "\\-|до"
      , Predicate $ and . sequence [isATimeOfDay, hasNoTimezone]
      , regex $ "\\b(" ++ timezoneName ++ ")\\b"
      ]
    , prod = \case
        (Token Time td1:
         _:
         Token Time td2:
         Token RegexMatch (GroupMatch (tz:_)):
         _) -> do
          tdz1 <- inTimezone (Text.toUpper tz) td1
          tdz2 <- inTimezone (Text.toUpper tz) td2
          Token Time <$> interval TTime.Closed tdz1 tdz2
        _ -> Nothing
    }
  
  rules :: [Rule]
  rules =
    [ ruleIntersect
    , ruleIntersectOf
    , ruleIntersectYear
    , ruleAbsorbOnDay
    , ruleAbsorbInMonthYear
    , ruleAbsorbCommaTOD
    , ruleAbsorbOnADOW
    , ruleNextDOW
    , ruleNextTime
    , ruleThisTime
    , ruleLastTime
    , ruleTimeBeforeLast
    , ruleTimeAfterNext
    , ruleLastDOWOfTime
    , ruleLastCycleOfTime
    , ruleLastNight
    , ruleLastWeekendOfMonth
    , ruleNthTimeOfTime
    , ruleNthTimeAfterTime
    , ruleNDOWFromTime
    , ruleYearLatent
    , ruleYearADBC
    , ruleTheDOMNumeral
    , ruleTheDOMOrdinal
    , ruleDOMLatent
    , ruleNamedDOMOrdinal
    , ruleMonthDOMNumeral
    , ruleDOMMonth
    , ruleDOMOrdinalMonthYear
    , ruleDOMMonthYear
    , ruleIdesOfMonth
    , ruleTODLatent
    , ruleAtTOD
    , ruleTODOClock
    , ruleTODAM
    , ruleTODPM
    , ruleHHMM
    , ruleHHhMM
    , ruleHHMMLatent
    , ruleHHMMSS
    , ruleHONumeral
    , ruleHODHalf
    , ruleHODQuarter
    , ruleNumeralToHOD
    , ruleQuarterToHOD
    , ruleNumeralAfterHOD
    , ruleYYYYQQ
    , ruleYYYYMM
    , ruleYYYYMMDD
    , ruleDDMMYYYYDot
    , ruleMMYYYY
    , ruleNoonMidnightEOD
    , rulePartOfDays
    , ruleEarlyMorning
    , rulePODThis
    , ruleTonight
    , ruleAfterPartofday
    , ruleTimePOD
    , rulePODofTime
    , ruleWeekend
    , ruleWeek
    , ruleTODPrecision
    , rulePrecisionTOD
    , ruleIntervalFromDDDDMonth
    , ruleIntervalMonthDDDD
    , ruleIntervalDDDDMonth
    , ruleIntervalDash
    , ruleIntervalSlash
    , ruleIntervalFrom
    , ruleIntervalBetween
    , ruleIntervalTODDash
    , ruleIntervalTODBetween
    , ruleIntervalBy
    , ruleIntervalByTheEndOf
    , ruleIntervalUntilTime
    , ruleIntervalAfterFromSinceTime
    , ruleCycleThisLastNext
    , ruleCycleAfterBeforeTime
    , ruleDayInDuration
    , ruleDurationInWithinAfter
    , ruleNDOWago
    , ruleDurationHenceAgo
    , ruleIntervalForDurationFrom
    , ruleIntervalFromTimeForDuration
    , ruleIntervalTimeForDuration
    , ruleInNumeral
    , ruleTimezone
    , ruleTimezoneBracket
    , ruleIntervalDashTimezone
    , rulePartOfMonth
    , ruleEndOrBeginningOfMonth
    , ruleEndOrBeginningOfYear
    , ruleEndOrBeginningOfWeek
    , ruleNow
    , ruleSeason
    , ruleEndOfMonth
    , ruleBeginningOfMonth
    , ruleEndOfYear
    , ruleBeginningOfYear
    , ruleMonthYear
    ]
    ++ ruleInstants
    ++ ruleDaysOfWeek
    ++ ruleMonths
    ++ ruleSeasons
    ++ ruleComputedHolidays
    ++ ruleComputedHolidays'
    ++ rulePeriodicHolidays
  