chronos 1.0.5 → 1.0.6
raw patch · 7 files changed
+870/−227 lines, 7 filesdep +doctestdep ~basedep ~hashabledep ~semigroupsPVP ok
version bump matches the API change (PVP)
Dependencies added: doctest
Dependency ranges changed: base, hashable, semigroups, torsor
API changes (from Hackage documentation)
+ Chronos: (...) :: Time -> Time -> TimeInterval
+ Chronos: TimeInterval :: {-# UNPACK #-} !Time -> {-# UNPACK #-} !Time -> TimeInterval
+ Chronos: buildUnboxedMonthMatch :: Unbox a => a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> UnboxedMonthMatch a
+ Chronos: caseDayOfWeek :: DayOfWeekMatch a -> DayOfWeek -> a
+ Chronos: caseUnboxedMonth :: Unbox a => UnboxedMonthMatch a -> Month -> a
+ Chronos: data TimeInterval
+ Chronos: infix 3 ...
+ Chronos: instance GHC.Classes.Eq Chronos.SubsecondPrecision
+ Chronos: instance GHC.Classes.Eq Chronos.TimeInterval
+ Chronos: instance GHC.Classes.Ord Chronos.SubsecondPrecision
+ Chronos: instance GHC.Classes.Ord Chronos.TimeInterval
+ Chronos: instance GHC.Enum.Bounded Chronos.Time
+ Chronos: instance GHC.Enum.Bounded Chronos.TimeInterval
+ Chronos: instance GHC.Read.Read Chronos.SubsecondPrecision
+ Chronos: instance GHC.Read.Read Chronos.TimeInterval
+ Chronos: instance GHC.Show.Show Chronos.SubsecondPrecision
+ Chronos: instance GHC.Show.Show Chronos.TimeInterval
+ Chronos: lowerBound :: TimeInterval -> Time
+ Chronos: singleton :: Time -> TimeInterval
+ Chronos: timeIntervalBuilder :: Time -> Time -> TimeInterval
+ Chronos: timeIntervalToTimespan :: TimeInterval -> Timespan
+ Chronos: upperBound :: TimeInterval -> Time
+ Chronos: whole :: TimeInterval
+ Chronos: width :: TimeInterval -> Timespan
+ Chronos: within :: Time -> TimeInterval -> Bool
+ Chronos.Types: TimeInterval :: {-# UNPACK #-} !Time -> {-# UNPACK #-} !Time -> TimeInterval
+ Chronos.Types: data TimeInterval
Files
- chronos.cabal +108/−62
- src/Chronos.hs +610/−143
- src/Chronos/Internal/CTimespec.hs +5/−2
- src/Chronos/Locale/English.hs +29/−0
- src/Chronos/Types.hs +2/−3
- test/Doctests.hs +8/−0
- test/Spec.hs +108/−17
chronos.cabal view
@@ -1,100 +1,146 @@-name: chronos-version: 1.0.5-synopsis: A performant time library+cabal-version: 2.2+name:+ chronos+version:+ 1.0.6+synopsis:+ A performant time library description:- Performance-oriented time library for haskell. The main differences- between this and "time" are that this library:- .- * Uses machine integers where possible. This means that some time-related- arithmetic should be faster. It also means that the types are incapable- of representing times that are very far in the future or the past.- .- * Provides "ToJSON" and "FromJSON" instances for json serialization.- .- * Provides "Unbox" instances for working with unboxed vectors.- .- * Uses normal non-overloaded haskell functions for encoding and decoding time. It provides- "attoparsec" parsers for both "Text" and "ByteString". Additionally, it- provides functions for encoding time as "Text" or "ByteString". The "time"- library uses accomplishes these with the "Data.Time.Format" module,- which uses UNIX-style datetime format strings. It is expected that- the approach taken in this library is faster and catches more mistakes- at compile time at the cost of being less expressive.- .- * Only provides nanosecond resolution instead of picosecond resolution.-+ Chronos is a performance-oriented time library for Haskell, with a+ straightforward API. The main differences between this+ and the <http://hackage.haskell.org/package/time time> library+ are: -homepage: https://github.com/andrewthad/chronos#readme-license: BSD3-license-file: LICENSE-author: Andrew Martin-maintainer: andrew.thaddeus@gmail.com-copyright: 2016 Andrew Martin-category: web-build-type: Simple-cabal-version: >=1.10+ * Chronos uses machine integers where possible. This means+ that time-related arithmetic should be faster, with the+ drawback that the types are incapable of representing times+ that are very far in the future or the past (because Chronos+ provides nanosecond, rather than picosecond, resolution).+ For most users, this is not a hindrance.+ * Chronos provides 'ToJSON'/'FromJSON' instances for serialisation.+ * Chronos provides 'Unbox' instances for working with unboxed vectors.+ * Chronos provides 'Prim' instances for working with byte arrays/primitive arrays.+ * Chronos uses normal non-overloaded haskell functions for+ encoding and decoding time. It provides <http://hackage.haskell.org/package/attoparsec attoparsec> parsers for both 'Text' and+ 'ByteString'. Additionally, Chronos provides functions for+ encoding time to 'Text' or 'ByteString'. The http://hackage.haskell.org/package/time time> library accomplishes these with the+ <http://hackage.haskell.org/package/time-1.9.3/docs/Data-Time-Format.html Data.Time.Format> module, which uses UNIX-style datetime+ 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+maintainer:+ Andrew Martin <andrew.thaddeus@gmail.com>+ chessai <chessai1996@gmail.com>+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- , Chronos.Locale.English- other-modules:+ Chronos.Types+ Chronos.Locale.English+ -- only exposed for doctests+ -- it is OPTIONS_HADDOCK-hidden Chronos.Internal.CTimespec build-depends:- base >= 4.9 && < 5 , aeson >= 1.1 && < 1.5 , attoparsec >= 0.13 && < 0.14+ , base >= 4.9 && < 5 , bytestring >= 0.10 && < 0.11 , clock >= 0.7 && < 0.9 , hashable >= 1.2 && < 1.3- , primitive >= 0.6 && < 0.7+ , primitive >= 0.7 && < 0.8 , semigroups >= 0.16 && < 0.19 , text >= 1.2 && < 1.3 , torsor >= 0.1 && < 0.2 , vector >= 0.11 && < 0.13- default-language: Haskell2010- c-sources: src/cbits/hs-time.c- ghc-options: -Wall -O2+ 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:- base- , chronos- , text- , bytestring+ , HUnit+ , QuickCheck , attoparsec+ , base+ , bytestring+ , chronos , test-framework+ , test-framework-hunit , test-framework-quickcheck2+ , text+ , torsor+ ghc-options:+ -threaded+ -rtsopts -with-rtsopts=-N+ default-language:+ Haskell2010++test-suite doctest+ type:+ exitcode-stdio-1.0+ hs-source-dirs:+ test+ main-is:+ Doctests.hs+ build-depends: , QuickCheck- , HUnit- , test-framework-hunit- ghc-options: -threaded -rtsopts -with-rtsopts=-N- default-language: Haskell2010+ , base+ , chronos+ , doctest >= 0.10+ 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:- base+ , QuickCheck , attoparsec+ , base , bytestring , chronos , criterion , deepseq , old-locale- , QuickCheck+ , text , thyme , time , vector- , text- 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
@@ -1,17 +1,41 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MagicHash #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeInType #-}-{-# LANGUAGE UnboxedTuples #-}+{-# language+ BangPatterns+ , CPP+ , DeriveGeneric+ , GeneralizedNewtypeDeriving+ , MagicHash+ , MultiParamTypeClasses+ , OverloadedStrings+ , ScopedTypeVariables+ , TypeFamilies+ , TypeInType+ , UnboxedTuples+ #-} -{-# OPTIONS_GHC -Wall #-}+{-| Chronos is a performance-oriented time library for Haskell, with a+ straightforward API. The main differences between this+ and the <http://hackage.haskell.org/package/time time> library+ are: + * Chronos uses machine integers where possible. This means+ that time-related arithmetic should be faster, with the+ drawback that the types are incapable of representing times+ that are very far in the future or the past (because Chronos+ provides nanosecond, rather than picosecond, resolution).+ For most users, this is not a hindrance.+ * Chronos provides 'ToJSON'/'FromJSON' instances for serialisation.+ * Chronos provides 'UVector.Unbox' instances for working with unboxed vectors.+ * Chronos provides 'Prim' instances for working with byte arrays/primitive arrays.+ * Chronos uses normal non-overloaded haskell functions for+ encoding and decoding time. It provides <http://hackage.haskell.org/package/attoparsec attoparsec> parsers for both 'Text' and+ 'ByteString'. Additionally, Chronos provides functions for+ encoding time to 'Text' or 'ByteString'. The http://hackage.haskell.org/package/time time> library accomplishes these with the+ <http://hackage.haskell.org/package/time-1.9.3/docs/Data-Time-Format.html Data.Time.Format> module, which uses UNIX-style datetime+ format strings. The approach taken by Chronos is faster and+ catches more mistakes at compile time, at the cost of being+ less expressive.+ -}+ module Chronos ( -- * Functions -- ** Current@@ -50,7 +74,10 @@ -- ** Matching , buildDayOfWeekMatch , buildMonthMatch+ , buildUnboxedMonthMatch+ , caseDayOfWeek , caseMonth+ , caseUnboxedMonth -- ** Format -- $format , w3c@@ -174,6 +201,16 @@ -- *** UTF-8 ByteString , encodeTimespanUtf8 , builderTimespanUtf8+ -- ** TimeInterval+ , within+ , timeIntervalToTimespan+ , whole+ , singleton+ , lowerBound+ , upperBound+ , width+ , timeIntervalBuilder+ , (...) -- * Types , Day(..) , DayOfWeek(..)@@ -198,6 +235,7 @@ , OffsetFormat(..) , DatetimeLocale(..) , MeridiemLocale(..)+ , TimeInterval(..) ) where import Data.Text (Text)@@ -245,26 +283,84 @@ import Data.Semigroup (Semigroup, (<>)) #endif +-- $setup+-- >>> import Test.QuickCheck hiding (within)+-- >>> import Test.QuickCheck.Gen+-- >>> import Data.Maybe (isJust)+-- >>> :set -XStandaloneDeriving+-- >>> :set -XGeneralizedNewtypeDeriving+-- >>> :set -XScopedTypeVariables+--+-- >>> deriving instance Arbitrary Time+-- >>> :{+-- instance Arbitrary TimeInterval where+-- arbitrary = do+-- t0 <- arbitrary+-- t1 <- suchThat arbitrary (>= t0)+-- pure (TimeInterval t0 t1)+-- instance Arbitrary TimeOfDay where+-- arbitrary = TimeOfDay+-- <$> choose (0,23)+-- <*> choose (0,59)+-- <*> choose (0, 60000000000 - 1)+-- instance Arbitrary Date where+-- arbitrary = Date+-- <$> fmap Year (choose (1800,2100))+-- <*> fmap Month (choose (0,11))+-- <*> fmap DayOfMonth (choose (1,28))+-- instance Arbitrary Datetime where+-- arbitrary = Datetime <$> arbitrary <*> arbitrary+-- instance Arbitrary OffsetDatetime where+-- arbitrary = OffsetDatetime <$> arbitrary <*> arbitrary+-- instance Arbitrary DatetimeFormat where+-- arbitrary = DatetimeFormat+-- <$> arbitrary+-- <*> elements [ Nothing, Just '/', Just ':', Just '-']+-- <*> arbitrary+-- instance Arbitrary OffsetFormat where+-- arbitrary = arbitraryBoundedEnum+-- shrink = genericShrink+-- instance Arbitrary Offset where+-- arbitrary = fmap Offset (choose ((-24) * 60, 24 * 60))+-- instance Arbitrary SubsecondPrecision where+-- arbitrary = frequency+-- [ (1, pure SubsecondPrecisionAuto)+-- , (1, SubsecondPrecisionFixed <$> choose (0,9))+-- ]+-- instance Arbitrary Day where+-- arbitrary = fmap Day (choose (0,50000))+-- :}+--++-- | A 'Timespan' representing a single second. second :: Timespan second = Timespan 1000000000 +-- | A 'Timespan' representing a single minute. minute :: Timespan minute = Timespan 60000000000 +-- | A 'Timespan' representing a single hour. hour :: Timespan hour = Timespan 3600000000000 +-- | A 'Timespan' representing a single day. day :: Timespan day = Timespan 86400000000000 +-- | A 'Timespan' representing a single week. week :: Timespan week = Timespan 604800000000000 -- | Convert 'Time' to 'Datetime'.+--+-- prop> \(t :: Time) -> (datetimeToTime (timeToDatetime t)) == t timeToDatetime :: Time -> Datetime timeToDatetime = utcTimeToDatetime . toUtc -- | Convert 'Datetime' to 'Time'.+--+-- prop> \(d :: Datetime) -> timeToDatetime (datetimeToTime d) == d datetimeToTime :: Datetime -> Time datetimeToTime = fromUtc . datetimeToUtcTime @@ -285,11 +381,34 @@ dayToTimeMidnight :: Day -> Time dayToTimeMidnight (Day d) = Time (fromIntegral (d - 40587) * 86400000000000) +-- | Convert 'Day' to a 'Date'.+--+-- prop> \(d :: Day) -> dateToDay (dayToDate d) == d+dayToDate :: Day -> Date+dayToDate theDay = Date year month dayOfMonth+ where+ OrdinalDate year yd = dayToOrdinalDate theDay+ MonthDate month dayOfMonth = dayOfYearToMonthDay (isLeapYear year) yd++-- | Convert a 'Date' to a 'Day'.+--+-- prop> \(d :: Date) -> dayToDate (dateToDay d) == d+dateToDay :: Date -> Day+dateToDay (Date y m d) = ordinalDateToDay $ OrdinalDate y+ (monthDateToDayOfYear (isLeapYear y) (MonthDate m d))+ -- | Construct a 'Datetime' from year, month, day, hour, minute, second: -- -- >>> datetimeFromYmdhms 2014 2 26 17 58 52--- foobar-datetimeFromYmdhms :: Int -> Int -> Int -> Int -> Int -> Int -> Datetime+-- Datetime {datetimeDate = Date {dateYear = Year {getYear = 2014}, dateMonth = Month {getMonth = 1}, dateDay = DayOfMonth {getDayOfMonth = 26}}, datetimeTime = TimeOfDay {timeOfDayHour = 17, timeOfDayMinute = 58, timeOfDayNanoseconds = 52000000000}}+datetimeFromYmdhms ::+ Int -- ^ Year+ -> Int -- ^ Month+ -> Int -- ^ Day+ -> Int -- ^ Hour+ -> Int -- ^ Minute+ -> Int -- ^ Second+ -> Datetime datetimeFromYmdhms y m d h m' s = Datetime (Date (Year $ fromIntegral y)@@ -306,7 +425,18 @@ then fromIntegral (m - 1) else 1 -timeFromYmdhms :: Int -> Int -> Int -> Int -> Int -> Int -> Time+-- | Construct a 'Time' from year, month, day, hour, minute, second:+--+-- >>> timeFromYmdhms 2014 2 26 17 58 52+-- Time {getTime = 1393437532000000000}+timeFromYmdhms ::+ Int -- ^ Year+ -> Int -- ^ Month+ -> Int -- ^ Day+ -> Int -- ^ Hour+ -> Int -- ^ Minute+ -> Int -- ^ Second+ -> Time timeFromYmdhms y m d h m' s = datetimeToTime (datetimeFromYmdhms y m d h m' s) -- | Gets the current 'Day'. This does not take the user\'s@@ -350,7 +480,7 @@ start <- CLK.getTime c a <- action >>= evaluate end <- CLK.getTime c- return (timeSpecToTimespan (CLK.diffTimeSpec end start),a)+ pure (timeSpecToTimespan (CLK.diffTimeSpec end start),a) -- | Variant of 'stopwatch_' that accepts a clock type. stopwatchWith_ :: CLK.Clock -> IO a -> IO Timespan@@ -358,23 +488,24 @@ start <- CLK.getTime c _ <- action end <- CLK.getTime c- return (timeSpecToTimespan (CLK.diffTimeSpec end start))+ pure (timeSpecToTimespan (CLK.diffTimeSpec end start)) timeSpecToTimespan :: CLK.TimeSpec -> Timespan timeSpecToTimespan (CLK.TimeSpec s ns) = Timespan (s * 1000000000 + ns) +-- UtcTime. Used internally only. data UtcTime = UtcTime {-# UNPACK #-} !Day -- day {-# UNPACK #-} !Int64 -- nanoseconds toUtc :: Time -> UtcTime-toUtc (Time i) = let (d,t) = divMod i (getTimespan dayLength)+toUtc (Time i) = let (d,t) = divMod i (getTimespan day) in UtcTime (add (fromIntegral d) epochDay) (fromIntegral t) fromUtc :: UtcTime -> Time fromUtc (UtcTime d ns') = Time $ getTimespan $ plus- (scale (intToInt64 (difference d epochDay)) dayLength)- (if ns > dayLength then dayLength else ns)+ (scale (intToInt64 (difference d epochDay)) day)+ (if ns > day then day else ns) where ns = Timespan ns' intToInt64 :: Int -> Int64@@ -383,15 +514,13 @@ epochDay :: Day epochDay = Day 40587 -dayLength :: Timespan-dayLength = Timespan 86400000000000- dayLengthInt64 :: Int64-dayLengthInt64 = 86400000000000+dayLengthInt64 = getTimespan day nanosecondsInMinute :: Int64 nanosecondsInMinute = 60000000000 +-- | All UTC time offsets. See <https://en.wikipedia.org/wiki/List_of_UTC_time_offsets List of UTC time offsets>. observedOffsets :: Vector Offset observedOffsets = Vector.fromList $ map Offset [ -1200@@ -462,13 +591,6 @@ timeOfDayToNanosecondsSinceMidnight (TimeOfDay h m ns) = fromIntegral h * 3600000000000 + fromIntegral m * 60000000000 + ns --- | Convert 'Day' to a 'Date'.-dayToDate :: Day -> Date-dayToDate theDay = Date year month dayOfMonth- where- OrdinalDate year yd = dayToOrdinalDate theDay- MonthDate month dayOfMonth = dayOfYearToMonthDay (isLeapYear year) yd- -- datetimeToOffsetDatetime :: Offset -> Datetime -> OffsetDatetime -- datetimeToOffsetDatetime offset @@ -496,18 +618,18 @@ (Day (theDay + dayAdjustment)) (timeOfDayToNanosecondsSinceMidnight tod) --- | Convert 'Date' to a 'Day'.-dateToDay :: Date -> Day-dateToDay (Date y m d) = ordinalDateToDay $ OrdinalDate y- (monthDateToDayOfYear (isLeapYear y) (MonthDate m d))--monthDateToDayOfYear :: Bool -> MonthDate -> DayOfYear+-- | Convert a 'MonthDate' to a 'DayOfYear'.+monthDateToDayOfYear ::+ Bool -- ^ Is it a leap year?+ -> MonthDate+ -> DayOfYear monthDateToDayOfYear isLeap (MonthDate month@(Month m) (DayOfMonth dayOfMonth)) = DayOfYear ((div (367 * (fromIntegral m + 1) - 362) 12) + k + day') where day' = fromIntegral $ clip 1 (daysInMonth isLeap month) dayOfMonth k = if month < Month 2 then 0 else if isLeap then -1 else -2 +-- | Convert an 'OrdinalDate' to a 'Day'. ordinalDateToDay :: OrdinalDate -> Day ordinalDateToDay (OrdinalDate year@(Year y') theDay) = Day mjd where y = y' - 1@@ -518,21 +640,33 @@ + (div y 4) - (div y 100) + (div y 400) - 678576 +-- | Is the 'Year' a leap year?+--+-- >>> isLeapYear (Year 1996)+-- True+--+-- >>> isLeapYear (Year 2019)+-- False isLeapYear :: Year -> Bool isLeapYear (Year year) = (mod year 4 == 0) && ((mod year 400 == 0) || not (mod year 100 == 0)) -dayOfYearToMonthDay :: Bool -> DayOfYear -> MonthDate+-- | Convert a 'DayOfYear' to a 'MonthDate'.+dayOfYearToMonthDay ::+ Bool -- ^ Is it a leap year?+ -> DayOfYear+ -> MonthDate dayOfYearToMonthDay isLeap dayOfYear =- let (!upperBound,!monthTable,!dayTable) =+ let (!doyUpperBound,!monthTable,!dayTable) = if isLeap then (DayOfYear 366, leapYearDayOfYearMonthTable, leapYearDayOfYearDayOfMonthTable) else (DayOfYear 365, normalYearDayOfYearMonthTable, normalYearDayOfYearDayOfMonthTable)- DayOfYear clippedDay = clip (DayOfYear 1) upperBound dayOfYear+ DayOfYear clippedDay = clip (DayOfYear 1) doyUpperBound dayOfYear clippedDayInt = fromIntegral clippedDay :: Int month = UVector.unsafeIndex monthTable clippedDayInt theDay = UVector.unsafeIndex dayTable clippedDayInt in MonthDate month theDay +-- | Convert a 'Day' to an 'OrdinalDate'. dayToOrdinalDate :: Day -> OrdinalDate dayToOrdinalDate (Day mjd) = OrdinalDate (Year $ fromIntegral year) (DayOfYear $ fromIntegral yd) where a = (fromIntegral mjd :: Int64) + 678575@@ -554,42 +688,59 @@ -} +-- | The W3C 'DatetimeFormat'.+--+-- >>> encode_YmdHMS SubsecondPrecisionAuto w3c (timeToDatetime (timeFromYmdhms 2014 2 26 17 58 52))+-- "2014-02-26T17:58:52"+--+-- prop> \(s :: SubsecondPrecision) (dt :: Datetime) -> isJust (decode_YmdHMS w3c (encode_YmdHMS s w3c dt)) w3c :: DatetimeFormat w3c = DatetimeFormat (Just '-') (Just 'T') (Just ':') +-- | A 'DatetimeFormat' that separates the members of+-- the 'Date' by slashes.+--+-- >>> encode_YmdHMS SubsecondPrecisionAuto slash (timeToDatetime (timeFromYmdhms 2014 2 26 17 58 52))+-- "2014/02/26 17:58:52"+--+-- prop> \(s :: SubsecondPrecision) (dt :: Datetime) -> isJust (decode_YmdHMS slash (encode_YmdHMS s slash dt)) slash :: DatetimeFormat slash = DatetimeFormat (Just '/') (Just ' ') (Just ':') +-- | A 'DatetimeFormat' that separates the members of+-- the 'Date' by hyphens.+--+-- >>> encode_YmdHMS SubsecondPrecisionAuto hyphen (timeToDatetime (timeFromYmdhms 2014 2 26 17 58 52))+-- "2014-02-26 17:58:52"+--+-- prop> \(s :: SubsecondPrecision) (dt :: Datetime) -> isJust (decode_YmdHMS hyphen (encode_YmdHMS s hyphen dt)) hyphen :: DatetimeFormat hyphen = DatetimeFormat (Just '-') (Just ' ') (Just ':') +-- | A 'DatetimeFormat' with no separators, except for a+-- `T` between the 'Date' and 'Time'.+--+-- >>> encode_YmdHMS SubsecondPrecisionAuto compact (timeToDatetime (timeFromYmdhms 2014 2 26 17 58 52))+-- "20140226T175852"+--+-- prop> \(s :: SubsecondPrecision) (dt :: Datetime) -> isJust (decode_YmdHMS compact (encode_YmdHMS s compact dt)) compact :: DatetimeFormat compact = DatetimeFormat Nothing (Just 'T') Nothing -buildDayOfWeekMatch :: a -> a -> a -> a -> a -> a -> a -> DayOfWeekMatch a-buildDayOfWeekMatch a b c d e f g =- DayOfWeekMatch (Vector.fromList [a,b,c,d,e,f,g])--internalBuildMonthMatch :: a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> MonthMatch a-internalBuildMonthMatch a b c d e f g h i j k l =- MonthMatch (Vector.fromList [a,b,c,d,e,f,g,h,i,j,k,l])--internalMatchMonth :: MonthMatch a -> Month -> a-internalMatchMonth (MonthMatch v) (Month ix) = Vector.unsafeIndex v (fromIntegral ix)-+-- | Return the number of days in a given month. daysInMonth :: Bool -- ^ Is this a leap year? -> Month -- ^ Month of year -> Int daysInMonth isLeap m = if isLeap- then internalMatchMonth leapYearMonthLength m- else internalMatchMonth normalYearMonthLength m+ then caseMonth leapYearMonthLength m+ else caseMonth normalYearMonthLength m leapYearMonthLength :: MonthMatch Int-leapYearMonthLength = internalBuildMonthMatch 31 29 31 30 31 30 31 31 30 31 30 31+leapYearMonthLength = buildMonthMatch 31 29 31 30 31 30 31 31 30 31 30 31 normalYearMonthLength :: MonthMatch Int-normalYearMonthLength = internalBuildMonthMatch 31 30 31 30 31 30 31 31 30 31 30 31+normalYearMonthLength = buildMonthMatch 31 30 31 30 31 30 31 31 30 31 30 31 leapYearDayOfYearDayOfMonthTable :: UVector.Vector DayOfMonth leapYearDayOfYearDayOfMonthTable = UVector.fromList $ (DayOfMonth 1:) $ concat@@ -659,14 +810,34 @@ ] {-# NOINLINE normalYearDayOfYearMonthTable #-} +-- | Build a 'MonthMatch' from twelve (12) values. buildMonthMatch :: a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> MonthMatch a-buildMonthMatch = internalBuildMonthMatch+buildMonthMatch a b c d e f g h i j k l =+ MonthMatch (Vector.fromListN 12 [a,b,c,d,e,f,g,h,i,j,k,l]) +-- | Match a 'Month' against a 'MonthMatch'. caseMonth :: MonthMatch a -> Month -> a-caseMonth = internalMatchMonth+caseMonth (MonthMatch v) (Month ix) = Vector.unsafeIndex v ix --- | This could be written much more efficiently since we know the--- exact size the resulting 'Text' will be.+-- | Build an 'UnboxedMonthMatch' from twelve (12) values.+buildUnboxedMonthMatch :: UVector.Unbox a => a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> a -> UnboxedMonthMatch a+buildUnboxedMonthMatch a b c d e f g h i j k l =+ UnboxedMonthMatch (UVector.fromListN 12 [a,b,c,d,e,f,g,h,i,j,k,l])++-- | Match a 'Month' against an 'UnboxedMonthMatch'.+caseUnboxedMonth :: UVector.Unbox a => UnboxedMonthMatch a -> Month -> a+caseUnboxedMonth (UnboxedMonthMatch v) (Month ix) = UVector.unsafeIndex v ix++-- | Build a 'DayOfWeekMatch' from seven (7) values.+buildDayOfWeekMatch :: a -> a -> a -> a -> a -> a -> a -> DayOfWeekMatch a+buildDayOfWeekMatch a b c d e f g =+ DayOfWeekMatch (Vector.fromListN 7 [a,b,c,d,e,f,g])++-- | Match a 'DayOfWeek' against a 'DayOfWeekMatch'.+caseDayOfWeek :: DayOfWeekMatch a -> DayOfWeek -> a+caseDayOfWeek (DayOfWeekMatch v) (DayOfWeek ix) = Vector.unsafeIndex v ix+-- | Given a 'Date' and a separator, construct a 'Text' 'TB.Builder'+-- corresponding to Year/Month/Day encoding. builder_Ymd :: Maybe Char -> Date -> TB.Builder builder_Ymd msep (Date (Year y) m d) = case msep of Nothing ->@@ -680,6 +851,8 @@ <> sepBuilder <> zeroPadDayOfMonth d +-- | Given a 'Date' and a separator, construct a 'Text' 'TB.Builder'+-- corresponding to a Day/Month/Year encoding. builder_Dmy :: Maybe Char -> Date -> TB.Builder builder_Dmy msep (Date (Year y) m d) = case msep of Nothing ->@@ -693,6 +866,8 @@ <> sepBuilder <> TB.decimal y +-- | Parse a Year/Month/Day-encoded 'Date' that uses the+-- given separator. parser_Ymd :: Maybe Char -> Parser Date parser_Ymd msep = do y <- parseFixedDigits 4@@ -702,8 +877,10 @@ traverse_ AT.char msep d <- parseFixedDigits 2 when (d < 1 || d > 31) (fail "day must be between 1 and 31")- return (Date (Year y) (Month $ m - 1) (DayOfMonth d))+ pure (Date (Year y) (Month $ m - 1) (DayOfMonth d)) +-- | Parse a Month/Day/Year-encoded 'Date' that uses the+-- given separator. parser_Mdy :: Maybe Char -> Parser Date parser_Mdy msep = do m <- parseFixedDigits 2@@ -713,8 +890,10 @@ when (d < 1 || d > 31) (fail "day must be between 1 and 31") traverse_ AT.char msep y <- parseFixedDigits 4- return (Date (Year y) (Month $ m - 1) (DayOfMonth d))+ pure (Date (Year y) (Month $ m - 1) (DayOfMonth d)) +-- | Parse a Day/Month/Year-encoded 'Date' that uses the+-- given separator. parser_Dmy :: Maybe Char -> Parser Date parser_Dmy msep = do d <- parseFixedDigits 2@@ -724,8 +903,10 @@ when (m < 1 || m > 12) (fail "month must be between 1 and 12") traverse_ AT.char msep y <- parseFixedDigits 4- return (Date (Year y) (Month $ m - 1) (DayOfMonth d))+ pure (Date (Year y) (Month $ m - 1) (DayOfMonth d)) +-- | Given a 'Date' and a separator, construct a 'ByteString' 'BB.Builder'+-- corresponding to a Day/Month/Year encoding. builderUtf8_Ymd :: Maybe Char -> Date -> BB.Builder builderUtf8_Ymd msep (Date (Year y) m d) = case msep of Nothing ->@@ -739,6 +920,8 @@ <> sepBuilder <> zeroPadDayOfMonthBS d +-- | Parse a Year/Month/Day-encoded 'Date' that uses the+-- given separator. parserUtf8_Ymd :: Maybe Char -> AB.Parser Date parserUtf8_Ymd msep = do y <- parseFixedDigitsIntBS 4@@ -748,13 +931,21 @@ traverse_ AB.char msep d <- parseFixedDigitsIntBS 2 when (d < 1 || d > 31) (fail "day must be between 1 and 31")- return (Date (Year y) (Month $ m - 1) (DayOfMonth d))+ pure (Date (Year y) (Month $ m - 1) (DayOfMonth d)) +-- | Given a 'SubsecondPrecision' and a separator, construct a+-- 'Text' 'TB.Builder' corresponding to a Hour/Minute/Second+-- encoding. builder_HMS :: SubsecondPrecision -> Maybe Char -> TimeOfDay -> TB.Builder builder_HMS sp msep (TimeOfDay h m ns) = indexTwoDigitTextBuilder h <> internalBuilder_NS sp msep m ns +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a separator,+-- construct a 'Text' 'TB.Builder' according to an IMS encoding.+--+-- This differs from 'builder_IMSp' in that their is a space+-- between the seconds and locale. builder_IMS_p :: MeridiemLocale Text -> SubsecondPrecision -> Maybe Char -> TimeOfDay -> TB.Builder builder_IMS_p meridiemLocale sp msep (TimeOfDay h m ns) = internalBuilder_I h@@ -762,6 +953,14 @@ <> " " <> internalBuilder_p meridiemLocale h +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a separator,+-- construct a 'Text' 'TB.Builder' according to an IMS encoding.+builder_IMSp :: MeridiemLocale Text -> SubsecondPrecision -> Maybe Char -> TimeOfDay -> TB.Builder+builder_IMSp meridiemLocale sp msep (TimeOfDay h m ns) =+ internalBuilder_I h+ <> internalBuilder_NS sp msep m ns+ <> internalBuilder_p meridiemLocale h+ internalBuilder_I :: Int -> TB.Builder internalBuilder_I h = indexTwoDigitTextBuilder $ if h > 12@@ -775,12 +974,8 @@ then TB.fromText pm else TB.fromText am -builder_IMSp :: MeridiemLocale Text -> SubsecondPrecision -> Maybe Char -> TimeOfDay -> TB.Builder-builder_IMSp meridiemLocale sp msep (TimeOfDay h m ns) =- internalBuilder_I h- <> internalBuilder_NS sp msep m ns- <> internalBuilder_p meridiemLocale h-+-- | Parse an Hour/Minute/Second-encoded 'TimeOfDay' that uses+-- the given separator. parser_HMS :: Maybe Char -> Parser TimeOfDay parser_HMS msep = do h <- parseFixedDigits 2@@ -790,7 +985,7 @@ when (m > 59) (fail "minute must be between 0 and 59") traverse_ AT.char msep ns <- parseSecondsAndNanoseconds- return (TimeOfDay h m ns)+ pure (TimeOfDay h m ns) -- | Parses text that is formatted as either of the following: --@@ -809,14 +1004,14 @@ when (m > 59) (fail "minute must be between 0 and 59") mc <- AT.peekChar case mc of- Nothing -> return (TimeOfDay h m 0)+ Nothing -> pure (TimeOfDay h m 0) Just c -> case msep of Just sep -> if c == sep then do _ <- AT.anyChar -- should be the separator ns <- parseSecondsAndNanoseconds- return (TimeOfDay h m ns)- else return (TimeOfDay h m 0)+ pure (TimeOfDay h m ns)+ else pure (TimeOfDay h m 0) -- if there is no separator, we will try to parse the -- remaining part as seconds. We commit to trying to -- parse as seconds if we see any number as the next@@ -824,8 +1019,8 @@ Nothing -> if isDigit c then do ns <- parseSecondsAndNanoseconds- return (TimeOfDay h m ns)- else return (TimeOfDay h m 0)+ pure (TimeOfDay h m ns)+ else pure (TimeOfDay h m 0) parseSecondsAndNanoseconds :: Parser Int64 parseSecondsAndNanoseconds = do@@ -842,19 +1037,19 @@ else if totalDigits < 9 then x * raiseTenTo (9 - totalDigits) else quot x (raiseTenTo (totalDigits - 9))- return (fromIntegral result)- ) <|> return 0- return (s * 1000000000 + nanoseconds)+ pure (fromIntegral result)+ ) <|> pure 0+ pure (s * 1000000000 + nanoseconds) countZeroes :: AT.Parser Int countZeroes = go 0 where go !i = do m <- AT.peekChar case m of- Nothing -> return i+ Nothing -> pure i Just c -> if c == '0' then AT.anyChar *> go (i + 1)- else return i+ else pure i nanosecondsBuilder :: Int64 -> TB.Builder nanosecondsBuilder w@@ -903,11 +1098,13 @@ (milli,milliRem) = quotRem nano 1000000 (micro,microRem) = quotRem nano 1000 -+-- | Encode a 'Timespan' as 'Text' using the given 'SubsecondPrecision'. encodeTimespan :: SubsecondPrecision -> Timespan -> Text encodeTimespan sp = LT.toStrict . TB.toLazyText . builderTimespan sp +-- | Construct a 'Text' 'TB.Builder' corresponding to an encoding+-- of the given 'Timespan' using the given 'SubsecondPrecision'. builderTimespan :: SubsecondPrecision -> Timespan -> TB.Builder builderTimespan sp (Timespan ns) = TB.decimal sInt64 <> prettyNanosecondsBuilder sp nsRemainder@@ -929,9 +1126,9 @@ (!sInt64,!nsRemainder) = quotRem ns 1000000000 !s = fromIntegral sInt64 ---+-- | Given a 'SubsecondPrecision' and a 'DatetimeFormat', construct a+-- 'Text' 'TB.Builder' corresponding to a+-- Day/Month/Year,Hour/Minute/Second encoding of the given 'Datetime'. builder_DmyHMS :: SubsecondPrecision -> DatetimeFormat -> Datetime -> TB.Builder builder_DmyHMS sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = case msep of@@ -941,35 +1138,58 @@ <> TB.singleton sep <> builder_HMS sp mtimeSep time +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision',+-- and a 'DatetimeFormat', construct a 'Text' 'TB.Builder'+-- corresponding to a Day/Month/Year,IMS encoding of the given+-- 'Datetime'. This differs from 'builder_DmyIMSp' in that+-- it adds a space between the locale and seconds. builder_DmyIMS_p :: MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> Datetime -> TB.Builder builder_DmyIMS_p locale sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = builder_Dmy mdateSep date <> maybe mempty TB.singleton msep <> builder_IMS_p locale sp mtimeSep time +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision',+-- and a 'DatetimeFormat', construct a 'Text' 'TB.Builder'+-- corresponding to a Day/Month/Year,IMS encoding of the given+-- 'Datetime'. builder_DmyIMSp :: MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> Datetime -> TB.Builder builder_DmyIMSp locale sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = builder_Dmy mdateSep date <> maybe mempty TB.singleton msep <> builder_IMS_p locale sp mtimeSep time -+-- | Given a 'SubsecondPrecision' and 'DatetimeFormat', construct+-- 'Text' that corresponds to a Day/Month/Year,Hour/Minute/Second+-- encoding of the given 'Datetime'. encode_DmyHMS :: SubsecondPrecision -> DatetimeFormat -> Datetime -> Text encode_DmyHMS sp format = LT.toStrict . TB.toLazyText . builder_DmyHMS sp format +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct 'Text' that corresponds to a+-- Day/Month/Year,IMS encoding of the given 'Datetime'. This+-- inserts a space between the locale and seconds. encode_DmyIMS_p :: MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> Datetime -> Text encode_DmyIMS_p a sp b = LT.toStrict . TB.toLazyText . builder_DmyIMS_p a sp b +-- | Given a 'SubsecondPrecision' and 'DatetimeFormat', construct+-- 'Text' that corresponds to a Year/Month/Day,Hour/Minute/Second+-- encoding of the given 'Datetime'. encode_YmdHMS :: SubsecondPrecision -> DatetimeFormat -> Datetime -> Text encode_YmdHMS sp format = LT.toStrict . TB.toLazyText . builder_YmdHMS sp format +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct 'Text' that corresponds to a+-- Year/Month/Day,IMS encoding of the given 'Datetime'. This+-- inserts a space between the locale and seconds. encode_YmdIMS_p :: MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> Datetime -> Text encode_YmdIMS_p a sp b = LT.toStrict . TB.toLazyText . builder_YmdIMS_p a sp b --- | This could be written much more efficiently since we know the--- exact size the resulting 'Text' will be.+-- | Given a 'SubsecondPrecision' and a 'DatetimeFormat', construct+-- a 'Text' 'TB.Builder' corresponding to a+-- Year/Month/Day,Hour/Minute/Second encoding of the given 'Datetime'. builder_YmdHMS :: SubsecondPrecision -> DatetimeFormat -> Datetime -> TB.Builder builder_YmdHMS sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = case msep of@@ -979,76 +1199,126 @@ <> TB.singleton sep <> builder_HMS sp mtimeSep time +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct a 'Text' 'TB.Builder' that+-- corresponds to a Year/Month/Day,IMS encoding of the+-- given 'Datetime'. This inserts a space between the locale+-- and seconds. builder_YmdIMS_p :: MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> Datetime -> TB.Builder builder_YmdIMS_p locale sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = builder_Ymd mdateSep date <> maybe mempty TB.singleton msep <> builder_IMS_p locale sp mtimeSep time +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct a 'Text' 'TB.Builder' that+-- corresponds to a Year/Month/Day,IMS encoding of the+-- given 'Datetime'. builder_YmdIMSp :: MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> Datetime -> TB.Builder builder_YmdIMSp locale sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = builder_Ymd mdateSep date <> maybe mempty TB.singleton msep <> builder_IMS_p locale sp mtimeSep time -+-- | Construct a 'Text' 'TB.Builder' corresponding to the W3C+-- encoding of the given 'Datetime'. builderW3C :: Datetime -> TB.Builder-builderW3C = builder_YmdHMS SubsecondPrecisionAuto (DatetimeFormat (Just '-') (Just 'T') (Just ':'))+builderW3C = builder_YmdHMS SubsecondPrecisionAuto w3c +-- | Decode a Year/Month/Day,Hour/Minute/Second-encoded 'Datetime'+-- from 'Text' that was encoded with the given 'DatetimeFormat'. decode_YmdHMS :: DatetimeFormat -> Text -> Maybe Datetime decode_YmdHMS format = either (const Nothing) Just . AT.parseOnly (parser_YmdHMS format) +-- | Parse a Day/Month/Year,Hour/Minute/Second-encoded 'Datetime'+-- that was encoded with the given 'DatetimeFormat'. parser_DmyHMS :: DatetimeFormat -> Parser Datetime parser_DmyHMS (DatetimeFormat mdateSep msep mtimeSep) = do date <- parser_Dmy mdateSep traverse_ AT.char msep time <- parser_HMS mtimeSep- return (Datetime date time)+ pure (Datetime date time) +-- | Parses text that is formatted as either of the following:+--+-- * @%H:%M@+-- * @%H:%M:%S@+--+-- That is, the seconds and subseconds part is optional. If it is+-- not provided, it is assumed to be zero. This format shows up+-- in Google Chrome\'s @datetime-local@ inputs. parser_DmyHMS_opt_S :: DatetimeFormat -> Parser Datetime parser_DmyHMS_opt_S (DatetimeFormat mdateSep msep mtimeSep) = do date <- parser_Dmy mdateSep traverse_ AT.char msep time <- parser_HMS_opt_S mtimeSep- return (Datetime date time)+ pure (Datetime date time) +-- | Decode a Day/Month/Year,Hour/Minute/Second-encoded 'Datetime'+-- from 'Text' that was encoded with the given 'DatetimeFormat'. decode_DmyHMS :: DatetimeFormat -> Text -> Maybe Datetime decode_DmyHMS format = either (const Nothing) Just . AT.parseOnly (parser_DmyHMS format) +-- | Parses text that is formatted as either of the following:+--+-- * @%H:%M@+-- * @%H:%M:%S@+--+-- That is, the seconds and subseconds part is optional. If it is+-- not provided, it is assumed to be zero. This format shows up+-- in Google Chrome\'s @datetime-local@ inputs. decode_DmyHMS_opt_S :: DatetimeFormat -> Text -> Maybe Datetime decode_DmyHMS_opt_S format = either (const Nothing) Just . AT.parseOnly (parser_DmyHMS_opt_S format)-+-- | Parses a Year/Month/Day,Hour/Minute/Second-encoded 'Datetime'+-- that was encoded using the given 'DatetimeFormat'. parser_YmdHMS :: DatetimeFormat -> Parser Datetime parser_YmdHMS (DatetimeFormat mdateSep msep mtimeSep) = do date <- parser_Ymd mdateSep traverse_ AT.char msep time <- parser_HMS mtimeSep- return (Datetime date time)+ pure (Datetime date time) +-- | Parses text that is formatted as either of the following:+--+-- * @%H:%M@+-- * @%H:%M:%S@+--+-- That is, the seconds and subseconds part is optional. If it is+-- not provided, it is assumed to be zero. This format shows up+-- in Google Chrome\'s @datetime-local@ inputs. parser_YmdHMS_opt_S :: DatetimeFormat -> Parser Datetime parser_YmdHMS_opt_S (DatetimeFormat mdateSep msep mtimeSep) = do date <- parser_Ymd mdateSep traverse_ AT.char msep time <- parser_HMS_opt_S mtimeSep- return (Datetime date time)+ pure (Datetime date time) +-- | Parses text that is formatted as either of the following:+--+-- * @%H:%M@+-- * @%H:%M:%S@+--+-- That is, the seconds and subseconds part is optional. If it is+-- not provided, it is assumed to be zero. This format shows up+-- in Google Chrome\'s @datetime-local@ inputs. decode_YmdHMS_opt_S :: DatetimeFormat -> Text -> Maybe Datetime decode_YmdHMS_opt_S format = either (const Nothing) Just . AT.parseOnly (parser_YmdHMS_opt_S format)-- --------------- -- ByteString stuff --------------- +-- | Given a 'SubsecondPrecision' and a separator, construct a 'ByteString' 'BB.Builder' corresponding to an Hour/Month/Second encoding of the given 'TimeOfDay'. builderUtf8_HMS :: SubsecondPrecision -> Maybe Char -> TimeOfDay -> BB.Builder builderUtf8_HMS sp msep (TimeOfDay h m ns) = indexTwoDigitByteStringBuilder h <> internalBuilderUtf8_NS sp msep m ns +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a separator, construct a 'ByteString' 'BB.Builder' corresponding to an IMS encoding of the given 'TimeOfDay'. This differs from 'builderUtf8_IMSp' in that+-- there is a space between the seconds and locale. builderUtf8_IMS_p :: MeridiemLocale ByteString -> SubsecondPrecision -> Maybe Char -> TimeOfDay -> BB.Builder builderUtf8_IMS_p meridiemLocale sp msep (TimeOfDay h m ns) = internalBuilderUtf8_I h@@ -1069,12 +1339,15 @@ then BB.byteString pm else BB.byteString am +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a separator, construct a 'ByteString' 'BB.Builder' corresponding to an IMS encoding of the given 'TimeOfDay'. builderUtf8_IMSp :: MeridiemLocale ByteString -> SubsecondPrecision -> Maybe Char -> TimeOfDay -> BB.Builder builderUtf8_IMSp meridiemLocale sp msep (TimeOfDay h m ns) = internalBuilderUtf8_I h <> internalBuilderUtf8_NS sp msep m ns <> internalBuilderUtf8_p meridiemLocale h +-- | Parse an Hour/Minute/Second-encoded 'TimeOfDay' that uses+-- the given separator. parserUtf8_HMS :: Maybe Char -> AB.Parser TimeOfDay parserUtf8_HMS msep = do h <- parseFixedDigitsIntBS 2@@ -1084,7 +1357,7 @@ when (m > 59) (fail "minute must be between 0 and 59") traverse_ AB.char msep ns <- parseSecondsAndNanosecondsUtf8- return (TimeOfDay h m ns)+ pure (TimeOfDay h m ns) -- | Parses text that is formatted as either of the following: --@@ -1103,14 +1376,14 @@ when (m > 59) (fail "minute must be between 0 and 59") mc <- AB.peekChar case mc of- Nothing -> return (TimeOfDay h m 0)+ Nothing -> pure (TimeOfDay h m 0) Just c -> case msep of Just sep -> if c == sep then do _ <- AB.anyChar -- should be the separator ns <- parseSecondsAndNanosecondsUtf8- return (TimeOfDay h m ns)- else return (TimeOfDay h m 0)+ pure (TimeOfDay h m ns)+ else pure (TimeOfDay h m 0) -- if there is no separator, we will try to parse the -- remaining part as seconds. We commit to trying to -- parse as seconds if we see any number as the next@@ -1118,8 +1391,8 @@ Nothing -> if isDigit c then do ns <- parseSecondsAndNanosecondsUtf8- return (TimeOfDay h m ns)- else return (TimeOfDay h m 0)+ pure (TimeOfDay h m ns)+ else pure (TimeOfDay h m 0) parseSecondsAndNanosecondsUtf8 :: AB.Parser Int64 parseSecondsAndNanosecondsUtf8 = do@@ -1136,19 +1409,19 @@ else if totalDigits < 9 then x * raiseTenTo (9 - totalDigits) else quot x (raiseTenTo (totalDigits - 9))- return (fromIntegral result)- ) <|> return 0- return (s * 1000000000 + nanoseconds)+ pure (fromIntegral result)+ ) <|> pure 0+ pure (s * 1000000000 + nanoseconds) countZeroesUtf8 :: AB.Parser Int countZeroesUtf8 = go 0 where go !i = do m <- AB.peekChar case m of- Nothing -> return i+ Nothing -> pure i Just c -> if c == '0' then AB.anyChar *> go (i + 1)- else return i+ else pure i nanosecondsBuilderUtf8 :: Int64 -> BB.Builder nanosecondsBuilderUtf8 w@@ -1197,10 +1470,14 @@ (milli,milliRem) = quotRem nano 1000000 (micro,microRem) = quotRem nano 1000 +-- | Given a 'SubsecondPrecision', construct a 'ByteString' corresponding+-- to an encoding of the given 'Timespan'. encodeTimespanUtf8 :: SubsecondPrecision -> Timespan -> ByteString encodeTimespanUtf8 sp = LB.toStrict . BB.toLazyByteString . builderTimespanUtf8 sp +-- | Given a 'SubsecondPrecision', construct a 'ByteString' 'BB.Builder'+-- corresponding to an encoding of the given 'Timespan'. builderTimespanUtf8 :: SubsecondPrecision -> Timespan -> BB.Builder builderTimespanUtf8 sp (Timespan ns) = int64Builder sInt64 <> prettyNanosecondsBuilderUtf8 sp nsRemainder@@ -1225,13 +1502,24 @@ (!sInt64,!nsRemainder) = quotRem ns 1000000000 !s = fromIntegral sInt64 +-- | Given a 'SubsecondPrecision' and a 'DatetimeFormat', construct+-- a 'ByteString' corresponding to a Year/Month/Day,Hour/Minute/Second+-- encoding of the given 'Datetime'. encodeUtf8_YmdHMS :: SubsecondPrecision -> DatetimeFormat -> Datetime -> ByteString encodeUtf8_YmdHMS sp format = LB.toStrict . BB.toLazyByteString . builderUtf8_YmdHMS sp format +-- | Given a 'MeridiemLocale', a 'SubsecondPrecision', and a 'DatetimeFormat',+-- construct a 'ByteString' corresponding to a Year/Month/Day,IMS encoding+-- of the given 'Datetime'. This inserts a space between the locale and+-- seconds. encodeUtf8_YmdIMS_p :: MeridiemLocale ByteString -> SubsecondPrecision -> DatetimeFormat -> Datetime -> ByteString encodeUtf8_YmdIMS_p a sp b = LB.toStrict . BB.toLazyByteString . builderUtf8_YmdIMS_p a sp b +-- | Given a 'SubsecondPrecision' and a 'DatetimeFormat', construct+-- a 'ByteString' 'BB.Builder' corresponding to a+-- Year/Month/Day,Hour/Minute/Second encoding of the+-- given 'Datetime'. builderUtf8_YmdHMS :: SubsecondPrecision -> DatetimeFormat -> Datetime -> BB.Builder builderUtf8_YmdHMS sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = case msep of@@ -1241,94 +1529,156 @@ <> BB.char7 sep <> builderUtf8_HMS sp mtimeSep time +-- | Given a 'SubsecondPrecision' and a 'DatetimeFormat', construct+-- a 'ByteString' 'BB.Builder' corresponding to a+-- Year/Month/Day,IMS encoding of the given 'Datetime'. This inserts+-- a space between the locale and seconds. builderUtf8_YmdIMS_p :: MeridiemLocale ByteString -> SubsecondPrecision -> DatetimeFormat -> Datetime -> BB.Builder builderUtf8_YmdIMS_p locale sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = builderUtf8_Ymd mdateSep date <> maybe mempty BB.char7 msep <> builderUtf8_IMS_p locale sp mtimeSep time +-- | Given a 'SubsecondPrecision' and a 'DatetimeFormat', construct+-- a 'ByteString' 'BB.Builder' corresponding to a+-- Year/Month/Day,IMS encoding of the given 'Datetime'. builderUtf8_YmdIMSp :: MeridiemLocale ByteString -> SubsecondPrecision -> DatetimeFormat -> Datetime -> BB.Builder builderUtf8_YmdIMSp locale sp (DatetimeFormat mdateSep msep mtimeSep) (Datetime date time) = builderUtf8_Ymd mdateSep date <> maybe mempty BB.char7 msep <> builderUtf8_IMS_p locale sp mtimeSep time -+-- | Construct a 'ByteString' 'BB.Builder' corresponding to+-- a W3C encoding of the given 'Datetime'. builderUtf8W3C :: Datetime -> BB.Builder builderUtf8W3C = builderUtf8_YmdHMS SubsecondPrecisionAuto (DatetimeFormat (Just '-') (Just 'T') (Just ':')) +-- | Decode a Year/Month/Day,Hour/Minute/Second-encoded 'Datetime' from+-- a 'ByteString'. decodeUtf8_YmdHMS :: DatetimeFormat -> ByteString -> Maybe Datetime decodeUtf8_YmdHMS format = either (const Nothing) Just . AB.parseOnly (parserUtf8_YmdHMS format) +-- | Parse a Year/Month/Day,Hour/Minute/Second-encoded 'Datetime' that was+-- encoded using the given 'DatetimeFormat'. parserUtf8_YmdHMS :: DatetimeFormat -> AB.Parser Datetime parserUtf8_YmdHMS (DatetimeFormat mdateSep msep mtimeSep) = do date <- parserUtf8_Ymd mdateSep traverse_ AB.char msep time <- parserUtf8_HMS mtimeSep- return (Datetime date time)+ pure (Datetime date time) +-- | Parses text that is formatted as either of the following:+--+-- * @%H:%M@+-- * @%H:%M:%S@+--+-- That is, the seconds and subseconds part is optional. If it is+-- not provided, it is assumed to be zero. This format shows up+-- in Google Chrome\'s @datetime-local@ inputs. parserUtf8_YmdHMS_opt_S :: DatetimeFormat -> AB.Parser Datetime parserUtf8_YmdHMS_opt_S (DatetimeFormat mdateSep msep mtimeSep) = do date <- parserUtf8_Ymd mdateSep traverse_ AB.char msep time <- parserUtf8_HMS_opt_S mtimeSep- return (Datetime date time)+ pure (Datetime date time) +-- | Parses text that is formatted as either of the following:+--+-- * @%H:%M@+-- * @%H:%M:%S@+--+-- That is, the seconds and subseconds part is optional. If it is+-- not provided, it is assumed to be zero. This format shows up+-- in Google Chrome\'s @datetime-local@ inputs. decodeUtf8_YmdHMS_opt_S :: DatetimeFormat -> ByteString -> Maybe Datetime decodeUtf8_YmdHMS_opt_S format = either (const Nothing) Just . AB.parseOnly (parserUtf8_YmdHMS_opt_S format) -+-- | Given an 'OffsetFormat', a 'SubsecondPrecision', and+-- a 'DatetimeFormat', construct a 'Text' 'TB.Builder'+-- corresponding to a Year/Month/Day,Hour/Minute/Second encoding+-- of the given 'OffsetDatetime'. builder_YmdHMSz :: OffsetFormat -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> TB.Builder builder_YmdHMSz offsetFormat sp datetimeFormat (OffsetDatetime datetime offset) = builder_YmdHMS sp datetimeFormat datetime <> builderOffset offsetFormat offset +-- | Parse a Year/Month/Day,Hour/Minute/Second-encoded 'OffsetDatetime'+-- that was encoded using the given 'OffsetFormat'+-- and 'DatetimeFormat'. parser_YmdHMSz :: OffsetFormat -> DatetimeFormat -> Parser OffsetDatetime parser_YmdHMSz offsetFormat datetimeFormat = OffsetDatetime <$> parser_YmdHMS datetimeFormat <*> parserOffset offsetFormat +-- | Given an 'OffsetFormat', a 'MeridiemLocale', a+-- 'SubsecondPrecision', and 'DatetimeFormat', construct a+-- 'Text' 'TB.Builder' corresponding to a Year/Month/Day,IMS-encoding+-- of the given 'OffsetDatetime'. builder_YmdIMS_p_z :: OffsetFormat -> MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> TB.Builder builder_YmdIMS_p_z offsetFormat meridiemLocale sp datetimeFormat (OffsetDatetime datetime offset) = builder_YmdIMS_p meridiemLocale sp datetimeFormat datetime <> " " <> builderOffset offsetFormat offset +-- | Given an 'OffsetFormat', a 'SubsecondPrecision',+-- and a 'DatetimeFormat', construct 'Text' corresponding to+-- the Year/Month/Day,Hour/Minute/Second-encoding of+-- the given 'OffsetDatetime'. encode_YmdHMSz :: OffsetFormat -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> Text encode_YmdHMSz offsetFormat sp datetimeFormat = LT.toStrict . TB.toLazyText . builder_YmdHMSz offsetFormat sp datetimeFormat +-- | Given an 'OffsetFormat', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct a 'Text' 'TB.Builder' corresponding+-- to the Day/Month/Year,Hour/Minute/Second-encoding of+-- the given 'OffsetDatetime'. builder_DmyHMSz :: OffsetFormat -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> TB.Builder builder_DmyHMSz offsetFormat sp datetimeFormat (OffsetDatetime datetime offset) = builder_DmyHMS sp datetimeFormat datetime <> builderOffset offsetFormat offset +-- | Parse a Day/Month/Year,Hour/Minute/Second-encoded 'OffsetDatetime'+-- that was encoded using the given 'OffsetFormat'+-- and 'DatetimeFormat'. parser_DmyHMSz :: OffsetFormat -> DatetimeFormat -> AT.Parser OffsetDatetime parser_DmyHMSz offsetFormat datetimeFormat = OffsetDatetime <$> parser_DmyHMS datetimeFormat <*> parserOffset offsetFormat +-- | Given an 'OffsetFormat', a 'MeridiemLocale', a+-- 'SubsecondPrecision', and a 'DatetimeFormat', construct a 'Text'+-- 'TB.Builder' corresponding to the Day/Month/Year,IMS encoding+-- of the given 'OffsetDatetime'. builder_DmyIMS_p_z :: OffsetFormat -> MeridiemLocale Text -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> TB.Builder builder_DmyIMS_p_z offsetFormat meridiemLocale sp datetimeFormat (OffsetDatetime datetime offset) = builder_DmyIMS_p meridiemLocale sp datetimeFormat datetime <> " " <> builderOffset offsetFormat offset +-- | Given an 'OffsetFormat', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct 'Text' corresponding to the+-- Day/Month/Year,Hour/Minute/Second encoding of the given+-- 'OffsetDatetime'. encode_DmyHMSz :: OffsetFormat -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> Text encode_DmyHMSz offsetFormat sp datetimeFormat = LT.toStrict . TB.toLazyText . builder_DmyHMSz offsetFormat sp datetimeFormat +-- | Construct a 'Text' 'TB.Builder' corresponding to the w3c-formatting+-- of the given 'OffsetDatetime'. builderW3Cz :: OffsetDatetime -> TB.Builder builderW3Cz = builder_YmdHMSz OffsetFormatColonOn SubsecondPrecisionAuto (DatetimeFormat (Just '-') (Just 'T') (Just ':')) +-- | Encode an 'Offset' to 'Text' using the given 'OffsetFormat'. encodeOffset :: OffsetFormat -> Offset -> Text encodeOffset fmt = LT.toStrict . TB.toLazyText . builderOffset fmt +-- | Construct a 'TB.Builder' corresponding to the given 'Offset'+-- encoded using the given 'OffsetFormat'. builderOffset :: OffsetFormat -> Offset -> TB.Builder builderOffset x = case x of OffsetFormatColonOff -> builderOffset_z@@ -1336,10 +1686,13 @@ OffsetFormatSecondsPrecision -> builderOffset_z2 OffsetFormatColonAuto -> builderOffset_z3 +-- | Decode an 'Offset' from 'Text' that was encoded+-- using the given 'OffsetFormat'. decodeOffset :: OffsetFormat -> Text -> Maybe Offset decodeOffset fmt = either (const Nothing) Just . AT.parseOnly (parserOffset fmt <* AT.endOfInput) +-- | Parse an 'Offset' that was encoded using the given 'OffsetFormat'. parserOffset :: OffsetFormat -> Parser Offset parserOffset x = case x of OffsetFormatColonOff -> parserOffset_z@@ -1352,9 +1705,9 @@ parseSignedness = do c <- AT.anyChar if c == '-'- then return False+ then pure False else if c == '+'- then return True+ then pure True else fail "while parsing offset, expected [+] or [-]" parserOffset_z :: Parser Offset@@ -1363,7 +1716,7 @@ h <- parseFixedDigits 2 m <- parseFixedDigits 2 let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res @@ -1374,7 +1727,7 @@ _ <- AT.char ':' m <- parseFixedDigits 2 let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res @@ -1386,7 +1739,7 @@ m <- parseFixedDigits 2 _ <- AT.string ":00" let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res @@ -1403,10 +1756,10 @@ _ <- AT.anyChar -- should be a colon m <- parseFixedDigits 2 let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res- _ -> return . Offset $ if pos+ _ -> pure . Offset $ if pos then h * 60 else h * (-60) @@ -1449,35 +1802,52 @@ <> ":" <> indexTwoDigitTextBuilder b +-- | Given an 'OffsetFormat', a 'SubsecondPrecision', and a+-- 'DatetimeFormat', construct a 'ByteString' 'BB.Builder'+-- corresponding to the Year/Month/Day,Hour/Minute/Second+-- encoding of the given 'OffsetDatetime'. builderUtf8_YmdHMSz :: OffsetFormat -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> BB.Builder builderUtf8_YmdHMSz offsetFormat sp datetimeFormat (OffsetDatetime datetime offset) = builderUtf8_YmdHMS sp datetimeFormat datetime <> builderOffsetUtf8 offsetFormat offset +-- | Parse a Year/Month/Day,Hour/Minute/Second-encoded 'OffsetDatetime'+-- that was encoded using the given 'OffsetFormat' and+-- 'DatetimeFormat'. parserUtf8_YmdHMSz :: OffsetFormat -> DatetimeFormat -> AB.Parser OffsetDatetime parserUtf8_YmdHMSz offsetFormat datetimeFormat = OffsetDatetime <$> parserUtf8_YmdHMS datetimeFormat <*> parserOffsetUtf8 offsetFormat +-- | Given an 'OffsetFormat', a 'MeridiemLocale, a 'SubsecondPrecision',+-- and a 'DatetimeFormat', construct a 'ByteString' 'BB.Builder'+-- corresponding to a Year/Month/Day,IMS-encoded 'OffsetDatetime'. builderUtf8_YmdIMS_p_z :: OffsetFormat -> MeridiemLocale ByteString -> SubsecondPrecision -> DatetimeFormat -> OffsetDatetime -> BB.Builder builderUtf8_YmdIMS_p_z offsetFormat meridiemLocale sp datetimeFormat (OffsetDatetime datetime offset) = builderUtf8_YmdIMS_p meridiemLocale sp datetimeFormat datetime <> " " <> builderOffsetUtf8 offsetFormat offset +-- | Construct a 'ByteString' 'BB.Builder' corresponding to the W3C+-- encoding of the given 'Datetime'. builderUtf8W3Cz :: OffsetDatetime -> BB.Builder builderUtf8W3Cz = builderUtf8_YmdHMSz OffsetFormatColonOn SubsecondPrecisionAuto (DatetimeFormat (Just '-') (Just 'T') (Just ':')) +-- | Encode an 'Offset' as a 'ByteString' using the given 'OffsetFormat'. encodeOffsetUtf8 :: OffsetFormat -> Offset -> ByteString encodeOffsetUtf8 fmt = LB.toStrict . BB.toLazyByteString . builderOffsetUtf8 fmt +-- | Decode an 'Offset' from a 'ByteString' that was encoded using the given+-- 'OffsetFormat'. decodeOffsetUtf8 :: OffsetFormat -> ByteString -> Maybe Offset decodeOffsetUtf8 fmt = either (const Nothing) Just . AB.parseOnly (parserOffsetUtf8 fmt) +-- | Construct a 'ByteString' 'BB.Builder' corresponding to the+-- encoding of an 'Offset' using the given 'OffsetFormat'. builderOffsetUtf8 :: OffsetFormat -> Offset -> BB.Builder builderOffsetUtf8 x = case x of OffsetFormatColonOff -> builderOffsetUtf8_z@@ -1485,6 +1855,8 @@ OffsetFormatSecondsPrecision -> builderOffsetUtf8_z2 OffsetFormatColonAuto -> builderOffsetUtf8_z3 +-- | Parse an 'Offset' that was encoded using the given+-- 'OffsetFormat'. parserOffsetUtf8 :: OffsetFormat -> AB.Parser Offset parserOffsetUtf8 x = case x of OffsetFormatColonOff -> parserOffsetUtf8_z@@ -1497,9 +1869,9 @@ parseSignednessUtf8 = do c <- AB.anyChar if c == '-'- then return False+ then pure False else if c == '+'- then return True+ then pure True else fail "while parsing offset, expected [+] or [-]" parserOffsetUtf8_z :: AB.Parser Offset@@ -1508,7 +1880,7 @@ h <- parseFixedDigitsIntBS 2 m <- parseFixedDigitsIntBS 2 let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res @@ -1519,7 +1891,7 @@ _ <- AB.char ':' m <- parseFixedDigitsIntBS 2 let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res @@ -1531,7 +1903,7 @@ m <- parseFixedDigitsIntBS 2 _ <- AB.string ":00" let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res @@ -1548,10 +1920,10 @@ _ <- AB.anyChar -- should be a colon m <- parseFixedDigitsIntBS 2 let !res = h * 60 + m- return . Offset $ if pos+ pure . Offset $ if pos then res else negate res- _ -> return . Offset $ if pos+ _ -> pure . Offset $ if pos then h * 60 else h * (-60) @@ -1596,20 +1968,24 @@ -- Zepto parsers +-- | Parse a 'Datetime' that was encoded using the+-- given 'DatetimeFormat'. zeptoUtf8_YmdHMS :: DatetimeFormat -> Z.Parser Datetime zeptoUtf8_YmdHMS (DatetimeFormat mdateSep msep' mtimeSep) = do date <- zeptoUtf8_Ymd mdateSep let msep = BC.singleton <$> msep' traverse_ Z.string msep time <- zeptoUtf8_HMS mtimeSep- return (Datetime date time)+ pure (Datetime date time) zeptoCountZeroes :: Z.Parser Int zeptoCountZeroes = do bs <- Z.takeWhile (0x30 ==) pure $! BC.length bs -zeptoUtf8_Ymd :: Maybe Char -> Z.Parser Chronos.Date+-- | Parse a 'Date' that was encoded using+-- the given separator.+zeptoUtf8_Ymd :: Maybe Char -> Z.Parser Date zeptoUtf8_Ymd msep' = do y <- zeptoFixedDigitsIntBS 4 let msep = BC.singleton <$> msep'@@ -1619,9 +1995,11 @@ traverse_ Z.string msep d <- zeptoFixedDigitsIntBS 2 when (d < 1 || d > 31) (fail "day must be between 1 and 31")- return (Date (Year y) (Month $ m - 1) (DayOfMonth d))+ pure (Date (Year y) (Month $ m - 1) (DayOfMonth d)) -zeptoUtf8_HMS :: Maybe Char -> Z.Parser Chronos.TimeOfDay+-- | Parse a 'TimeOfDay' that was encoded using+-- the given separator.+zeptoUtf8_HMS :: Maybe Char -> Z.Parser TimeOfDay zeptoUtf8_HMS msep' = do h <- zeptoFixedDigitsIntBS 2 when (h > 23) (fail "hour must be between 0 and 23")@@ -1631,7 +2009,7 @@ when (m > 59) (fail "minute must be between 0 and 59") traverse_ Z.string msep ns <- zeptoSecondsAndNanosecondsUtf8- return (TimeOfDay h m ns)+ pure (TimeOfDay h m ns) zeptoFixedDigitsIntBS :: Int -> Z.Parser Int zeptoFixedDigitsIntBS n = do@@ -1639,7 +2017,7 @@ case BC.readInt t of Nothing -> fail "datetime decoding could not parse integral bytestring (a)" Just (i,r) -> if BC.null r- then return i+ then pure i else fail "datetime decoding could not parse integral bytestring (b)" zeptoSecondsAndNanosecondsUtf8 :: Z.Parser Int64@@ -1657,9 +2035,9 @@ else if totalDigits < 9 then x * raiseTenTo (9 - totalDigits) else quot x (raiseTenTo (totalDigits - 9))- return (fromIntegral result)- ) <|> return 0- return (s * 1000000000 + nanoseconds)+ pure (fromIntegral result)+ ) <|> pure 0+ pure (s * 1000000000 + nanoseconds) zdecimal :: Z.Parser Int64 zdecimal = do@@ -1671,64 +2049,82 @@ wordIsDigit :: Word8 -> Bool wordIsDigit a = 0x30 <= a && a <= 0x39 +-- | The 'Month' of January. january :: Month january = Month 0 +-- | The 'Month' of February. february :: Month february = Month 1 +-- | The 'Month' of March. march :: Month march = Month 2 +-- | The 'Month' of April. april :: Month april = Month 3 +-- | The 'Month' of May. may :: Month may = Month 4 +-- | The 'Month' of June. june :: Month june = Month 5 +-- | The 'Month' of July. july :: Month july = Month 6 +-- | The 'Month' of August. august :: Month august = Month 7 +-- | The 'Month' of September. september :: Month september = Month 8 +-- | The 'Month' of October. october :: Month october = Month 9 +-- | The 'Month' of November. november :: Month november = Month 10 +-- | The 'Month' of December. december :: Month december = Month 11 +-- | The 'DayOfWeek' Sunday. sunday :: DayOfWeek sunday = DayOfWeek 0 +-- | The 'DayOfWeek' Monday. monday :: DayOfWeek monday = DayOfWeek 1 +-- | The 'DayOfWeek' Tuesday. tuesday :: DayOfWeek tuesday = DayOfWeek 2 +-- | The 'DayOfWeek' Wednesday. wednesday :: DayOfWeek wednesday = DayOfWeek 3 +-- | The 'DayOfWeek' Thursday. thursday :: DayOfWeek thursday = DayOfWeek 4 +-- | The 'DayOfWeek' Friday. friday :: DayOfWeek friday = DayOfWeek 5 +-- | The 'DayOfWeek' Saturday. saturday :: DayOfWeek saturday = DayOfWeek 6 - countDigits :: (Integral a) => a -> Int countDigits v0 | fromIntegral v64 == v0 = go 1 v64@@ -1766,7 +2162,7 @@ case Text.decimal t of Left err -> fail err Right (i,r) -> if Text.null r- then return i+ then pure i else fail "datetime decoding could not parse integral text" parseFixedDigitsIntBS :: Int -> AB.Parser Int@@ -1775,7 +2171,7 @@ case BC.readInt t of Nothing -> fail "datetime decoding could not parse integral bytestring (a)" Just (i,r) -> if BC.null r- then return i+ then pure i else fail "datetime decoding could not parse integral bytestring (b)" -- Only provide positive numbers to this function.@@ -1836,11 +2232,54 @@ zeroPadDayOfMonthBS :: DayOfMonth -> BB.Builder zeroPadDayOfMonthBS (DayOfMonth d) = indexTwoDigitByteStringBuilder d +-- | Is the given 'Time' within the 'TimeInterval'?+within :: Time -> TimeInterval -> Bool+t `within` (TimeInterval t0 t1) = t >= t0 && t <= t1++-- | Convert a 'TimeInterval' to a 'Timespan'. This is equivalent to 'width'.+timeIntervalToTimespan :: TimeInterval -> Timespan+timeIntervalToTimespan = width++-- | The 'TimeInterval' that covers the entire range of 'Time's that Chronos supports.+--+-- prop> \(t :: Time) -> within t whole+whole :: TimeInterval+whole = TimeInterval minBound maxBound++-- | The singleton (degenerate) 'TimeInterval'.+singleton :: Time -> TimeInterval+singleton x = TimeInterval x x++-- | Get the lower bound of the 'TimeInterval'.+lowerBound :: TimeInterval -> Time+lowerBound (TimeInterval t0 _) = t0++-- | Get the upper bound of the 'TimeInterval'.+upperBound :: TimeInterval -> Time+upperBound (TimeInterval _ t1) = t1++-- | The width of the 'TimeInterval'. This is equivalent to 'timeIntervalToTimespan'.+width :: TimeInterval -> Timespan+width (TimeInterval x y) = difference y x++-- | A smart constructor for 'TimeInterval'. In general, you should prefer using this+-- over the 'TimeInterval' constructor, since it maintains the invariant that+-- @'lowerBound' interval '<=' 'upperBound' interval@.+timeIntervalBuilder :: Time -> Time -> TimeInterval+timeIntervalBuilder x y = case compare x y of+ GT -> TimeInterval y x+ _ -> TimeInterval x y++infix 3 ...++-- | An infix 'timeIntervalBuilder'.+(...) :: Time -> Time -> TimeInterval+(...) = timeIntervalBuilder+ -- | A day represented as the modified Julian date, the number of days -- since midnight on November 17, 1858. newtype Day = Day { getDay :: Int } deriving (Show,Read,Eq,Ord,Hashable,Enum,ToJSON,FromJSON,Storable,Prim)- instance Torsor Day Int where add i (Day d) = Day (d + i) difference (Day a) (Day b) = a - b@@ -1872,6 +2311,7 @@ else error "Enum.pred{Month}: tried to take pred of January" enumFrom x = enumFromTo x (Month 11) +-- | 'Month' starts at 0 and ends at 11 (January to December) instance Bounded Month where minBound = Month 0 maxBound = Month 11@@ -1881,17 +2321,28 @@ newtype Year = Year { getYear :: Int } deriving (Show,Read,Eq,Ord) +-- | A <https://en.wikipedia.org/wiki/UTC_offset UTC offset>. newtype Offset = Offset { getOffset :: Int } deriving (Show,Read,Eq,Ord,Enum) -- | POSIX time with nanosecond resolution. newtype Time = Time { getTime :: Int64 }- deriving (FromJSON,ToJSON,Hashable,Eq,Ord,Show,Read,Storable,Prim)+ deriving (FromJSON,ToJSON,Hashable,Eq,Ord,Show,Read,Storable,Prim,Bounded) +-- | Match a 'DayOfWeek'. By `match`, we mean that a 'DayOfWeekMatch'+-- is a mapping from the integer value of a 'DayOfWeek' to some value+-- of type @a@. You should construct a 'DayOfWeekMatch' with+-- 'buildDayOfWeekMatch', and match it using 'caseDayOfWeek'. newtype DayOfWeekMatch a = DayOfWeekMatch { getDayOfWeekMatch :: Vector a } +-- | Match a 'Month'. By `match`, we mean that a 'MonthMatch' is+-- a mapping from the integer value of a 'Month' to some value of+-- type @a@. You should construct a 'MonthMatch' with+-- 'buildMonthMatch', and match it using 'caseMonth'. newtype MonthMatch a = MonthMatch { getMonthMatch :: Vector a } +-- | Like 'MonthMatch', but the matched value can have an instance of+-- 'UVector.Unbox'. newtype UnboxedMonthMatch a = UnboxedMonthMatch { getUnboxedMonthMatch :: UVector.Vector a } -- | A timespan. This is represented internally as a number@@ -1921,6 +2372,7 @@ data SubsecondPrecision = SubsecondPrecisionAuto -- ^ Rounds to second, millisecond, microsecond, or nanosecond | SubsecondPrecisionFixed {-# UNPACK #-} !Int -- ^ Specify number of places after decimal+ deriving (Eq, Ord, Show, Read) -- | A date as represented by the Gregorian calendar. data Date = Date@@ -1929,7 +2381,8 @@ , dateDay :: {-# UNPACK #-} !DayOfMonth } deriving (Show,Read,Eq,Ord) --- | The year and number of days elapsed since the beginning it began.+-- | An 'OrdinalDate' is a 'Year' and the number of days elapsed+-- since the 'Year' began. data OrdinalDate = OrdinalDate { ordinalDateYear :: {-# UNPACK #-} !Year , ordinalDateDayOfYear :: {-# UNPACK #-} !DayOfYear@@ -1942,13 +2395,14 @@ , monthDateDay :: {-# UNPACK #-} !DayOfMonth } deriving (Show,Read,Eq,Ord) --- | A date as represented by the Gregorian calendar--- and a time of day.+-- | A 'Date' as represented by the Gregorian calendar+-- and a 'TimeOfDay'. data Datetime = Datetime { datetimeDate :: {-# UNPACK #-} !Date , datetimeTime :: {-# UNPACK #-} !TimeOfDay } deriving (Show,Read,Eq,Ord) +-- | A 'Datetime' with a time zone 'Offset'. data OffsetDatetime = OffsetDatetime { offsetDatetimeDatetime :: {-# UNPACK #-} !Datetime , offsetDatetimeOffset :: {-# UNPACK #-} !Offset@@ -1961,6 +2415,9 @@ , timeOfDayNanoseconds :: {-# UNPACK #-} !Int64 } deriving (Show,Read,Eq,Ord) +-- | The format of a 'Datetime'. In particular+-- this provides separators for parts of the 'Datetime'+-- and nothing else. data DatetimeFormat = DatetimeFormat { datetimeFormatDateSeparator :: !(Maybe Char) -- ^ Separator in the date@@ -1992,6 +2449,16 @@ -- ^ abbreviated months starting with January, 12 elements } +-- | A TimeInterval represents a start and end time.+-- It can sometimes be more ergonomic than the 'Torsor' API when+-- you only care about whether or not a 'Time' is within a certain range.+--+-- To construct a 'TimeInterval', it is best to use 'timeIntervalBuilder',+-- which maintains the invariant that @'lowerBound' interval '<=' 'upperBound' interval@+-- (all functions that act on 'TimeInterval's assume this invariant).+data TimeInterval = TimeInterval {-# UNPACK #-} !Time {-# UNPACK #-} !Time+ deriving (Read,Show,Eq,Ord,Bounded)+ -- | Locale-specific formatting for AM and PM. data MeridiemLocale a = MeridiemLocale { meridiemLocaleAm :: !a@@ -2165,4 +2632,4 @@ aesonParserOffset :: Text -> AET.Parser Offset aesonParserOffset t = case decodeOffset OffsetFormatColonOn t of Nothing -> fail "could not parse Offset"- Just x -> return x+ Just x -> pure x
src/Chronos/Internal/CTimespec.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE JavaScriptFFI #-} +{-# OPTIONS_HADDOCK hide #-} -- for doctests+ module Chronos.Internal.CTimespec ( getPosixNanoseconds+ , CTimespec(..) ) where import Foreign@@ -11,8 +14,8 @@ #ifdef ghcjs_HOST_OS foreign import javascript unsafe "Date.now()" currentSeconds :: IO Double getPosixNanoseconds :: IO Int64-getPosixNanoseconds = do - x <- currentSeconds +getPosixNanoseconds = do+ x <- currentSeconds pure $ fromIntegral $ 1000000 * (round x) #else
src/Chronos/Locale/English.hs view
@@ -1,5 +1,11 @@ {-# LANGUAGE OverloadedStrings #-} +{-| This module provides some english locale helpers.+ It is very incomplete. Please send a pull request+ to https://github.com/andrewthad/chronos if you need+ additions to this API.+ -}+ module Chronos.Locale.English ( lower , upper@@ -13,24 +19,47 @@ import Chronos.Types import Data.Text (Text) +-- $setup+--+-- >>> :set -XOverloadedStrings+-- >>> import Chronos (january, august, december, october, caseMonth)++-- | Lowercase "am"/"pm". lower :: MeridiemLocale Text lower = MeridiemLocale "am" "pm" +-- | Uppercase "AM"/"PM". upper :: MeridiemLocale Text upper = MeridiemLocale "AM" "PM" +-- | Lowercase "a.m."/"p.m." lowerDots :: MeridiemLocale Text lowerDots = MeridiemLocale "a.m." "p.m." +-- | Uppercase "A.M."/"P.M." upperDots :: MeridiemLocale Text upperDots = MeridiemLocale "A.M." "P.M." +-- | Unabbreviated 'Month's of the year.+--+-- >>> caseMonth unabbreviated january+-- "January"+--+-- >>> caseMonth unabbreviated december+-- "December" unabbreviated :: MonthMatch Text unabbreviated = buildMonthMatch "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December" +-- | Abbreviated 'Month's of the year.+--+-- >>> caseMonth abbreviated october+-- "Oct"+--+-- >>> caseMonth abbreviated august+-- "Aug" abbreviated :: MonthMatch Text abbreviated = buildMonthMatch "Jan" "Feb" "Mar" "Apr" "May" "Jun"
src/Chronos/Types.hs view
@@ -1,5 +1,3 @@-{-# OPTIONS_GHC -Wall -Werror #-}- {- | Data types for representing different date and time-related information. @@ -19,7 +17,7 @@ 1680 and 2260. All other types in this library correctly represent time a million years before or after 1970. - The vector unbox instances, not yet available, will store+ The vector unbox instances store data in a reasonably compact manner. For example, the instance for 'Day' has three unboxed vectors: 'Int' for the year, 'Int8' for the month, and 'Int8' for the day. This only causes@@ -53,6 +51,7 @@ , OffsetFormat(..) , DatetimeLocale(..) , MeridiemLocale(..)+ , TimeInterval(..) ) where import Chronos
+ test/Doctests.hs view
@@ -0,0 +1,8 @@+import Test.DocTest++main :: IO ()+main = doctest+ [ "-isrc"+ , "src/Chronos.hs"+ , "src/Chronos/Locale/English.hs"+ ]
test/Spec.hs view
@@ -6,26 +6,27 @@ module Main (main) where import Chronos.Types-import Data.List (intercalate)-import Test.QuickCheck (Gen, Arbitrary(..), choose, arbitraryBoundedEnum, genericShrink, elements)-import Test.QuickCheck.Property (failed,succeeded,Result(..))-import Test.Framework (defaultMain, defaultMainWithOpts, testGroup, Test)-import qualified Test.Framework as TF-import Test.Framework.Providers.QuickCheck2 (testProperty)-import Test.HUnit (Assertion,(@?=),assertBool)-import qualified Test.Framework.Providers.HUnit as PH--import Data.Text (Text) import Data.ByteString (ByteString)+import Data.Int (Int64)+import Data.List (intercalate)+import Data.Text (Text) import Data.Text.Lazy.Builder (Builder)+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.Property (failed,succeeded,Result(..))+import qualified Chronos as C+import qualified Data.Attoparsec.ByteString as AttoBS+import qualified Data.Attoparsec.Text as Atto+import qualified Data.ByteString.Builder as BBuilder+import qualified Data.ByteString.Lazy as LByteString import qualified Data.Text as Text import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Builder as Builder-import qualified Data.ByteString.Builder as BBuilder-import qualified Data.ByteString.Lazy as LByteString-import qualified Data.Attoparsec.Text as Atto-import qualified Data.Attoparsec.ByteString as AttoBS-import qualified Chronos as C+import qualified Test.Framework as TF+import qualified Test.Framework.Providers.HUnit as PH+import qualified Torsor as T -- We increase the default number of property-based tests (provided -- by quickcheck) to 1000. Some of the encoding and decoding functions@@ -38,6 +39,7 @@ main = defaultMainWithOpts tests mempty { TF.ropt_test_options = Just mempty { TF.topt_maximum_generated_tests = Just 1000+ , TF.topt_maximum_unsuitable_generated_tests = Just 10000 } } @@ -183,6 +185,33 @@ , testProperty "Isomorphism" $ propEncodeDecodeFullIso C.timeToDatetime C.datetimeToTime ] ]+ , testGroup "TimeInterval"+ [ testGroup "within"+ [ testProperty "Verify that Time bounds are inside TimeInterval" propWithinInsideInterval+ , testProperty "Verify that the sum of Time and the span of TimeInterval is outside the interval"+ propWithinOutsideInterval+ ]+ , testGroup "timeIntervalToTimespan"+ [ PH.testCase "Verify Timespan correctness with TimeInterval"+ (C.timeIntervalToTimespan (TimeInterval (Time 13) (Time 25)) @?= Timespan 12)+ , PH.testCase "Verify Timespan correctness with equal TimeInterval bounds"+ (C.timeIntervalToTimespan (TimeInterval (Time 13) (Time 13)) @?= Timespan 0)+ , testProperty "Almost isomorphism" propEncodeDecodeTimeInterval+ ]+ , testGroup "whole"+ [ PH.testCase "Verify TimeInterval's bound correctness"+ (C.whole @?= TimeInterval (Time (minBound :: Int64)) (Time (maxBound :: Int64)))+ ]+ , testGroup "singleton"+ [ testProperty "Verify that upper and lower bound are always equals" propSingletonBoundsEquals+ ]+ , testGroup "width"+ [ testProperty "Verify Time bounds correctness with TimeSpan" propWidthVerifyBounds+ ]+ , testGroup "timeIntervalBuilder"+ [ testProperty "Verify TimeInterval construction correctness" propTimeIntervalBuilder+ ]+ ] ] failure :: String -> Result@@ -240,7 +269,6 @@ AttoBS.parseOnly (C.parserUtf8_HMS m <* AttoBS.endOfInput) t @?= Right expected - timeOfDayBuilder :: SubsecondPrecision -> Maybe Char -> Text -> TimeOfDay -> Assertion timeOfDayBuilder sp m expected tod = LText.toStrict (Builder.toLazyText (C.builder_HMS sp m tod))@@ -264,6 +292,55 @@ matchBuilder :: Text -> Builder -> Assertion matchBuilder a b = LText.toStrict (Builder.toLazyText b) @?= a +propWithinInsideInterval :: TimeInterval -> Bool+propWithinInsideInterval ti@(TimeInterval t0 t1) = C.within t1 ti && C.within t0 ti++propWithinOutsideInterval :: RelatedTimes -> Bool+propWithinOutsideInterval (RelatedTimes t ti@(TimeInterval t0 t1))+ | t == t0 = discard+ | t == t1 = discard+ | t1 <= t0 = discard+ | t0 < (Time 0) = discard+ | t < (Time 0) = discard+ | otherwise =+ let+ span = C.timeIntervalToTimespan ti+ tm = T.add span t+ in+ not $ C.within tm ti++propEncodeDecodeTimeInterval :: TimeInterval -> Bool+propEncodeDecodeTimeInterval ti@(TimeInterval t0 t1)+ | t0 < (Time 0) = discard+ | t0 >= t1 = discard+ | otherwise =+ let+ span = C.timeIntervalToTimespan ti+ tm = T.add span t0+ in+ t1 == tm++propSingletonBoundsEquals :: Time -> Bool+propSingletonBoundsEquals tm =+ let+ (TimeInterval (Time ti) (Time te)) = C.singleton tm+ in+ ti == te++propWidthVerifyBounds :: TimeInterval -> Bool+propWidthVerifyBounds ti@(TimeInterval (Time lower) (Time upper)) =+ let+ tiWidth = (getTimespan . C.width) ti+ in+ T.add lower tiWidth == upper && T.difference upper tiWidth == lower++propTimeIntervalBuilder :: Time -> Time -> Bool+propTimeIntervalBuilder t0 t1 =+ let+ (TimeInterval ti te) = (C.timeIntervalBuilder t0 t1)+ in+ (getTime te) >= (getTime ti)+ instance Arbitrary TimeOfDay where arbitrary = TimeOfDay <$> choose (0,23)@@ -302,6 +379,20 @@ deriving instance Arbitrary Time +instance Arbitrary TimeInterval where+ arbitrary = do+ t0 <- arbitrary+ t1 <- suchThat arbitrary (>= t0)+ pure (TimeInterval t0 t1)++data RelatedTimes = RelatedTimes Time TimeInterval+ deriving (Show)++instance Arbitrary RelatedTimes where+ arbitrary = do+ ti@(TimeInterval t0 t1) <- arbitrary+ tm <- fmap Time (choose (getTime t0, getTime t1))+ pure $ RelatedTimes tm ti+ instance Arbitrary Offset where arbitrary = fmap Offset (choose ((-24) * 60, 24 * 60))-