From 6fb7db4326c6d66810f2d9b89581dd3b3a112309 Mon Sep 17 00:00:00 2001 From: drlkf Date: Sun, 25 Oct 2026 21:12:09 +0100 Subject: [PATCH] feat!: add `Byte` support --- package.yaml | 4 +- src/Text/Megaparsec/Time.hs | 153 ++----------------------- src/Text/Megaparsec/Time/Byte.hs | 113 ++++++++++++++++++ src/Text/Megaparsec/Time/Char.hs | 111 ++++++++++++++++++ src/Text/Megaparsec/Time/Common.hs | 151 ++++++++++++++++++++++++ stack-lts-22.yaml | 2 +- stack-lts-23.yaml | 2 +- stack.yaml | 2 +- stack.yaml.lock | 8 +- test/Text/Megaparsec/TimeStreamSpec.hs | 51 +++++++++ 10 files changed, 448 insertions(+), 149 deletions(-) create mode 100644 src/Text/Megaparsec/Time/Byte.hs create mode 100644 src/Text/Megaparsec/Time/Char.hs create mode 100644 src/Text/Megaparsec/Time/Common.hs create mode 100644 test/Text/Megaparsec/TimeStreamSpec.hs diff --git a/package.yaml b/package.yaml index d9bb0e8..6b8b136 100644 --- a/package.yaml +++ b/package.yaml @@ -25,7 +25,7 @@ description: | dependencies: - base >= 4.7 && < 5 - megaparsec >= 9.0 && < 10 - - megaparsec-utils >= 0.1 && < 1 + - megaparsec-utils >= 0.2 && < 1 - parser-combinators >= 1.0 && < 2 - time >= 1.10 && < 2 @@ -54,5 +54,7 @@ tests: - -with-rtsopts=-N dependencies: - megaparsec-time + - bytestring - hspec - QuickCheck + - text diff --git a/src/Text/Megaparsec/Time.hs b/src/Text/Megaparsec/Time.hs index dfe8aa2..f177d9e 100644 --- a/src/Text/Megaparsec/Time.hs +++ b/src/Text/Megaparsec/Time.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE FlexibleContexts #-} - -- | -- Module : Text.Megaparsec.Time -- Description : Various parsers for types related to time. @@ -8,151 +6,24 @@ -- Maintainer : drlkf@drlkf.net -- Stability : experimental -- --- Parsers for types related to time to use with 'megaparsec'. +-- Re-exports of 'Text.Megaparsec.Time.Char'. New code should use +-- 'Text.Megaparsec.Time.Char' or 'Text.Megaparsec.Time.Byte' directly. module Text.Megaparsec.Time ( -- * Types DayResult, -- * Simple parsers - hoursParser, - minutesParser, - secondsParser, - timeParser, - dayParser, - gregorianDayParser, + Char.hoursParser, + Char.minutesParser, + Char.secondsParser, + Char.timeParser, + Char.dayParser, + Char.gregorianDayParser, -- * Composite parsers - dateParser, - durationParser, + Char.dateParser, + Char.durationParser, ) where -import Control.Applicative (optional, (<|>)) -import Control.Monad (replicateM) -import Control.Monad.Combinators (choice, some) -import Data.Char (toLower) -import Data.Functor (($>)) -import Data.Maybe (fromMaybe) -import Data.Time ( - Day, - DayOfWeek (..), - NominalDiffTime, - TimeOfDay (..), - defaultTimeLocale, - makeTimeOfDayValid, - parseTimeM, - secondsToNominalDiffTime, - ) -import Text.Megaparsec (Parsec, takeRest, try) -import Text.Megaparsec.Char ( - char, - digitChar, - space, - space1, - string', - ) -import Text.Printf (printf) -import Text.Megaparsec.Utils (posNumParser) - --- | Representation of a parser result with either a number of days relative to --- the current day, or a 'DayOfWeek'. -type DayResult = Either Int DayOfWeek - --- | Parse a tuple containing a day or not, and a 'TimeOfDay', separated with a --- space. -dateParser - :: Ord e - => Parsec e String (Maybe DayResult, TimeOfDay) -dateParser = (,) <$> optional (try (dayParser <* space1)) <*> timeParser - --- | Parse a day using one of the following, all case-insensitive: --- --- * a short (3-letters) or long day name e.g @mon@ or @monday@ --- * @yesterday@ or @tomorrow@ --- * a day number relative to the current day i.e @+2@ is two days from today --- * an absolute number for a 'DayOfWeek', refer to its 'Num' instance for more information. -dayParser - :: Ord e - => Parsec e String DayResult -dayParser = - choice - [ Right <$> longDay - , Right <$> shortDay - , Left <$> (string' "yesterday" $> -1) - , Left <$> (string' "tomorrow" $> 1) - , Right <$> absoluteDay - , Left <$> relativeDay - ] - where - shortDay = choice $ map (ciString (fmap toLower . take 3 . show)) weekDays - longDay = choice $ map (ciString (fmap toLower . show)) weekDays - ciString f d = try (string' (f d)) $> d - weekDays = [Monday .. Friday] - sign = (char '-' $> negate) <|> (char '+' $> id) - absoluteDay = toEnum . read <$> some digitChar - relativeDay = ($) <$> sign <*> (read <$> some digitChar) - --- | Parse a 'NominalDiffTime' using strings like @1h23m45s@, with all --- components being optional as long as one is present. -durationParser - :: Ord e - => Parsec e String NominalDiffTime -durationParser = try hours <|> try minutes <|> secondsParser - where - hours = do - h <- hoursParser <* space - m <- fromMaybe zero <$> optional (try minutes) - s <- fromMaybe zero <$> optional secondsParser - - return (h + m + s) - - minutes = do - m <- minutesParser <* space - s <- fromMaybe zero <$> optional secondsParser - - return (m + s) - --- | Parse a Gregorian 'Day' from a @%F@ or @%d\/%m\/%Y@ format. -gregorianDayParser - :: Ord e - => Parsec e String Day -gregorianDayParser = do - s <- takeRest - - let parseTime = flip (parseTimeM False defaultTimeLocale) s - - parseTime "%F" <|> parseTime "%d/%m/%Y" - --- | Parse a 'NominalDiffTime' from a number of hours from a string like @1h@. -hoursParser - :: Ord e - => Parsec e String NominalDiffTime -hoursParser = secondsToNominalDiffTime . (* 3600) <$> posNumParser <* char 'h' - --- | Parse a 'NominalDiffTime' from a number of minutes from a string like @1m@. -minutesParser - :: Ord e - => Parsec e String NominalDiffTime -minutesParser = secondsToNominalDiffTime . (* 60) <$> posNumParser <* char 'm' - --- | Parse a 'NominalDiffTime' from a number of seconds from a string like @1s@. -secondsParser - :: Ord e - => Parsec e String NominalDiffTime -secondsParser = secondsToNominalDiffTime <$> posNumParser <* optional (char 's') - --- | Parse a 'TimeOfDay' from a string like @01:23@. -timeParser - :: Ord e - => Parsec e String TimeOfDay -timeParser = do - h <- read <$> replicateM 2 digitChar <* char ':' - m <- read <$> replicateM 2 digitChar - - maybe - (fail (printf "invalid hours or minutes %02d:%02d" h m)) - pure - (makeTimeOfDayValid h m 0) - --- | Zero seconds in 'NominalDiffTime'. -zero :: NominalDiffTime -zero = secondsToNominalDiffTime 0 +import qualified Text.Megaparsec.Time.Char as Char +import Text.Megaparsec.Time.Common (DayResult) diff --git a/src/Text/Megaparsec/Time/Byte.hs b/src/Text/Megaparsec/Time/Byte.hs new file mode 100644 index 0000000..8e95786 --- /dev/null +++ b/src/Text/Megaparsec/Time/Byte.hs @@ -0,0 +1,113 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +-- | +-- Module : Text.Megaparsec.Time.Byte +-- Description : Time parsers for streams of 'Word8'. +-- Copyright : (c) drlkf, 2026 +-- License : GPL-3 +-- Maintainer : drlkf@drlkf.net +-- Stability : experimental +-- +-- Parsers for types related to time for streams of 'Word8' (strict and lazy +-- 'Data.ByteString.ByteString'). +module Text.Megaparsec.Time.Byte ( + -- * Simple parsers + hoursParser, + minutesParser, + secondsParser, + timeParser, + dayParser, + gregorianDayParser, + + -- * Composite parsers + dateParser, + durationParser, +) where + +import Control.Applicative (optional) +import Data.Time (Day, NominalDiffTime, TimeOfDay, secondsToNominalDiffTime) +import Data.Word (Word8) +import Text.Megaparsec (MonadParsec, Token) +import Text.Megaparsec.Byte (char, char', digitChar, space, space1) +import Text.Megaparsec.Time.Common ( + DayResult, + mkDateParser, + mkDayParser, + mkDurationParser, + mkGregorianDayParser, + mkTimeParser, + ) +import Text.Megaparsec.Utils.Byte (posNumParser) + +fromChar :: Char -> Word8 +fromChar = toEnum . fromEnum + +toChar :: Word8 -> Char +toChar = toEnum . fromIntegral + +-- | Parse a tuple containing a day or not, and a 'TimeOfDay', separated with a +-- space. +dateParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m (Maybe DayResult, TimeOfDay) +dateParser = mkDateParser space1 dayParser timeParser + +-- | Parse a day, see 'Text.Megaparsec.Time.Char.dayParser'. +dayParser + :: MonadParsec e s m + => Token s ~ Word8 + => m DayResult +dayParser = mkDayParser char' fromChar digitChar toChar + +-- | Parse a 'NominalDiffTime' using strings like @1h23m45s@, with all +-- components being optional as long as one is present. +durationParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m NominalDiffTime +durationParser = mkDurationParser space hoursParser minutesParser secondsParser + +-- | Parse a Gregorian 'Day' from a @%F@ or @%d\/%m\/%Y@ format. +gregorianDayParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m Day +gregorianDayParser = mkGregorianDayParser toChar + +-- | Parse a 'NominalDiffTime' from a number of hours from a string like @1h@. +hoursParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m NominalDiffTime +hoursParser = secondsToNominalDiffTime . (* 3600) <$> posNumParser <* char (fromChar 'h') + +-- | Parse a 'NominalDiffTime' from a number of minutes from a string like @1m@. +minutesParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m NominalDiffTime +minutesParser = secondsToNominalDiffTime . (* 60) <$> posNumParser <* char (fromChar 'm') + +-- | Parse a 'NominalDiffTime' from a number of seconds from a string like @1s@. +secondsParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m NominalDiffTime +secondsParser = secondsToNominalDiffTime <$> posNumParser <* optional (char (fromChar 's')) + +-- | Parse a 'TimeOfDay' from a string like @01:23@. +timeParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Word8 + => m TimeOfDay +timeParser = mkTimeParser digitChar (char (fromChar ':')) toChar diff --git a/src/Text/Megaparsec/Time/Char.hs b/src/Text/Megaparsec/Time/Char.hs new file mode 100644 index 0000000..cca35cb --- /dev/null +++ b/src/Text/Megaparsec/Time/Char.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +-- | +-- Module : Text.Megaparsec.Time.Char +-- Description : Time parsers for streams of 'Char'. +-- Copyright : (c) drlkf, 2024 +-- License : GPL-3 +-- Maintainer : drlkf@drlkf.net +-- Stability : experimental +-- +-- Parsers for types related to time for streams of 'Char' ('String', 'Text', +-- lazy 'Text'). +module Text.Megaparsec.Time.Char ( + -- * Simple parsers + hoursParser, + minutesParser, + secondsParser, + timeParser, + dayParser, + gregorianDayParser, + + -- * Composite parsers + dateParser, + durationParser, +) where + +import Control.Applicative (optional) +import Data.Time (Day, NominalDiffTime, TimeOfDay, secondsToNominalDiffTime) +import Text.Megaparsec (MonadParsec, Token) +import Text.Megaparsec.Char (char, char', digitChar, space, space1) +import Text.Megaparsec.Time.Common ( + DayResult, + mkDateParser, + mkDayParser, + mkDurationParser, + mkGregorianDayParser, + mkTimeParser, + ) +import Text.Megaparsec.Utils.Char (posNumParser) + +-- | Parse a tuple containing a day or not, and a 'TimeOfDay', separated with a +-- space. +dateParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m (Maybe DayResult, TimeOfDay) +dateParser = mkDateParser space1 dayParser timeParser + +-- | Parse a day using one of the following, all case-insensitive: +-- +-- * a short (3-letters) or long day name e.g @mon@ or @monday@ +-- * @yesterday@ or @tomorrow@ +-- * a day number relative to the current day i.e @+2@ is two days from today +-- * an absolute number for a 'Data.Time.DayOfWeek', refer to its 'Num' instance for more information. +dayParser + :: MonadParsec e s m + => Token s ~ Char + => m DayResult +dayParser = mkDayParser char' id digitChar id + +-- | Parse a 'NominalDiffTime' using strings like @1h23m45s@, with all +-- components being optional as long as one is present. +durationParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m NominalDiffTime +durationParser = mkDurationParser space hoursParser minutesParser secondsParser + +-- | Parse a Gregorian 'Day' from a @%F@ or @%d\/%m\/%Y@ format. +gregorianDayParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m Day +gregorianDayParser = mkGregorianDayParser id + +-- | Parse a 'NominalDiffTime' from a number of hours from a string like @1h@. +hoursParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m NominalDiffTime +hoursParser = secondsToNominalDiffTime . (* 3600) <$> posNumParser <* char 'h' + +-- | Parse a 'NominalDiffTime' from a number of minutes from a string like @1m@. +minutesParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m NominalDiffTime +minutesParser = secondsToNominalDiffTime . (* 60) <$> posNumParser <* char 'm' + +-- | Parse a 'NominalDiffTime' from a number of seconds from a string like @1s@. +secondsParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m NominalDiffTime +secondsParser = secondsToNominalDiffTime <$> posNumParser <* optional (char 's') + +-- | Parse a 'TimeOfDay' from a string like @01:23@. +timeParser + :: MonadFail m + => MonadParsec e s m + => Token s ~ Char + => m TimeOfDay +timeParser = mkTimeParser digitChar (char ':') id diff --git a/src/Text/Megaparsec/Time/Common.hs b/src/Text/Megaparsec/Time/Common.hs new file mode 100644 index 0000000..44e6f46 --- /dev/null +++ b/src/Text/Megaparsec/Time/Common.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE FlexibleContexts #-} + +-- | +-- Module : Text.Megaparsec.Time.Common +-- Description : Stream-agnostic time parser builders. +-- Copyright : (c) drlkf, 2024 +-- License : GPL-3 +-- Maintainer : drlkf@drlkf.net +-- Stability : experimental +-- +-- Builders shared by 'Text.Megaparsec.Time.Char' and 'Text.Megaparsec.Time.Byte'. +module Text.Megaparsec.Time.Common ( + -- * Types + DayResult, + + -- * Shared parser builders + mkDateParser, + mkDayParser, + mkDurationParser, + mkGregorianDayParser, + mkTimeParser, +) where + +import Control.Applicative (many, optional, (<|>)) +import Control.Monad (replicateM) +import Control.Monad.Combinators (choice, some) +import Data.Foldable (traverse_) +import Data.Functor (($>)) +import Data.Maybe (fromMaybe) +import Data.Time ( + Day, + DayOfWeek (..), + NominalDiffTime, + TimeOfDay (..), + defaultTimeLocale, + makeTimeOfDayValid, + parseTimeM, + ) +import Text.Megaparsec (MonadParsec, Token, anySingle, try) +import Text.Printf (printf) + +-- | Representation of a parser result with either a number of days relative to +-- the current day, or a 'DayOfWeek'. +type DayResult = Either Int DayOfWeek + +-- | Build a parser for a tuple containing a day or not, and a 'TimeOfDay', +-- separated with a space. +mkDateParser + :: MonadParsec e s m + => m () + -- ^ Non-empty space parser. + -> m DayResult + -- ^ Day parser. + -> m TimeOfDay + -- ^ Time parser. + -> m (Maybe DayResult, TimeOfDay) +mkDateParser space1 day time = (,) <$> optional (try (day <* space1)) <*> time + +-- | Build a case-insensitive day parser, see 'Text.Megaparsec.Time.Char.dayParser'. +mkDayParser + :: MonadParsec e s m + => (Token s -> m (Token s)) + -- ^ Case-insensitive single-token parser. + -> (Char -> Token s) + -- ^ Convert a literal 'Char' to a token. + -> m (Token s) + -- ^ Digit parser. + -> (Token s -> Char) + -- ^ Convert a token to a 'Char'. + -> m DayResult +mkDayParser char' fromChar digitChar toChar = + choice + [ Right <$> longDay + , Right <$> shortDay + , Left <$> (str "yesterday" $> -1) + , Left <$> (str "tomorrow" $> 1) + , Right <$> absoluteDay + , Left <$> relativeDay + ] + where + str = try . traverse_ (char' . fromChar) + shortDay = choice $ map (dayString (take 3 . show)) weekDays + longDay = choice $ map (dayString show) weekDays + dayString f d = str (f d) $> d + weekDays = [Monday .. Friday] + sign = (str "-" $> negate) <|> (str "+" $> id) + digits = map toChar <$> some digitChar + absoluteDay = toEnum . read <$> digits + relativeDay = ($) <$> sign <*> (read <$> digits) + +-- | Build a 'NominalDiffTime' parser from hours, minutes and seconds parsers, +-- with all components being optional as long as one is present. +mkDurationParser + :: MonadParsec e s m + => m () + -- ^ Space parser. + -> m NominalDiffTime + -- ^ Hours parser. + -> m NominalDiffTime + -- ^ Minutes parser. + -> m NominalDiffTime + -- ^ Seconds parser. + -> m NominalDiffTime +mkDurationParser space hoursParser minutesParser secondsParser = + try hours <|> try minutes <|> secondsParser + where + hours = do + h <- hoursParser <* space + m <- fromMaybe 0 <$> optional (try minutes) + s <- fromMaybe 0 <$> optional secondsParser + + return (h + m + s) + + minutes = do + m <- minutesParser <* space + s <- fromMaybe 0 <$> optional secondsParser + + return (m + s) + +-- | Build a Gregorian 'Day' parser from a @%F@ or @%d\/%m\/%Y@ format. +mkGregorianDayParser + :: MonadFail m + => MonadParsec e s m + => (Token s -> Char) + -- ^ Convert a token to a 'Char'. + -> m Day +mkGregorianDayParser toChar = do + s <- map toChar <$> many anySingle + + let parseTime = flip (parseTimeM False defaultTimeLocale) s + + parseTime "%F" <|> parseTime "%d/%m/%Y" + +-- | Build a 'TimeOfDay' parser from a string like @01:23@. +mkTimeParser + :: MonadFail m + => m t + -- ^ Digit parser. + -> m t + -- ^ Colon parser. + -> (t -> Char) + -- ^ Convert a token to a 'Char'. + -> m TimeOfDay +mkTimeParser digitChar colon toChar = do + h <- read . map toChar <$> replicateM 2 digitChar <* colon + m <- read . map toChar <$> replicateM 2 digitChar + + maybe + (fail (printf "invalid hours or minutes %02d:%02d" h m)) + pure + (makeTimeOfDayValid h m 0) diff --git a/stack-lts-22.yaml b/stack-lts-22.yaml index 133f234..b3037d9 100644 --- a/stack-lts-22.yaml +++ b/stack-lts-22.yaml @@ -3,5 +3,5 @@ resolver: lts-22.44 packages: - . extra-deps: - - megaparsec-utils-0.1.1.3 + - megaparsec-utils-0.2.0.0 ... diff --git a/stack-lts-23.yaml b/stack-lts-23.yaml index 30c8c92..542a006 100644 --- a/stack-lts-23.yaml +++ b/stack-lts-23.yaml @@ -3,5 +3,5 @@ resolver: lts-23.28 packages: - . extra-deps: - - megaparsec-utils-0.1.1.3 + - megaparsec-utils-0.2.0.0 ... diff --git a/stack.yaml b/stack.yaml index 2ea1613..146dcfc 100644 --- a/stack.yaml +++ b/stack.yaml @@ -3,5 +3,5 @@ resolver: lts-24.56 packages: - . extra-deps: - - megaparsec-utils-0.1.1.3 + - megaparsec-utils-0.2.0.0 ... diff --git a/stack.yaml.lock b/stack.yaml.lock index 561e8d4..15622be 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -5,12 +5,12 @@ packages: - completed: - hackage: megaparsec-utils-0.1.1.3@sha256:32f1df297f0a3fa56d9aed552a7212da735cf24cb53761b7c629c3883441880a,2073 + hackage: megaparsec-utils-0.2.0.0@sha256:6934c23b0ae24392ad21fd5c9382e4466ce75223244ca4d6fcb235c36bc8f39b,2262 pantry-tree: - sha256: d7db87d6acde0a34dcc3abba9d48da147854a2e54b1aa04a9a04de691b18e5ed - size: 410 + sha256: 99b4518330360212e34c17ee59c0b942ded2861d541375997e9484564142be29 + size: 661 original: - hackage: megaparsec-utils-0.1.1.3 + hackage: megaparsec-utils-0.2.0.0 snapshots: - completed: sha256: 121a2b65e6842f67819409330694d068e6276f64df87faaf2a66c0016ddf277b diff --git a/test/Text/Megaparsec/TimeStreamSpec.hs b/test/Text/Megaparsec/TimeStreamSpec.hs new file mode 100644 index 0000000..08df75a --- /dev/null +++ b/test/Text/Megaparsec/TimeStreamSpec.hs @@ -0,0 +1,51 @@ +module Text.Megaparsec.TimeStreamSpec ( + spec, +) where + +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time (DayOfWeek (..), TimeOfDay (..), fromGregorian) +import Data.Void (Void) +import Test.Hspec (Spec, describe, it, shouldBe) +import Text.Megaparsec (ParseErrorBundle, Parsec, runParser) +import qualified Text.Megaparsec.Time.Byte as Byte +import qualified Text.Megaparsec.Time.Char as Char + +parseText :: Parsec Void Text a -> Text -> Either (ParseErrorBundle Text Void) a +parseText p = runParser p "test" + +parseBytes + :: Parsec Void ByteString a + -> ByteString + -> Either (ParseErrorBundle ByteString Void) a +parseBytes p = runParser p "test" + +spec :: Spec +spec = do + describe "Char parsers on Text" $ do + it "dateParser" $ + parseText Char.dateParser (T.pack "Tue 11:03") + `shouldBe` Right (Just (Right Tuesday), TimeOfDay 11 3 0) + + it "durationParser" $ + parseText Char.durationParser (T.pack "1h 2m 3s") `shouldBe` Right 3723 + + it "gregorianDayParser" $ + parseText Char.gregorianDayParser (T.pack "25/10/2026") + `shouldBe` Right (fromGregorian 2026 10 25) + + describe "Byte parsers on ByteString" $ do + it "dateParser" $ do + parseBytes Byte.dateParser (B.pack "Tue 11:03") + `shouldBe` Right (Just (Right Tuesday), TimeOfDay 11 3 0) + parseBytes Byte.dateParser (B.pack "-2 11:03") + `shouldBe` Right (Just (Left (-2)), TimeOfDay 11 3 0) + + it "durationParser" $ + parseBytes Byte.durationParser (B.pack "1h 2m 3s") `shouldBe` Right 3723 + + it "gregorianDayParser" $ + parseBytes Byte.gregorianDayParser (B.pack "2026-10-25") + `shouldBe` Right (fromGregorian 2026 10 25)