packages feed

fuzzy-time-gen-0.2.0.0: test/Data/FuzzyTime/ParserSpec.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

module Data.FuzzyTime.ParserSpec
  ( spec,
  )
where

import Control.Monad
import Data.FuzzyTime
import Data.FuzzyTime.Types.Gen ()
import Data.GenValidity.Text ()
import Data.Int
import Data.List (nub)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
import Data.Void
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Test.Validity
import Text.Megaparsec
import Text.Printf

spec :: Spec
spec = do
  describe "fuzzyLocalTimeP" $ do
    parsesValidSpec fuzzyLocalTimeP
    let p = parseJustSpec fuzzyLocalTimeP
        pr = parseJustSpecR fuzzyLocalTimeP
        f = parseNothingSpec fuzzyLocalTimeP
    p "1" (FuzzyLocalTime $ One $ OnlyDay 1)
    p "today" (FuzzyLocalTime $ One Today)
    p "Today" (FuzzyLocalTime $ One Today)
    p "TodAY" (FuzzyLocalTime $ One Today)
    p "monday" (FuzzyLocalTime $ One $ NextDayOfTheWeek Monday)
    p "Monday" (FuzzyLocalTime $ One $ NextDayOfTheWeek Monday)
    p "MoNDay" (FuzzyLocalTime $ One $ NextDayOfTheWeek Monday)
    p "8:" (FuzzyLocalTime $ Other $ AtHour 8)
    p "09:" (FuzzyLocalTime $ Other $ AtHour 9)
    p "05:06" (FuzzyLocalTime $ Other $ AtMinute 5 6)
    p "evening" (FuzzyLocalTime $ Other Evening)
    p "tues 05:06" (FuzzyLocalTime $ Both (NextDayOfTheWeek Tuesday) (AtMinute 5 6))
    p "wed 5:06" (FuzzyLocalTime $ Both (NextDayOfTheWeek Wednesday) (AtMinute 5 6))
    p "thu 11:" (FuzzyLocalTime $ Both (NextDayOfTheWeek Thursday) (AtHour 11))
    p "8 05:06" (FuzzyLocalTime $ Both (OnlyDay 8) (AtMinute 5 6))
    p "02-07 05:06" (FuzzyLocalTime $ Both (DayInMonth 2 7) (AtMinute 5 6))
    pr 3 "noon" $ FuzzyLocalTime $ Other Noon
    pr 3 "Noon" $ FuzzyLocalTime $ Other Noon
    pr 4 "midday" $ FuzzyLocalTime $ Other Noon
    pr 4 "Midday" $ FuzzyLocalTime $ Other Noon
    pr 4 "midnight" $ FuzzyLocalTime $ Other Midnight
    pr 4 "Midnight" $ FuzzyLocalTime $ Other Midnight
    pr 3 "morning" $ FuzzyLocalTime $ Other Morning
    pr 3 "Morning" $ FuzzyLocalTime $ Other Morning
    pr 1 "evening" $ FuzzyLocalTime $ Other Evening
    pr 1 "Evening" $ FuzzyLocalTime $ Other Evening
    p "6:07" $ FuzzyLocalTime $ Other (AtMinute 6 7)
    p "08:09" $ FuzzyLocalTime $ Other (AtMinute 8 9)
    p "1011" $ FuzzyLocalTime $ Other (AtMinute 10 11)
    p "0324" $ FuzzyLocalTime $ Other (AtMinute 3 24)
    p "23:59:22" $ FuzzyLocalTime $ Other $ AtExact (TimeOfDay 23 59 22)
    p "5:06:23" $ FuzzyLocalTime $ Other $ AtExact (TimeOfDay 5 6 23)
    p "0506:23" $ FuzzyLocalTime $ Other $ AtExact (TimeOfDay 5 6 23)
    p "+5h" $ FuzzyLocalTime $ Other (HoursDiff 5)
    p "-6h" $ FuzzyLocalTime $ Other (HoursDiff (-6))
    p "+7m" $ FuzzyLocalTime $ One (DiffMonths 7)
    p "-8m" $ FuzzyLocalTime $ One (DiffMonths (-8))
    p "+9s" $ FuzzyLocalTime $ Other (SecondsDiff 9)
    p "-10s" $ FuzzyLocalTime $ Other (SecondsDiff (-10))
    f "hello"
    f "world"
  describe "twoDigitsSegmentP" $ do
    parsesValidSpec twoDigitsSegmentP
    let p = parseJustSpec twoDigitsSegmentP
        f = parseNothingSpec twoDigitsSegmentP
    p "0" 0
    p "6" 6
    p "01" 1
    p "12" 12
    p "52" 52
    f "152"
    f "6:"
  describe "hourSegmentP" $ do
    parsesValidSpec hourSegmentP
    let p = parseJustSpec hourSegmentP
        f = parseNothingSpec hourSegmentP
    p "0" 0
    p "6" 6
    p "01" 1
    p "12" 12
    p "7:" 7
    f "25"
    f "52"
    f "152"
  describe "minuteSegmentP" $ do
    parsesValidSpec minuteSegmentP
    let p = parseJustSpec minuteSegmentP
        f = parseNothingSpec minuteSegmentP
    p "0" 0
    p "6" 6
    p "01" 1
    p "12" 12
    p "25" 25
    p "52" 52
    f "152"
    f "8:"
  describe "atHourP" $ do
    parsesValidSpec atHourP
    let p = parseJustSpec atHourP
        f = parseNothingSpec atHourP
    p "0" (AtHour 0)
    p "2" (AtHour 2)
    p "23" (AtHour 23)
    p "08" (AtHour 8)
    p "04" (AtHour 4)
    p "6:" (AtHour 6)
    p "06:" (AtHour 6)
    f "26"
    f "103"
  describe "atMinuteP" $ do
    parsesValidSpec atMinuteP
    let p = parseJustSpec atMinuteP
        f = parseNothingSpec atMinuteP
    p "2:52" (AtMinute 2 52)
    p "23:52" (AtMinute 23 52)
    p "08:15" (AtMinute 08 15)
    p "0426" (AtMinute 4 26)
    f "6:"
    f "06:"
  describe "atExactP" $ do
    parsesValidSpec atExactP
    let p = parseJustSpec atExactP
        f = parseNothingSpec atExactP
    p "23:59:22" $ AtExact (TimeOfDay 23 59 22)
    p "5:06:23" $ AtExact (TimeOfDay 5 6 23)
    p "0506:23" $ AtExact (TimeOfDay 5 6 23)
    f "50623"
    f "050623"
    f "05:0623"
  describe "fuzzyTimeOfDayP" $ do
    parsesValidSpec fuzzyTimeOfDayP
    let p = parseJustSpec fuzzyTimeOfDayP
    let pr = parseJustSpecR fuzzyTimeOfDayP
    pr 2 "noon" Noon
    pr 4 "midday" Noon
    pr 4 "midnight" Midnight
    pr 2 "morning" Morning
    pr 1 "evening" Evening
    describe "AtHour" $ do
      p "0" (AtHour 0)
      p "4" (AtHour 4)
      p "05" (AtHour 5)
      p "6:" (AtHour 6)
      p "07:" (AtHour 7)
    describe "AtMinute" $ do
      p "6:07" (AtMinute 6 7)
      p "08:09" (AtMinute 8 9)
      p "1011" (AtMinute 10 11)
      p "0324" (AtMinute 3 24)
    describe "AtExact" $ do
      p "23:59:22" $ AtExact (TimeOfDay 23 59 22)
      p "5:06:23" $ AtExact (TimeOfDay 5 6 23)
      p "0506:23" $ AtExact (TimeOfDay 5 6 23)
      p "05:06:23.1" $ AtExact (TimeOfDay 5 6 23.1)
      p "05:06:23.01" $ AtExact (TimeOfDay 5 6 23.01)
      p "05:06:23.001" $ AtExact (TimeOfDay 5 6 23.001)
      p "05:06:23.0001" $ AtExact (TimeOfDay 5 6 23.0001)
      p "05:06:23.00001" $ AtExact (TimeOfDay 5 6 23.00001)
      p "05:06:23.000001" $ AtExact (TimeOfDay 5 6 23.000001)
      p "05:06:23.0000001" $ AtExact (TimeOfDay 5 6 23.0000001)
      p "05:06:23.00000001" $ AtExact (TimeOfDay 5 6 23.00000001)
      p "05:06:23.000000001" $ AtExact (TimeOfDay 5 6 23.000000001)
      p "05:06:23.0000000001" $ AtExact (TimeOfDay 5 6 23.0000000001)
      p "05:06:23.00000000001" $ AtExact (TimeOfDay 5 6 23.00000000001)
      p "05:06:23.000000000001" $ AtExact (TimeOfDay 5 6 23.000000000001)
      it "can parse whatever is rendered" $
        forAllValid $ \tod ->
          let s = formatTime defaultTimeLocale "%T%Q" tod
           in case parseForTest fuzzyTimeOfDayP (T.pack s) of
                Left e -> expectationFailure $ errorBundlePretty e
                Right r -> resolveTimeOfDay tod r `shouldBe` tod
    describe "HoursDiff" $ do
      p "+3" (HoursDiff 3)
      p "-4" (HoursDiff (-4))
      p "+5h" (HoursDiff 5)
      p "-6h" (HoursDiff (-6))
    describe "MinutesDiff" $ do
      p "+7m" (MinutesDiff 7)
      p "-8m" (MinutesDiff (-8))
    describe "SecondsDiff" $ do
      p "+9s" (SecondsDiff 9)
      p "-10s" (SecondsDiff (-10))
  describe "fuzzyDayP" $ do
    parsesValidSpec fuzzyDayP
    let fd = parseJustSpecR fuzzyDayP
    fd 1 "yesterday" Yesterday
    fd 3 "today" Today
    fd 3 "tomorrow" Tomorrow
    fd 1 "now" Now
    it "parses exact (recent) days with %Y-%m-%d" $
      forAll (ModifiedJulianDay . toInteger <$> (genValid :: Gen Int16)) $ \day ->
        let t = T.pack $ formatTime defaultTimeLocale "%Y-%m-%d" day
         in parseJust fuzzyDayP t $ ExactDay day
    let s = parseJustSpec fuzzyDayP
    let f = parseNothingSpec fuzzyDayP
    it "parses x as OnlyDay x for x between 1 and 31" $
      forAll (choose (1, 31)) $ \i -> parseJust fuzzyDayP (T.pack (show i)) (OnlyDay i)
    s "+3" (DiffDays 3)
    s "-3" (DiffDays $ -3)
    it "Parses +x as DiffDays x" $
      forAllValid $ \i -> parseJust fuzzyDayP (T.pack (printf "%+d" i)) (DiffDays i)
    s "+4d" (DiffDays 4)
    s "-4d" (DiffDays $ -4)
    it "Parses +xd as DiffDays x" $
      forAllValid $ \i -> parseJust fuzzyDayP (T.pack (printf "%+dd" i)) (DiffDays i)
    s "+5w" (DiffWeeks 5)
    s "-5w" (DiffWeeks $ -5)
    it "Parses +xw as DiffWeeks x" $
      forAllValid $ \i -> parseJust fuzzyDayP (T.pack (printf "%+dw" i)) (DiffWeeks i)
    s "+6m" (DiffMonths 6)
    s "-6m" (DiffMonths $ -6)
    it "Parses +xw as DiffMonths x" $
      forAllValid $ \i -> parseJust fuzzyDayP (T.pack (printf "%+dm" i)) (DiffMonths i)
    f "0-0"
    s "2-13" (DayInMonth 2 13)
    s "12-3" (DayInMonth 12 3)
    s "02-13" (DayInMonth 2 13)
    s "12-03" (DayInMonth 12 3)
    s "02-03" (DayInMonth 2 3)
    f "002-03"
    f "02-003"
    f "002-003"
    modifyMaxSuccess (\x -> (x * (365 * 4)) `div` 100) $
      it "parses m-d (in any format) as DayInMonth" $
        forAll (elements $ daysInMonth 2004) $ \(month, mds) ->
          let m = monthNum month
           in forAll (choose (1, mds)) $ \d ->
                let options =
                      nub $ do
                        ms <- [printf "%d" m, printf "%02d" m]
                        ds <- [printf "%d" d, printf "%02d" d]
                        pure $ T.pack $ concat [ms, "-", ds] :: [Text]
                 in forAll (elements options) $ \s_ -> parseJust fuzzyDayP s_ (DayInMonth m d)
    it "parses whatever the fuzzy day parser parses, as the next day of the week" $
      forAllValid $ \t ->
        case (,) <$> parse (fuzzyDayOfTheWeekP <* eof) "test input" t
          <*> parse (fuzzyDayP <* eof) "test input" t of
          Left _ -> pure ()
          Right (dow, fd_) ->
            case fd_ of
              NextDayOfTheWeek dow' -> dow' `shouldBe` dow
              _ -> expectationFailure "fuzzyDayP parsed something other than a day of the week"
    it "parses the day of the week string as NextDayOfTheWeek" $
      forAll (elements dayOfTheWeekStrings) $ \(dow, i, t) ->
        forAll (elements $ drop i $ T.inits t) $ \t_ ->
          parseJust fuzzyDayP t_ (NextDayOfTheWeek dow)
  describe "fuzzyDayOfTheWeekP" $ do
    parsesValidSpec fuzzyDayOfTheWeekP
    let fd = parseJustSpecR fuzzyDayOfTheWeekP
    forM_ dayOfTheWeekStrings $ \(dow, ix, s) -> fd ix s dow

dayOfTheWeekStrings :: [(DayOfWeek, Int, Text)]
dayOfTheWeekStrings =
  [ (Monday, 1, "monday"),
    (Monday, 1, "Monday"),
    (Tuesday, 2, "tuesday"),
    (Tuesday, 2, "Tuesday"),
    (Wednesday, 1, "wednesday"),
    (Wednesday, 1, "Wednesday"),
    (Thursday, 2, "thursday"),
    (Thursday, 2, "Thursday"),
    (Friday, 1, "friday"),
    (Friday, 1, "Friday"),
    (Saturday, 2, "saturday"),
    (Saturday, 2, "Saturday"),
    (Sunday, 2, "sunday"),
    (Sunday, 2, "Sunday")
  ]

parseJustSpecR :: (Show a, Eq a) => Parser a -> Int -> Text -> a -> Spec
parseJustSpecR p i t res = mapM_ (\s_ -> parseJustSpec p s_ res) $ drop i $ T.inits t

parseJustSpec :: (Show a, Eq a) => Parser a -> Text -> a -> Spec
parseJustSpec p s res = it (unwords ["parses", show s, "as", show res]) $ parseJust p s res

parseNothingSpec :: (Show a, Eq a) => Parser a -> Text -> Spec
parseNothingSpec p s = it (unwords ["fails to parse", show s]) $ parseNothing p s

parsesValidSpec :: (Show a, Eq a, Validity a) => Parser a -> Spec
parsesValidSpec p = it "only parses valid values" $ forAllValid $ parsesValid p

parseJust :: (Show a, Eq a) => Parser a -> Text -> a -> Expectation
parseJust p s res =
  case parseForTest p s of
    Right out -> out `shouldBe` res
    Left err ->
      expectationFailure $
        unlines ["Parser failed on input", show s, "with error", errorBundlePretty err]

parseNothing :: (Show a, Eq a) => Parser a -> Text -> Expectation
parseNothing p s =
  case parseForTest p s of
    Left _ -> pure ()
    Right v ->
      expectationFailure $
        unlines
          ["Parser succeeded on input", show s, "at parsing", show v, "but it should have failed."]

parsesValid :: (Show a, Eq a, Validity a) => Parser a -> Text -> Expectation
parsesValid p s =
  case parseForTest p s of
    Left _ -> pure ()
    Right out -> shouldBeValid out

parseForTest :: Parser a -> Text -> Either (ParseErrorBundle Text Void) a
parseForTest p s = parse (p <* eof) "test input" s