chronos 1.1.2 → 1.1.3
raw patch · 4 files changed
+384/−177 lines, 4 filesdep +bytebuilddep +byteslicedep +bytesmithdep −clockdep ~aesondep ~attoparsecdep ~basePVP ok
version bump matches the API change (PVP)
Dependencies added: bytebuild, byteslice, bytesmith, natural-arithmetic, text-short
Dependencies removed: clock
Dependency ranges changed: aeson, attoparsec, base
API changes (from Hackage documentation)
+ Chronos: boundedBuilderUtf8BytesIso8601 :: OffsetDatetime -> Builder 50
+ Chronos: boundedBuilderUtf8BytesIso8601Zoneless :: Datetime -> Builder 44
+ Chronos: decodeShortTextIso8601 :: ShortText -> Maybe OffsetDatetime
+ Chronos: decodeShortTextIso8601Zoneless :: ShortText -> Maybe Datetime
+ Chronos: decodeUtf8BytesIso8601 :: Bytes -> Maybe OffsetDatetime
+ Chronos: decodeUtf8BytesIso8601Zoneless :: Bytes -> Maybe Datetime
+ Chronos: encodeShortTextIso8601 :: OffsetDatetime -> ShortText
+ Chronos: encodeShortTextIso8601Zoneless :: Datetime -> ShortText
+ Chronos: encodeShortTextIso8601Zulu :: Datetime -> ShortText
+ Chronos: parserUtf8BytesIso8601 :: Parser () s OffsetDatetime
Files
- bench/Bench.hs +86/−45
- chronos.cabal +37/−60
- src/Chronos.hs +230/−67
- test/Spec.hs +31/−5
bench/Bench.hs view
@@ -7,81 +7,53 @@ module Main where -import qualified Data.Vector.Unboxed as UVector+import Control.Applicative import Criterion.Main import Data.Int-import Data.Maybe (fromMaybe)+import Data.Word+ import Control.DeepSeq (($!!), NFData(..), deepseq)-import qualified Data.ByteString.Char8 as BS8-import Test.QuickCheck (arbitrary, generate)+import Control.Monad (when) import Data.Attoparsec.ByteString.Char8 (Parser, parseOnly, char)-import qualified Data.Attoparsec.Zepto as Z import Data.Foldable (traverse_)-import Data.Word-import Control.Monad (when)-import Control.Applicative+import Data.Maybe (fromMaybe) import Data.Text.Lazy.Builder (toLazyText)+import Data.Text.Short (ShortText)+import Test.QuickCheck (arbitrary, generate) -import qualified Data.Time as Time+import qualified Chronos+import qualified Data.Attoparsec.Zepto as Z+import qualified Data.ByteString.Char8 as BS8+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Short as TS import qualified Data.Thyme as Thyme+import qualified Data.Time as Time+import qualified Data.Vector.Unboxed as UVector import qualified System.Locale as Thyme-import qualified Chronos main :: IO () main = do utcthyme <- generate arbitrary :: IO Thyme.UTCTime let- isoFormatString = "%Y-%m-%dT%H:%M:%S" renderIsoTime = Thyme.formatTime Thyme.defaultTimeLocale isoFormatString- timeParser :: String -> Time.UTCTime- timeParser =- fromMaybe (error "Failed to parse in timeParser")- . Time.parseTimeM True Time.defaultTimeLocale isoFormatString- thymeParser :: String -> Thyme.UTCTime- thymeParser =- fromMaybe (error "Failed to parse in thymeParser")- . Thyme.parseTime Thyme.defaultTimeLocale isoFormatString- thymeAttoparsec :: BS8.ByteString -> Thyme.UTCTime- thymeAttoparsec =- Thyme.buildTime @Thyme.UTCTime- . either error id- . parseOnly (Thyme.timeParser Thyme.defaultTimeLocale isoFormatString) -- With chronos, we explicitly convert the ymdhms time to the -- nanoseconds since the epoch. This is done to make the benchmark -- compare apples to apples. Both thyme and time are computing epoch -- times, so we want chronos to do the same.- chronosAttoparsec :: BS8.ByteString -> Chronos.Time- chronosAttoparsec =- either error Chronos.datetimeToTime- . parseOnly (Chronos.parserUtf8_YmdHMS Chronos.w3c)- chronosZepto :: BS8.ByteString -> Chronos.Time- chronosZepto =- either error Chronos.datetimeToTime- . Z.parse (Chronos.zeptoUtf8_YmdHMS Chronos.w3c) dmy = "%d:%m:%y."- hms = "%%H:%M:%S."+ hms = "%H:%M:%S." dmyhms = isoFormatString timePretty = Time.formatTime Time.defaultTimeLocale thymePretty = Thyme.formatTime Thyme.defaultTimeLocale- chronosPrettyDmy = toLazyText- . Chronos.builder_Dmy (Just ':')- . Chronos.datetimeDate- . Chronos.timeToDatetime- chronosPrettyHMS = toLazyText- . Chronos.builder_HMS Chronos.SubsecondPrecisionAuto (Just ':')- . Chronos.datetimeTime- . Chronos.timeToDatetime- chronosPrettyYmdHMS = toLazyText- . Chronos.builder_YmdHMS Chronos.SubsecondPrecisionAuto Chronos.w3c- . Chronos.timeToDatetime timeTime <- Time.getCurrentTime thymeTime <- Thyme.getCurrentTime chronosTime <- Chronos.now string <- return $!! renderIsoTime utcthyme- bytestring <- return $!! BS8.pack (renderIsoTime utcthyme)+ bytestring <- return $!! BS8.pack string+ shortText <- return $!! TS.pack string defaultMain [ bgroup "parsing"@@ -90,6 +62,7 @@ , bench "Thyme.timeParser" $ nf thymeAttoparsec bytestring , bench "Chronos.parserUtf8_YmdHMS" $ nf chronosAttoparsec bytestring , bench "Chronos.zeptoUtf8_YmdHMS" $ nf chronosZepto bytestring+ , bench "Chronos.decodeShortTextIso8601" $ nf chronosIso8601 shortText ] , bgroup "prettyPrint"@@ -108,5 +81,73 @@ , bench "Thyme.formatTime" $ nf (thymePretty dmyhms) thymeTime , bench "Chronos.builder_YmdHMS" $ nf chronosPrettyYmdHMS chronosTime ]+ , bgroup "ISO-8601-Zulu"+ [ bench "Chronos.encodeShortTextIso8601Zulu" $ nf encodeChronosIso8601Zulu chronosTime+ ] ] ]++encodeChronosIso8601Zulu :: Chronos.Time -> ShortText+encodeChronosIso8601Zulu !t =+ Chronos.encodeShortTextIso8601Zulu (Chronos.timeToDatetime t)++chronosIso8601 :: ShortText -> Chronos.Time+{-# noinline chronosIso8601 #-}+chronosIso8601 !t = case Chronos.decodeShortTextIso8601Zoneless t of+ Just x -> Chronos.datetimeToTime x+ Nothing -> errorWithoutStackTrace "chronosIso8601: decode failure"++chronosZepto :: BS8.ByteString -> Chronos.Time+{-# noinline chronosZepto #-}+chronosZepto !bs = either error Chronos.datetimeToTime+ (Z.parse (Chronos.zeptoUtf8_YmdHMS Chronos.w3c) bs)+++chronosPrettyYmdHMS :: Chronos.Time -> LT.Text+{-# noinline chronosPrettyYmdHMS #-}+chronosPrettyYmdHMS = toLazyText+ . Chronos.builder_YmdHMS Chronos.SubsecondPrecisionAuto Chronos.w3c+ . Chronos.timeToDatetime++chronosPrettyHMS :: Chronos.Time -> LT.Text+{-# noinline chronosPrettyHMS #-}+chronosPrettyHMS = toLazyText+ . Chronos.builder_HMS Chronos.SubsecondPrecisionAuto (Just ':')+ . Chronos.datetimeTime+ . Chronos.timeToDatetime++chronosPrettyDmy :: Chronos.Time -> LT.Text+{-# noinline chronosPrettyDmy #-}+chronosPrettyDmy = toLazyText+ . Chronos.builder_Dmy (Just ':')+ . Chronos.datetimeDate+ . Chronos.timeToDatetime++chronosAttoparsec :: BS8.ByteString -> Chronos.Time+{-# noinline chronosAttoparsec #-}+chronosAttoparsec =+ either error Chronos.datetimeToTime+ . parseOnly (Chronos.parserUtf8_YmdHMS Chronos.w3c)++timeParser :: String -> Time.UTCTime+{-# noinline timeParser #-}+timeParser =+ fromMaybe (error "Failed to parse in timeParser")+ . Time.parseTimeM True Time.defaultTimeLocale isoFormatString++thymeParser :: String -> Thyme.UTCTime+{-# noinline thymeParser #-}+thymeParser =+ fromMaybe (error "Failed to parse in thymeParser")+ . Thyme.parseTime Thyme.defaultTimeLocale isoFormatString++thymeAttoparsec :: BS8.ByteString -> Thyme.UTCTime+{-# noinline thymeAttoparsec #-}+thymeAttoparsec =+ Thyme.buildTime @Thyme.UTCTime+ . either error id+ . parseOnly (Thyme.timeParser Thyme.defaultTimeLocale isoFormatString)++isoFormatString :: String+{-# noinline isoFormatString #-}+isoFormatString = "%Y-%m-%dT%H:%M:%S"
chronos.cabal view
@@ -1,10 +1,7 @@ cabal-version: 3.0-name:- chronos-version:- 1.1.2-synopsis:- A high-performance time library+name: chronos+version: 1.1.3+synopsis: A high-performance time library description: Chronos is a performance-oriented time library for Haskell, with a straightforward API. The main differences between this@@ -28,27 +25,19 @@ format strings. The approach taken by Chronos is faster and catches more mistakes at compile time, at the cost of being less expressive.-homepage:- https://github.com/andrewthad/chronos-license:- BSD-3-Clause-license-file:- LICENSE-author:- Andrew Martin+homepage: https://github.com/andrewthad/chronos+license: BSD-3-Clause+license-file: LICENSE+author: Andrew Martin maintainer: Andrew Martin <andrew.thaddeus@gmail.com> chessai <chessai1996@gmail.com>-copyright:- 2016 Andrew Martin-category:- Data, Time, Parsing, Development-build-type:- Simple+copyright: 2016 Andrew Martin+category: Data, Time, Parsing, Development+build-type: Simple library- hs-source-dirs:- src+ hs-source-dirs: src exposed-modules: Chronos Chronos.Types@@ -57,9 +46,9 @@ -- it is OPTIONS_HADDOCK-hidden Chronos.Internal.CTimespec build-depends:- , aeson >= 1.1 && < 1.5- , attoparsec >= 0.13 && < 0.14- , base >= 4.9 && < 5+ , aeson >= 1.1 && < 1.6+ , attoparsec >= 0.13 && < 0.15+ , base >= 4.12 && < 5 , bytestring >= 0.10 && < 0.11 , deepseq >= 1.4.4.0 , hashable >= 1.2 && < 1.4@@ -68,29 +57,27 @@ , text >= 1.2 && < 1.3 , torsor >= 0.1 && < 0.2 , vector >= 0.11 && < 0.13+ , bytebuild >= 0.3.8 && < 0.4+ , bytesmith >= 0.3.7 && < 0.4+ , byteslice >= 0.2.5.2 && <0.3+ , text-short >= 0.1.3 && <0.2+ , natural-arithmetic >= 0.1.2 && <0.2 if os(windows) build-depends: Win32 >= 2.2 && < 2.9- if impl(ghc < 8.4)- build-depends: clock >= 0.7 && < 0.9- default-language:- Haskell2010- c-sources:- src/cbits/hs-time.c+ default-language: Haskell2010+ c-sources: src/cbits/hs-time.c ghc-options: -Wall -O2 test-suite chronos-test- type:- exitcode-stdio-1.0- hs-source-dirs:- test- main-is:- Spec.hs+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs build-depends: , HUnit , QuickCheck- , aeson >= 1.1 && < 1.5+ , aeson >= 1.1 && < 1.6 , attoparsec , base , bytestring@@ -104,16 +91,12 @@ ghc-options: -threaded -rtsopts -with-rtsopts=-N- default-language:- Haskell2010+ default-language: Haskell2010 test-suite doctest- type:- exitcode-stdio-1.0- hs-source-dirs:- test- main-is:- Doctests.hs+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Doctests.hs build-depends: , QuickCheck , base@@ -121,16 +104,12 @@ , deepseq >= 1.4.4.0 , doctest >= 0.10 , torsor >= 0.1 && < 0.2- default-language:- Haskell2010+ default-language: Haskell2010 benchmark bench- type:- exitcode-stdio-1.0- hs-source-dirs:- bench- main-is:- Bench.hs+ type: exitcode-stdio-1.0+ hs-source-dirs: bench+ main-is: Bench.hs build-depends: , QuickCheck , attoparsec@@ -142,14 +121,12 @@ , deepseq >= 1.4.4.0 , old-locale , text+ , text-short , thyme , time , vector- default-language:- Haskell2010+ default-language: Haskell2010 source-repository head- type:- git- location:- https://github.com/andrewthad/chronos+ type: git+ location: https://github.com/andrewthad/chronos
src/Chronos.hs view
@@ -3,7 +3,9 @@ , CPP , DeriveGeneric , GeneralizedNewtypeDeriving+ , LambdaCase , MultiParamTypeClasses+ , NumericUnderscores , OverloadedStrings , RecordWildCards , ScopedTypeVariables@@ -52,10 +54,6 @@ -- ** Duration , stopwatch , stopwatch_-#if !MIN_VERSION_base(4,11,0)- , stopwatchWith- , stopwatchWith_-#endif -- ** Construction , datetimeFromYmdhms , timeFromYmdhms@@ -199,6 +197,13 @@ , parserUtf8_YmdHMS , parserUtf8_YmdHMS_opt_S , zeptoUtf8_YmdHMS+ -- *** UTF-8 Bytes+ , boundedBuilderUtf8BytesIso8601Zoneless+ , decodeUtf8BytesIso8601Zoneless+ -- *** Short Text+ , decodeShortTextIso8601Zoneless+ , encodeShortTextIso8601Zulu+ , encodeShortTextIso8601Zoneless -- ** Offset Datetime -- *** Text , encode_YmdHMSz@@ -215,6 +220,13 @@ , parserUtf8_YmdHMSz , builderUtf8_YmdIMS_p_z , builderUtf8W3Cz+ -- *** UTF-8 Bytes+ , parserUtf8BytesIso8601+ , boundedBuilderUtf8BytesIso8601+ , decodeUtf8BytesIso8601+ -- *** ShortText+ , decodeShortTextIso8601+ , encodeShortTextIso8601 -- ** Offset -- *** Text , encodeOffset@@ -277,6 +289,8 @@ import Control.Monad import Data.Aeson (FromJSON,ToJSON,FromJSONKey,ToJSONKey) import Data.Attoparsec.Text (Parser)+import Data.Bool (bool)+import Data.Bytes (Bytes) import Data.ByteString (ByteString) import Data.Char (isDigit) import Data.Foldable@@ -284,36 +298,42 @@ import Data.Int (Int64) import Data.Primitive import Data.Text (Text)+import Data.Text.Short (ShortText) import Data.Vector (Vector) import Data.Word (Word64, Word8) import Foreign.Storable+import GHC.Clock (getMonotonicTimeNSec) import GHC.Generics (Generic) import Torsor+import qualified Arithmetic.Lte as Lte+import qualified Arithmetic.Nat as Nat import qualified Data.Aeson as AE import qualified Data.Aeson.Encoding as AEE import qualified Data.Aeson.Types as AET import qualified Data.Attoparsec.ByteString.Char8 as AB import qualified Data.Attoparsec.Text as AT import qualified Data.Attoparsec.Zepto as Z+import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Builder.Bounded as Bounded+import qualified Data.Bytes.Parser as BVP+import qualified Data.Bytes.Parser.Latin as Latin import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString.Short.Internal as SBS import qualified Data.Semigroup as SG import qualified Data.Text as Text import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder.Int as TB import qualified Data.Text.Read as Text+import qualified Data.Text.Short as TS+import qualified Data.Text.Short.Unsafe as TS import qualified Data.Vector as Vector import qualified Data.Vector.Generic as GVector import qualified Data.Vector.Generic.Mutable as MGVector import qualified Data.Vector.Primitive as PVector import qualified Data.Vector.Unboxed as UVector-#if MIN_VERSION_base(4,11,0)-import GHC.Clock (getMonotonicTimeNSec)-#else-import qualified System.Clock as CLK-#endif #ifdef mingw32_HOST_OS import System.Win32.Time (SYSTEMTIME(..))@@ -322,10 +342,6 @@ import Chronos.Internal.CTimespec (getPosixNanoseconds) #endif -#if !MIN_VERSION_base(4,11,0)-import Data.Semigroup (Semigroup, (<>))-#endif- -- $setup -- >>> import Test.QuickCheck hiding (within) -- >>> import Test.QuickCheck.Gen@@ -552,7 +568,6 @@ epoch :: Time epoch = Time 0 -#if MIN_VERSION_base(4,11,0) -- | Measures the time it takes to run an action and evaluate -- its result to WHNF. This measurement uses a monotonic clock -- instead of the standard system clock.@@ -572,44 +587,7 @@ _ <- action end <- getMonotonicTimeNSec pure (Timespan (fromIntegral (end - start)))-#else --- | Measures the time it takes to run an action and evaluate--- its result to WHNF. This measurement uses a monotonic clock--- instead of the standard system clock.-stopwatch :: IO a -> IO (Timespan, a)-stopwatch = stopwatchWith CLK.Monotonic---- | Measures the time it takes to run an action. The result--- is discarded. This measurement uses a monotonic clock--- instead of the standard system clock.-stopwatch_ :: IO a -> IO Timespan-stopwatch_ = stopwatchWith_ CLK.Monotonic---- | Variant of 'stopwatch' that accepts a clock type. Users--- need to import @System.Clock@ from the @clock@ package--- in order to provide the clock type.-stopwatchWith :: CLK.Clock -> IO a -> IO (Timespan, a)-stopwatchWith c action = do- start <- CLK.getTime c- a <- action >>= evaluate- end <- CLK.getTime c- pure (timeSpecToTimespan (CLK.diffTimeSpec end start),a)-{-# DEPRECATED stopwatchWith "stopwatchWith will be removed in a future majour version" #-}---- | Variant of 'stopwatch_' that accepts a clock type.-stopwatchWith_ :: CLK.Clock -> IO a -> IO Timespan-stopwatchWith_ c action = do- start <- CLK.getTime c- _ <- action- end <- CLK.getTime c- pure (timeSpecToTimespan (CLK.diffTimeSpec end start))-{-# DEPRECATED stopwatchWith_ "stopwatchWith_ will be removed in a future majour version" #-}--timeSpecToTimespan :: CLK.TimeSpec -> Timespan-timeSpecToTimespan (CLK.TimeSpec s ns) = Timespan (s * 1000000000 + ns)-#endif- -- UtcTime. Used internally only. data UtcTime = UtcTime {-# UNPACK #-} !Day -- day@@ -1238,14 +1216,17 @@ nanoseconds <- ( do _ <- AT.char '.' numberOfZeroes <- countZeroes- x <- AT.decimal- let totalDigits = countDigits x + numberOfZeroes- result = if totalDigits == 9- then x- else if totalDigits < 9- then x * raiseTenTo (9 - totalDigits)- else quot x (raiseTenTo (totalDigits - 9))- pure (fromIntegral result)+ AT.peekChar >>= \case+ Just c | c >= '0' && c <= '9' -> do+ x <- AT.decimal+ let totalDigits = countDigits x + numberOfZeroes+ result = if totalDigits == 9+ then x+ else if totalDigits < 9+ then x * raiseTenTo (9 - totalDigits)+ else quot x (raiseTenTo (totalDigits - 9))+ pure (fromIntegral result)+ _ -> pure 0 ) <|> pure 0 pure (s * 1000000000 + nanoseconds) @@ -1844,18 +1825,22 @@ parseSecondsAndNanosecondsUtf8 = do s' <- parseFixedDigitsIntBS 2 let !s = fromIntegral s' :: Int64+ -- TODO: whoops, this should probably be gt 59, not 60 when (s > 60) (fail "seconds must be between 0 and 60") nanoseconds <- ( do _ <- AB.char '.' numberOfZeroes <- countZeroesUtf8- x <- AB.decimal- let totalDigits = countDigits x + numberOfZeroes- result = if totalDigits == 9- then x- else if totalDigits < 9- then x * raiseTenTo (9 - totalDigits)- else quot x (raiseTenTo (totalDigits - 9))- pure (fromIntegral result)+ AB.peekChar >>= \case+ Just c | c >= '0' && c <= '9' -> do+ x <- AB.decimal+ let totalDigits = countDigits x + numberOfZeroes+ result = if totalDigits == 9+ then x+ else if totalDigits < 9+ then x * raiseTenTo (9 - totalDigits)+ else quot x (raiseTenTo (totalDigits - 9))+ pure (fromIntegral result)+ _ -> pure 0 ) <|> pure 0 pure (s * 1000000000 + nanoseconds) @@ -3185,3 +3170,181 @@ , timePartsSubsecond = fromIntegral subsecond , timePartsOffset = getOffset o }++-- | Decode an ISO-8601-encode datetime. The encoded time must not by suffixed+-- by an offset. Any offset (e.g. @-05:00@, @+00:00@, @Z@) will cause a decode+-- failure.+decodeShortTextIso8601Zoneless :: ShortText -> Maybe Chronos.Datetime+decodeShortTextIso8601Zoneless !t = decodeUtf8BytesIso8601Zoneless+ (Bytes.fromShortByteString (TS.toShortByteString t))++-- | Decode an ISO-8601-encode datetime. The encoded time must include an offset+-- (e.g. @-05:00@, @+00:00@, @Z@).+decodeShortTextIso8601 :: ShortText -> Maybe Chronos.OffsetDatetime+decodeShortTextIso8601 !t = decodeUtf8BytesIso8601+ (Bytes.fromShortByteString (TS.toShortByteString t))++decodeUtf8BytesIso8601Zoneless :: Bytes -> Maybe Chronos.Datetime+decodeUtf8BytesIso8601Zoneless !b =+ BVP.parseBytesMaybe (parserUtf8BytesIso8601Zoneless <* BVP.endOfInput ()) b++decodeUtf8BytesIso8601 :: Bytes -> Maybe Chronos.OffsetDatetime+decodeUtf8BytesIso8601 !b =+ BVP.parseBytesMaybe (parserUtf8BytesIso8601 <* BVP.endOfInput ()) b++parserUtf8BytesIso8601Zoneless :: BVP.Parser () s Chronos.Datetime+{-# noinline parserUtf8BytesIso8601Zoneless #-}+parserUtf8BytesIso8601Zoneless = do+ year <- Latin.decWord ()+ Latin.char () '-'+ month' <- Latin.decWord ()+ let !month = month' - 1+ when (month >= 12) (BVP.fail ())+ Latin.char () '-'+ dayWord <- Latin.decWord ()+ when (dayWord > 31) (BVP.fail ())+ let !date = Chronos.Date+ (Chronos.Year (fromIntegral year))+ (Chronos.Month (fromIntegral month))+ (Chronos.DayOfMonth (fromIntegral dayWord))+ Latin.char () 'T'+ hourWord <- Latin.decWord8 ()+ when (hourWord > 23) (BVP.fail ())+ Latin.char () ':'+ minuteWord <- Latin.decWord8 ()+ when (minuteWord > 59) (BVP.fail ())+ Latin.char () ':'+ sec <- Latin.decWord8 ()+ when (sec > 59) (BVP.fail ())+ !nanos <- Latin.trySatisfy (=='.') >>= \case+ True -> do+ (n,w) <- BVP.measure (Latin.decWord64 ())+ when (n > 9) (BVP.fail ())+ let go !acc !b = case b of+ 0 -> acc+ _ -> go (acc * 10) (b - 1)+ !ns = go w (9 - n)+ pure ns+ False -> pure 0+ let !td = Chronos.TimeOfDay+ (fromIntegral hourWord)+ (fromIntegral minuteWord)+ (fromIntegral @Word64 @Int64 (fromIntegral sec * 1000000000 + nanos))+ pure $! Chronos.Datetime date td++-- | Consume an ISO-8601-encoded datetime with offset. This will consume any of+-- the following:+--+-- > 2021-12-05T23:01:09Z+-- > 2021-12-05T23:01:09.000Z+-- > 2021-12-05T23:01:09.123456789Z+-- > 2021-12-05T23:01:09+05:00+-- > 2021-12-05T23:01:09.357-11:00+parserUtf8BytesIso8601 :: BVP.Parser () s Chronos.OffsetDatetime+{-# noinline parserUtf8BytesIso8601 #-}+parserUtf8BytesIso8601 = do+ dt <- parserUtf8BytesIso8601Zoneless+ off <- Latin.any () >>= \case+ 'Z' -> pure 0+ '+' -> parserBytesOffset+ '-' -> do+ !off <- parserBytesOffset+ pure (negate off)+ _ -> BVP.fail ()+ pure $! Chronos.OffsetDatetime dt (Chronos.Offset off)++-- Should consume exactly five characters: HH:MM. However, the implementation+-- is more generous.+parserBytesOffset :: BVP.Parser () s Int+parserBytesOffset = do+ h <- Latin.decWord8 ()+ Latin.char () ':'+ m <- Latin.decWord8 ()+ let !r = ((fromIntegral @Word8 @Int h) * 60) + fromIntegral @Word8 @Int m+ pure r++encodeShortTextIso8601Zulu :: Datetime -> ShortText+{-# noinline encodeShortTextIso8601Zulu #-}+encodeShortTextIso8601Zulu !dt =+ let !(ByteArray x) = Bounded.run Nat.constant+ ( boundedBuilderUtf8BytesIso8601Zoneless dt+ `Bounded.append`+ Bounded.ascii 'Z'+ )+ in TS.fromShortByteStringUnsafe (SBS.SBS x)++encodeShortTextIso8601Zoneless :: Datetime -> ShortText+{-# noinline encodeShortTextIso8601Zoneless #-}+encodeShortTextIso8601Zoneless !dt =+ let !(ByteArray x) = Bounded.run Nat.constant+ (boundedBuilderUtf8BytesIso8601Zoneless dt)+ in TS.fromShortByteStringUnsafe (SBS.SBS x)++encodeShortTextIso8601 :: OffsetDatetime -> ShortText+{-# noinline encodeShortTextIso8601 #-}+encodeShortTextIso8601 offdt =+ let !(ByteArray x) = Bounded.run Nat.constant+ (boundedBuilderUtf8BytesIso8601 offdt)+ in TS.fromShortByteStringUnsafe (SBS.SBS x)++boundedBuilderUtf8BytesIso8601 :: OffsetDatetime -> Bounded.Builder 50+boundedBuilderUtf8BytesIso8601 (OffsetDatetime dt off) =+ ( boundedBuilderUtf8BytesIso8601Zoneless dt+ `Bounded.append`+ boundedBuilderOffset off+ )++-- | Encode a datetime with ISO-8601. The result does not include any+-- indication of a time zone. If the subsecond part is zero, it is suppressed.+-- Examples of output:+--+-- > 2021-01-05T23:00:51+-- > 2021-01-05T23:00:52.123000000+-- > 2021-01-05T23:00:53.674094347+boundedBuilderUtf8BytesIso8601Zoneless :: Datetime -> Bounded.Builder 44+boundedBuilderUtf8BytesIso8601Zoneless (Datetime (Date (Year y) (Month mth) (DayOfMonth d)) (TimeOfDay h mt sns)) = + let (s,ns) = quotRem sns 1_000_000_000 in+ Bounded.wordDec (fromIntegral y)+ `Bounded.append`+ Bounded.ascii '-'+ `Bounded.append`+ Bounded.wordPaddedDec2 (fromIntegral (mth + 1))+ `Bounded.append`+ Bounded.ascii '-'+ `Bounded.append`+ Bounded.wordPaddedDec2 (fromIntegral d)+ `Bounded.append`+ Bounded.ascii 'T'+ `Bounded.append`+ Bounded.wordPaddedDec2 (fromIntegral h)+ `Bounded.append`+ Bounded.ascii ':'+ `Bounded.append`+ Bounded.wordPaddedDec2 (fromIntegral mt)+ `Bounded.append`+ Bounded.ascii ':'+ `Bounded.append`+ Bounded.wordPaddedDec2 (fromIntegral s)+ `Bounded.append`+ (case ns of+ 0 -> Bounded.weaken @0 @10 Lte.constant Bounded.empty+ _ ->+ Bounded.ascii '.'+ `Bounded.append`+ Bounded.wordPaddedDec9 (fromIntegral ns)+ )++boundedBuilderOffset :: Offset -> Bounded.Builder 6+boundedBuilderOffset (Offset mins) = case mins of+ 0 -> Bounded.weaken @1 @6 Lte.constant (Bounded.ascii 'Z')+ _ -> + let !absMins = fromIntegral @Int @Word (abs mins)+ !absHrs = quot absMins 60+ !absMinutes = rem absMins 60+ in Bounded.ascii (bool '-' '+' (mins > 0))+ `Bounded.append`+ Bounded.wordPaddedDec2 absHrs+ `Bounded.append`+ Bounded.ascii ':'+ `Bounded.append`+ Bounded.wordPaddedDec2 absMinutes
test/Spec.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} module Main (main) where@@ -15,7 +17,8 @@ import Test.Framework (defaultMain,defaultMainWithOpts,testGroup,Test) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.HUnit (Assertion,(@?=),assertBool)-import Test.QuickCheck (Gen, Arbitrary(..),discard,choose,arbitraryBoundedEnum,genericShrink,elements, suchThat)+import Test.QuickCheck (Gen,Arbitrary(..),discard,genericShrink,elements,suchThat)+import Test.QuickCheck (choose,chooseInt,arbitraryBoundedEnum) import Test.QuickCheck.Property (failed,succeeded,Result(..)) import qualified Chronos as C import qualified Data.Attoparsec.ByteString as AttoBS@@ -405,6 +408,12 @@ either (const Nothing) Just $ flip Atto.parseOnly input $ C.parser_YmdHMSz offsetFormat datetimeFormat )+ , testProperty "ISO-8601 Roundtrip" $ propEncodeDecodeIso+ C.encodeShortTextIso8601Zulu+ (\input -> case C.decodeShortTextIso8601 input of+ Just (OffsetDatetime dt (Offset 0)) -> Just dt+ _ -> Nothing+ ) ] , testGroup "Posix Time" [ PH.testCase "Get now" $ do@@ -637,7 +646,21 @@ <$> choose (0,23) <*> choose (0,59) -- never use leap seconds for property-based tests- <*> choose (0,60000000000 - 1)+ <*> ( do subsecPrecision <- chooseInt (0,9)+ secs <- chooseInt (0,59)+ case subsecPrecision of+ 0 -> pure (fromIntegral @Int @Int64 secs * 1_000_000_000)+ _ -> do+ subsecs <- chooseInt (0,((10 :: Int) ^ subsecPrecision) - 1)+ let subsecs' = subsecs * ((10 :: Int) ^ (9 - subsecPrecision))+ if subsecs' < 0 || subsecs' >= 1_000_000_000+ then error "Mistake in Arbitrary instance for TimeOfDay"+ else pure+ ( (fromIntegral @Int @Int64 secs * 1_000_000_000)+ ++ (fromIntegral @Int @Int64 subsecs)+ )+ ) instance Arbitrary Date where arbitrary = Date@@ -660,9 +683,12 @@ instance Arbitrary DatetimeFormat where arbitrary = DatetimeFormat- <$> arbitrary- <*> elements [Nothing, Just '/', Just ':', Just '-']- <*> arbitrary+ <$> elements [Nothing, Just '/', Just ':', Just '-', Just '.']+ <*> elements [Nothing, Just '/', Just ':', Just '-', Just 'T']+ <*> elements [Nothing, Just ':', Just '-']+ shrink (DatetimeFormat a b c)+ | a == Just '-', b == Just 'T', c == Just ':' = []+ | otherwise = [DatetimeFormat (Just '-') (Just 'T') (Just ':')] instance Arbitrary OffsetFormat where arbitrary = arbitraryBoundedEnum