time-hourglass 0.2.14 → 0.3.0
raw patch · 22 files changed
+805/−368 lines, 22 filesdep ~basePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base
API changes (from Hackage documentation)
- Time.Compat: dateFromPOSIXEpoch :: Integer -> Date
- Time.Compat: dateFromTAIEpoch :: Integer -> Date
+ Data.Hourglass: Format_DayYear3 :: TimeFormatElem
+ Data.Hourglass: fromRationalElapsedP :: Rational -> ElapsedP
+ Data.Hourglass: fromRationalSecondsP :: Rational -> (Seconds, NanoSeconds)
+ Data.Hourglass: mkElapsedP :: Seconds -> NanoSeconds -> ElapsedP
+ Time.Epoch: MJDEpoch :: MJDEpoch
+ Time.Epoch: data MJDEpoch
+ Time.Epoch: instance GHC.Classes.Eq Time.Epoch.MJDEpoch
+ Time.Epoch: instance GHC.Real.Real (Time.Epoch.ElapsedSinceP e)
+ Time.Epoch: instance GHC.Show.Show Time.Epoch.MJDEpoch
+ Time.Epoch: instance Time.Epoch.Epoch Time.Epoch.MJDEpoch
+ Time.Types: fromRationalElapsedP :: Rational -> ElapsedP
+ Time.Types: fromRationalSecondsP :: Rational -> (Seconds, NanoSeconds)
+ Time.Types: instance GHC.Read.Read Time.Types.TimezoneOffset
+ Time.Types: mkElapsedP :: Seconds -> NanoSeconds -> ElapsedP
- Data.Hourglass: TimeFormatFct :: String -> (DateTime -> String -> Either String (DateTime, String)) -> (DateTime -> String) -> TimeFormatFct
+ Data.Hourglass: TimeFormatFct :: String -> ((DateTime, TimezoneOffset) -> String -> Either String ((DateTime, TimezoneOffset), String)) -> (DateTime -> TimezoneOffset -> String) -> TimeFormatFct
- Data.Hourglass: [timeFormatParse] :: TimeFormatFct -> DateTime -> String -> Either String (DateTime, String)
+ Data.Hourglass: [timeFormatParse] :: TimeFormatFct -> (DateTime, TimezoneOffset) -> String -> Either String ((DateTime, TimezoneOffset), String)
- Data.Hourglass: [timeFormatPrint] :: TimeFormatFct -> DateTime -> String
+ Data.Hourglass: [timeFormatPrint] :: TimeFormatFct -> DateTime -> TimezoneOffset -> String
Files
- CHANGELOG.md +38/−3
- src/Data/Hourglass.hs +15/−10
- src/Data/Hourglass/Compat.hs +0/−38
- src/Data/Hourglass/Epoch.hs +0/−16
- src/Data/Hourglass/Types.hs +0/−21
- src/System/Hourglass.hs +0/−16
- src/Time/Calendar.hs +11/−8
- src/Time/Compat.hs +16/−39
- src/Time/Diff.hs +18/−11
- src/Time/Epoch.hs +34/−17
- src/Time/Format.hs +267/−65
- src/Time/LocalTime.hs +11/−2
- src/Time/System.hs +3/−4
- src/Time/Time.hs +32/−15
- src/Time/Timezone.hs +18/−4
- src/Time/Types.hs +109/−17
- src/Time/Utils.hs +27/−11
- src/unix/Time/Internal.hs +56/−37
- src/win/Time/Internal.hs +6/−4
- tests/CustomTimeFormatFct.hs +65/−0
- tests/Tests.hs +59/−22
- time-hourglass.cabal +20/−8
CHANGELOG.md view
@@ -6,12 +6,47 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). -## 0.2.14 +## 0.3.0 - 2025-08-01 +* Drop support for GHC < 8.6. +* Add `Real` instance for `ElapsedSinceP`. +* Add `Read` instances for `TimezoneOffset` and `Read t => LocalTime t`. +* Export `fromRationalSecondsP`, `mkElapsedP`, and `fromRationalElapsedP`, to + facilitate creation of `ElapsedP` values from precise amounts of seconds or + numbers of seconds and nanoseconds. +* Test added to test suite for `toRational :: ElapsedSinceP -> Rational` and + `fromRationalElapsedP`. +* Add `Format_DayYear3` for a day of the year padded to 3 characters, + represented by @"JJJ"@. +* Change the types of fields of `TimeFormatFct`: the parser and printer are in + terms of `DateTime` and `TimezoneOffset` (rather than just `DateTime`). +* In `localTimePrint` etc, implement `Format_TimezoneName` and `Format_Fct`. +* In `localTimeParseE` etc, implement `Format_Month`, `Format_DayYear`, + `Format_Day`, `Format_TimezoneName`, `Format_Tz_Offset` and `Format_Fct`. +* Test added to test suite for `Format_Fct` parsing and printing. +* In `localTimeParseE` etc, `Format_Spaces` now parses one or more space-like + characters (as previously documented), rather than one space character (as + previously implemented). +* Export `MJDEpoch`, representing the Modified Julian Date (MJD) epoch. +* Drop deprecated modules `Data.Hourglass.Compat`, `Data.Hourglass.Epoch`, + `Data.Hourglass.Types` and `System.Hourglass`. Use modules `Time.Compat`, + `Time.Epoch` and `Time.Types`. +* Drop deprecated function `dateFromPOSIXEpoch`. Use `dataFromUnixEpoch`. +* Drop deprecated function `dateFromTAIEpoch`. Use `dateFromMJDEpoch`. +* Fix Haddock documentaton for `Format_Hours`, `Format_Minutes` and + `Format_Seconds`; they all pad to 2 characters. +* Fix Haddock documentaton for `Format_Millisecond`, `Format_MicroSecond` and + `Format_NanoSecond`; they parse and print components only, and all pad to 3 + characters. +* Fix error message if a `Format_Text` parse fails. +* Add Haddock documentation for the `String` instance of `TimeFormat`. + +## 0.2.14 - 2025-07-24 + * In test-suite and benchmark, depend on main library, drop dependency on package `hourglass`. -## 0.2.13 +## 0.2.13 - 2025-07-23 * Drop support for GHC < 8.4. * Move library modules to directory `src` and benchmark module to directory @@ -32,7 +67,7 @@ * Export new `dateFromMJDEpoch` and deprecate identical `dateFromTAIEpoch` to fix the latter being a misnomer. -## 0.2.12 +## 0.2.12 - 2025-07-21 * Rename `hourglass-0.2.12` package as `time-hourglass-0.2.12`. * Cabal file specifies `cabal-version: 1.12` (not `>= 1.10`).
src/Data/Hourglass.hs view
@@ -8,9 +8,10 @@ Time-related types and functions. Basic types for representing points in time are t'Elapsed' and t'ElapsedP`. The -\'P\' is short for \'precise\'. t'Elapsed' represents numbers of seconds elapsed -since the start of the Unix epoch (1970-01-01 00:00:00 UTC). t'ElapsedP' -represents numbers of seconds and nanoseconds elapsed. +\'P\' is short for \'precise\'. t'Elapsed' represents numbers of non-leap +seconds elapsed since the Unix epoch (that is, the point of time +represented by 1970-01-01 00:00:00 UTC). t'ElapsedP' represents numbers of +non-leap seconds and nanoseconds elapsed since that point in time. Values of other types representing points in time can be converted to and from values of the t'Elapsed' and t'ElapsedP' types. For example: @@ -35,9 +36,13 @@ , Month (..) , WeekDay (..) -- * Points in time - -- ** Elapsed time since the start of the Unix epoch + -- ** Precise amounts of seconds + , fromRationalSecondsP + -- ** Elapsed time since the Unix epoch , Elapsed (..) , ElapsedP (..) + , mkElapsedP + , fromRationalElapsedP -- ** Date, time, and date and time , Date (..) , TimeOfDay (..) @@ -82,20 +87,20 @@ , TimeInterval (..) -- * Parsing and Printing -- ** Format strings + , TimeFormat (..) + , TimeFormatString (..) , TimeFormatElem (..) , TimeFormatFct (..) - , TimeFormatString (..) - , TimeFormat (..) -- ** Common built-in formats , ISO8601_Date (..) , ISO8601_DateAndTime (..) -- ** Format methods - , timePrint - , timeParse - , timeParseE , localTimePrint - , localTimeParse + , timePrint , localTimeParseE + , localTimeParse + , timeParseE + , timeParse ) where import Time.Calendar
− src/Data/Hourglass/Compat.hs
@@ -1,38 +0,0 @@-{- | -Module : Data.Hourglass.Compat -License : BSD-style -Copyright : (c) 2015 Nicolas DI PRIMA <nicolas@di-prima.fr> - -Basic Time conversion compatibility. - -This module aims to help conversion between the types from the package time to -the package hourglass. - -Example of use (extracted from file Example/Time/Compat.hs): - -> import Data.Hourglass as H -> import Time.Compat as C -> import Data.Time as T -> -> transpose :: T.ZonedTime -> H.LocalTime H.DateTime -> transpose oldTime = H.localTime -> offsetTime -> (H.DateTime newDate timeofday) -> where -> T.ZonedTime (T.LocalTime day tod) (T.TimeZone tzmin _ _) = oldTime -> -> newDate :: H.Date -> newDate = C.dateFromMJDEpoch $ T.toModifiedJulianDay day -> -> timeofday :: H.TimeOfDay -> timeofday = C.diffTimeToTimeOfDay $ toRational $ T.timeOfDayToTime tod -> -> offsetTime = H.TimezoneOffset $ fromIntegral tzmin --} - -module Data.Hourglass.Compat -{-# DEPRECATED "Use Time.Compat instead. Will be removed from future versions of this package." #-} - ( module Time.Compat - ) where - -import Time.Compat
− src/Data/Hourglass/Epoch.hs
@@ -1,16 +0,0 @@-{- | -Module : Data.Hourglass.Epoch -License : BSD-style -Copyright : (c) 2014 Vincent Hanquez <vincent@snarc.org> -Stability : experimental -Portability : unknown - -Types and functions related to epochs. --} - -module Data.Hourglass.Epoch - {-# DEPRECATED "Use Time.Epoch instead. Will be removed from future versions of this package." #-} - ( module Time.Epoch - ) where - -import Time.Epoch
− src/Data/Hourglass/Types.hs
@@ -1,21 +0,0 @@-{- | -Module : Data.Hourglass.Types -License : BSD-style -Copyright : (c) 2014 Vincent Hanquez <vincent@snarc.org> - -Basic times units and types. - -While pratically some units could hold infinite values, for practical and -efficient purpose they are limited to int64 types for seconds and int types for -years. - -Most units use the Unix epoch referential, but by no means reduce portability. -The Unix referential works under the Windows platform or any other platforms. --} - -module Data.Hourglass.Types - {-# DEPRECATED "Use Time.Types instead. Will be removed from future versions of this package." #-} - ( module Time.Types - ) where - -import Time.Types
− src/System/Hourglass.hs
@@ -1,16 +0,0 @@-{- | -Module : System.Hourglass -License : BSD-style -Copyright : (c) 2014 Vincent Hanquez <vincent@snarc.org> -Stability : experimental -Portability : unknown - -Get the system timezone and current time value in multiple formats. --} - -module System.Hourglass - {-# DEPRECATED "Use Time.System instead. Will be removed from future versions of this package." #-} - ( module Time.System - ) where - -import Time.System
src/Time/Calendar.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE NumericUnderscores #-} + {- | Module : Time.Calendar License : BSD-style @@ -93,17 +95,18 @@ daysOfDate (Date y m d) = daysBeforeYear y + daysUntilMonth y m + d -- | For the given date in the proleptic Gregorian calendar, and assuming a time --- of 00:00:00 UTC, yield the number of seconds since the start of the Unix --- epoch (1970-01-01 00:00:00 UTC). This assumes each day is 24 hours long. +-- of 00:00:00 UTC, yield the number of non-leap seconds since the Unix epoch +-- (1970-01-01 00:00:00 UTC). dateToUnixEpoch :: Date -> Elapsed dateToUnixEpoch date = Elapsed $ Seconds (fromIntegral (daysOfDate date - epochDays) * secondsPerDay) where - epochDays = 719163 - secondsPerDay = 86400 -- Julian day is 24h + epochDays = 719_163 + secondsPerDay = 86_400 -- Julian day is 24h --- | For the given period of time since the start of the Unix epoch --- (1970-01-01 00:00:00 UTC), yield the corresponding date. +-- | For the given period of time since the Unix epoch +-- (1970-01-01 00:00:00 UTC), yield the corresponding date in the proleptic +-- Gregorian calendar. dateFromUnixEpoch :: Elapsed -> Date dateFromUnixEpoch e = dtDate $ dateTimeFromUnixEpoch e @@ -113,8 +116,8 @@ todToSeconds (TimeOfDay h m s _) = toSeconds h + toSeconds m + s -- | For the given date (in the proleptic Gregorian calendar) and time (in UTC), --- yield the number of seconds that have elapsed since the start of the Unix --- epoch. +-- yield the number of non-leap seconds that have elapsed since the Unix epoch +-- (1970-01-01 00:00:00 UTC). dateTimeToUnixEpoch :: DateTime -> Elapsed dateTimeToUnixEpoch (DateTime d t) = dateToUnixEpoch d + Elapsed (todToSeconds t)
src/Time/Compat.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE NumericUnderscores #-} + {- | Module : Time.Compat License : BSD-style @@ -34,44 +36,30 @@ ( dateFromUnixEpoch , dateFromMJDEpoch , diffTimeToTimeOfDay - -- * Deprecated - , dateFromPOSIXEpoch - , dateFromTAIEpoch ) where import Time.Time ( timeConvert ) import Time.Types ( Date, Elapsed (..), TimeOfDay (..) ) --- | Given an integer which represents the number of days since the start of the --- Unix epoch (1970-01-01 00:00:00 UTC), yield the corresponding date in the +-- | Given an integer which represents the number of days since the Unix epoch +-- (1970-01-01 00:00:00 UTC), yield the corresponding date in the -- proleptic Gregorian calendar. dateFromUnixEpoch :: Integer - -- ^ Number of days since the start of the Unix epoch - -- (1970-01-01 00:00:00 UTC). + -- ^ Number of days since the Unix epoch (1970-01-01 00:00:00 UTC). -> Date dateFromUnixEpoch day = do - let sec = Elapsed $ fromIntegral $ day * 86400 + let sec = Elapsed $ fromIntegral $ day * 86_400 timeConvert sec --- | Same as 'dateFromUnixEpoch'. -dateFromPOSIXEpoch :: - Integer - -- ^ Number of days since the start of the Unix epoch - -- (1970-01-01 00:00:00 UTC). - -> Date -dateFromPOSIXEpoch = dateFromUnixEpoch -{-# DEPRECATED dateFromPOSIXEpoch "Will be removed from future versions of this package. Use dateFromUnixEpoch" #-} - --- | The number of days between the start of the Modified Julian Date (MJD) --- epoch (1858-11-17 00:00:00 UTC) and the start of the Unix epoch --- (1970-01-01 00:00:00 UTC). +-- | The number of days between the Modified Julian Date (MJD) epoch +-- (1858-11-17 00:00:00 UTC) and the Unix epoch (1970-01-01 00:00:00 UTC). daysMJDtoUnix :: Integer -daysMJDtoUnix = 40587 +daysMJDtoUnix = 40_587 --- | Given an integer which represents the number of days since the start of the --- Modified Julian Date (MJD) epoch (1858-11-17 00:00:00 UTC), yields the --- corresponding date in the proleptic Gregorian calendar. +-- | Given an integer which represents the number of days since the Modified +-- Julian Date (MJD) epoch (1858-11-17 00:00:00 UTC), yields the corresponding +-- date in the proleptic Gregorian calendar. -- -- This function allows a user to convert a t'Data.Time.Calendar.Day' -- into t'Date'. @@ -88,19 +76,8 @@ dateFromMJDEpoch dtai = dateFromUnixEpoch (dtai - daysMJDtoUnix) --- | Same as 'dateFromMJDEpoch'. --- --- The name of this function is a misnomer, as the International Atomic Time --- (TAI) epoch starts on 1958-01-01 00:00:00 UTC. -dateFromTAIEpoch :: - Integer - -- ^ Number of days since 1858-11-17 00:00:00 UTC. - -> Date -dateFromTAIEpoch = dateFromMJDEpoch -{-# DEPRECATED dateFromTAIEpoch "Will be removed from future versions of this package. Use dateFromMJDEpoch" #-} - --- | Given a real number representing the number of seconds since the start of --- the day, yield a t'TimeOfDay' value. +-- | Given a real number representing the number of non-leap seconds since the +-- start of the day, yield a t'TimeOfDay' value (assuming no leap seconds). -- -- Example with t'Data.Time.Clock.DiffTime' type from package @time@: -- @@ -120,7 +97,7 @@ diffTimeToTimeOfDay :: Real t => t - -- ^ Number of seconds of the time of the day. + -- ^ Number of non-leap seconds of the time of the day. -> TimeOfDay diffTimeToTimeOfDay dt = do TimeOfDay @@ -134,6 +111,6 @@ r = toRational dt (secs, nR) = properFraction r :: (Integer, Rational) nsecs :: Integer - nsecs = round (nR * 1000000000) + nsecs = round (nR * 1_000_000_000) (minsofday, seconds) = secs `divMod` 60 :: (Integer, Integer) (hours, minutes) = minsofday `divMod` 60 :: (Integer, Integer)
src/Time/Diff.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE NumericUnderscores #-} {- | Module : Time.Diff @@ -30,7 +31,8 @@ ) -- | Type representing periods of time in years, months and days. --- See t'Duration' for periods of time hours, minutes, seconds and nanoseconds. +-- See t'Duration' for periods of time in hours, minutes, seconds (non-leap or +-- all) and nanoseconds. data Period = Period { periodYears :: !Int , periodMonths :: !Int @@ -48,13 +50,18 @@ instance Monoid Period where mempty = Period 0 0 0 --- | Type represeting periods of time in hours, minutes, seconds and --- nanoseconds. See t'Period' for periods of time in years, months and days. +-- | Type represeting periods of time in hours, minutes, seconds (non-leap or +-- all) and nanoseconds. See t'Period' for periods of time in years, months and +-- days. data Duration = Duration - { durationHours :: !Hours -- ^ number of hours - , durationMinutes :: !Minutes -- ^ number of minutes - , durationSeconds :: !Seconds -- ^ number of seconds - , durationNs :: !NanoSeconds -- ^ number of nanoseconds + { durationHours :: !Hours + -- ^ Number of hours. + , durationMinutes :: !Minutes + -- ^ Number of minutes. + , durationSeconds :: !Seconds + -- ^ Number of seconds (non-leap or all). + , durationNs :: !NanoSeconds + -- ^ Number of nanoseconds. } deriving (Data, Eq, Ord, Read, Show) @@ -78,7 +85,7 @@ durationFlatten (Duration h m s (NanoSeconds ns)) = (toSeconds h + toSeconds m + s + Seconds sacc, NanoSeconds ns') where - (sacc, ns') = ns `divMod` 1000000000 + (sacc, ns') = ns `divMod` 1_000_000_000 -- | Normalize a t'Duration' to represent the same period of time with the -- biggest units possible. For example, 62 minutes is normalized as @@ -89,7 +96,7 @@ where (hacc, mi') = (mi+miacc) `divMod` 60 (miacc, s') = (s+sacc) `divMod` 60 - (sacc, ns') = ns `divMod` 1000000000 + (sacc, ns') = ns `divMod` 1_000_000_000 -- | Add the given period of time to the given date. dateAddPeriod :: Date -> Period -> Date @@ -112,11 +119,11 @@ where dMonth = daysInMonth y (toEnum m) --- | Add the given number of seconds to the given t'Elapsed' value. +-- | Add the given number of non-leap seconds to the given t'Elapsed' value. elapsedTimeAddSeconds :: Elapsed -> Seconds -> Elapsed elapsedTimeAddSeconds (Elapsed s1) s2 = Elapsed (s1 + s2) --- | Add the given number of seconds to the given t'ElapsedP' value. +-- | Add the given number of non-leap seconds to the given t'ElapsedP' value. elapsedTimeAddSecondsP :: ElapsedP -> Seconds -> ElapsedP elapsedTimeAddSecondsP (ElapsedP (Elapsed s1) ns1) s2 = ElapsedP (Elapsed (s1 + s2)) ns1
src/Time/Epoch.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE NumericUnderscores #-} {- | Module : Time.Epoch @@ -8,11 +9,11 @@ Stability : experimental Portability : unknown -Types and functions related to epochs. +Types, type classes and functions related to epochs. -} module Time.Epoch - ( -- * Elapsed time from the start of epochs + ( -- * Elapsed time from epochs ElapsedSince (..) , ElapsedSinceP (..) -- * Epoch @@ -20,23 +21,25 @@ -- * Commonly-encountered epochs , UnixEpoch (..) , WindowsEpoch (..) + , MJDEpoch (..) ) where import Control.DeepSeq ( NFData (..) ) import Data.Data ( Data ) +import Data.Ratio ( (%) ) import Time.Time ( Time (..), Timeable (..) ) import Time.Types ( Elapsed (..), ElapsedP (..), NanoSeconds (..) , Seconds (..) ) --- | A type representing the number of seconds that have elapsed since the start --- of a specified epoch. +-- | A type representing the number of non-leap seconds that have elapsed since +-- the specified epoch. newtype ElapsedSince epoch = ElapsedSince Seconds - deriving (Data, Eq, NFData, Num, Ord, Read, Show) + deriving (Data, Eq, NFData, Num, Ord, Read, Show) --- | A type representing the number of seconds and nanoseconds that have elapsed --- since the start of a specified epoch. The \'P\' is short for \'precise\'. +-- | A type representing the number of non-leap seconds and nanoseconds that +-- have elapsed since the specified epoch. The \'P\' is short for \'precise\'. data ElapsedSinceP epoch = ElapsedSinceP {-# UNPACK #-} !(ElapsedSince epoch) {-# UNPACK #-} !NanoSeconds @@ -63,21 +66,26 @@ fromInteger i = ElapsedSinceP (ElapsedSince (fromIntegral i)) 0 --- FIXME instance Real (ElapsedSinceP e) +instance Real (ElapsedSinceP e) where + toRational (ElapsedSinceP (ElapsedSince (Seconds s)) (NanoSeconds 0)) = + fromIntegral s --- | A type class promising epoch-related functionality. --- + toRational (ElapsedSinceP (ElapsedSince (Seconds s)) (NanoSeconds ns)) = + fromIntegral s + (fromIntegral ns % 1_000_000_000) + +-- | A type class promising epoch-related functionality. (Epochs, in this +-- context, are fixed points in time.) class Epoch epoch where -- | The name of the epoch. epochName :: epoch -> String - -- | The start of the epoch relative to the start of the Unix epoch - -- (1970-01-01 00:00:00 UTC), in seconds. A negative number means the epoch - -- starts before the starts of the Unix epoch. + -- | The epoch relative to the Unix epoch (1970-01-01 00:00:00 UTC), in + -- non-leap seconds. A negative number means the epoch is before the Unix + -- epoch. epochDiffToUnix :: epoch -> Seconds --- | A type representing the Unix epoch, which started on --- 1970-01-01 00:00:00 UTC. +-- | A type representing the Unix epoch (the point in time represented by +-- 1970-01-01 00:00:00 UTC). data UnixEpoch = UnixEpoch deriving (Eq, Show) @@ -87,13 +95,22 @@ -- | A type representing the -- [Windows epoch](https://learn.microsoft.com/en-us/windows/win32/sysinfo/file-times), --- which started on 1601-01-01 00:00:00 UTC. +-- (the point in time represented by 1601-01-01 00:00:00 UTC). data WindowsEpoch = WindowsEpoch deriving (Eq, Show) instance Epoch WindowsEpoch where epochName _ = "windows" - epochDiffToUnix _ = -11644473600 + epochDiffToUnix _ = -11_644_473_600 + +-- | A type representing the Modified Julian Date (MJD) epoch (the point in time +-- represented by 1858-11-17 00:00:00 UTC). +data MJDEpoch = MJDEpoch + deriving (Eq, Show) + +instance Epoch MJDEpoch where + epochName _ = "Modified Julian Date" + epochDiffToUnix _ = -3_506_716_800 instance Epoch epoch => Timeable (ElapsedSince epoch) where timeGetElapsedP es = ElapsedP (Elapsed e) 0
src/Time/Format.hs view
@@ -15,10 +15,10 @@ module Time.Format ( -- * Parsing and Printing -- ** Format strings - TimeFormatElem (..) - , TimeFormatFct (..) + TimeFormat (..) , TimeFormatString (..) - , TimeFormat (..) + , TimeFormatElem (..) + , TimeFormatFct (..) -- ** Common built-in formats , ISO8601_Date (..) , ISO8601_DateAndTime (..) @@ -31,13 +31,18 @@ , localTimeParseE ) where -import Data.Char ( isDigit, ord ) +import Data.Char ( isDigit, isSpace, ord ) import Data.Int ( Int64 ) +import Time.Calendar ( getDayOfTheYear ) import Time.Internal ( dateTimeFromUnixEpochP ) import Time.LocalTime ( LocalTime (..), localTime, localTimeToGlobal ) -import Time.Time ( Timeable (..), timeGetDateTimeOfDay ) -import Time.Utils ( pad2, pad4, padN ) +import Time.Time + ( Period (..), Timeable (..), dateAddPeriod + , timeGetDateTimeOfDay + ) +import Time.Timezone ( Timezone (..), TimezoneMinutes (..) ) +import Time.Utils ( pad2, pad3, pad4, padN ) import Time.Types ( Date (..), DateTime (..), Elapsed (..), ElapsedP (..) , Hours (..), Minutes (..), Month (..), NanoSeconds (..) @@ -48,71 +53,90 @@ -- | Type representing formatters that can be part of a time format string. data TimeFormatElem = Format_Year2 - -- ^ 2 digit years (70 is 1970, 69 is 2069). + -- ^ 2-digit years (@70@ is 1970, @69@ is 2069). | Format_Year4 - -- ^ 4 digits years. + -- ^ 4-digit years. | Format_Year -- ^ Any digits years. | Format_Month - -- ^ Months (1 to 12). + -- ^ Months (@1@ to @12@). | Format_Month2 - -- ^ Months padded to 2 characters (01 to 12). + -- ^ Months padded to 2 characters (@01@ to @12@). | Format_MonthName_Short - -- ^ Short name of nthe month (\'Jan\', \'Feb\' ..). + -- ^ Short name of the month (@Jan@, @Feb@, ..). | Format_DayYear - -- ^ Day of the year (1 to 365, 366 for leap years). + -- ^ Day of the year (@1@ to @365@, @366@ for leap years). + | Format_DayYear3 + -- ^ Day of the year padded to 3 characters (@001@ to @365@, @366@ for leap + -- years). | Format_Day - -- ^ Day of the month (1 to 31). + -- ^ Day of the month (@1@ to @31@). | Format_Day2 - -- ^ Day of the month padded to 2 characters (01 to 31). + -- ^ Day of the month padded to 2 characters (@01@ to @31@). | Format_Hour - -- ^ Hours (0 to 23). + -- ^ Hours padded to 2 characters (@00@ to @23@). | Format_Minute - -- ^ Minutes (0 to 59). + -- ^ Minutes padded to 2 characters (@00@ to @59@). | Format_Second - -- ^ sSeconds (0 to 59, 60 for leap seconds). + -- ^ Seconds padded to 2 characters (@00@ to @59@, @60@ for leap seconds). | Format_UnixSecond - -- ^ Number of seconds since the start of the Unix epoch + -- ^ Number of non-leap seconds since the Unix epoch -- (1970-01-01 00:00:00 UTC). | Format_MilliSecond - -- ^ Milliseconds padded to 3 characters (000 to 999). + -- ^ The millisecond component only, padded to 3 characters (@000@ to + -- @999@). See 'Format_MicroSecond' and 'Format_NanoSecond' for other + -- named sub-second components. | Format_MicroSecond - -- ^ MicroSeconds padded to 6 characters (000000 to 999999). + -- ^ The microseconds component only, padded to 3 characters (@000@ to + -- @999@). See 'Format_MilliSecond' and 'Format_NanoSecond' for other + -- named sub-second components. | Format_NanoSecond - -- ^ NanoSeconds padded to 9 characters (000000000 to 999999999). + -- ^ The nanoseconds component only, padded to 3 characters (@000@ to + -- @999@). See 'Format_MilliSecond' and 'Format_MicroSecond' for other + -- named sub-second components. | Format_Precision Int - -- ^ Sub seconds display with a precision of n digits, with n between @1@ + -- ^ Sub-second display with a precision of n digits, with n between @1@ -- and @9@. | Format_TimezoneName - -- ^ Timezone name (e.g. GMT, PST). Not yet implemented. --- Format_TimezoneOffset - -- ^ Timezone offset offset (+02:00). + -- ^ Timezone name. | Format_TzHM_Colon_Z - -- ^ Zero UTC offset (Z) or timezone offset with colon (+02:00). + -- ^ Zero UTC offset (@Z@) or timezone offset with colon (for example, + -- @02:00@, @+02:00@ or @-02:00@). | Format_TzHM_Colon - -- ^ Timezone offset with colon (+02:00). + -- ^ Timezone offset with colon (for example, @02:00@, @+02:00@ or + -- @-02:00@). | Format_TzHM - -- ^ Timezone offset without colon (+0200). + -- ^ Timezone offset without colon (for example, @0200@, @+0200@ or + -- @-0200@). | Format_Tz_Offset - -- ^ Timezone offset in minutes. + -- ^ Timezone offset in minutes (for example, @120@, @+120@ or @-120@). | Format_Spaces - -- ^ One or more space-like chars. + -- ^ One or more space-like characters. | Format_Text Char -- ^ A verbatim character. | Format_Fct TimeFormatFct - -- ^ Not implemented. + -- ^ A custom time format function. See v'TimeFormatFct'. deriving (Eq, Show) -- | Type representing format functions. data TimeFormatFct = TimeFormatFct { timeFormatFctName :: String -- ^ The name of the format function. - , timeFormatParse :: DateTime -> String -> Either String (DateTime, String) - -- ^ A parser. - , timeFormatPrint :: DateTime -> String - -- A printer. + , timeFormatParse :: + (DateTime, TimezoneOffset) + -> String + -> Either String ((DateTime, TimezoneOffset), String) + -- ^ A parser of a given 'String'. The first argument is the + -- @(@v'DateTime'@,@ v'TimezoneOffset'@)@ value before the parser is + -- applied. If the parser fails, the 'Left' value provides an error message. + -- If it succeeds, the 'Right' value provides a pair of a + -- @(@v'DateTime'@,@ v'TimezoneOffset'@)@ value and any input not consumed + -- by the parser. + , timeFormatPrint :: DateTime -> TimezoneOffset -> String + -- ^ A printer of a given v'DateTime' value and v'TimezoneOffset' value. } +-- | Show the 'timeFormatFctName' field. instance Show TimeFormatFct where show = timeFormatFctName @@ -126,6 +150,57 @@ -- | A type class promising the ability to convert values to -- a t'TimeFormatString'. +-- +-- 'String' is an instance of 'TimeFormat'. Sequences of characters are +-- interpreted as follows (case-sensitive). The longest valid sequence is parsed +-- first: +-- +-- [@YY@]: 'Format_Year2'. 2-digit years (@70@ is 1970, @69@ is 2069). +-- [@YYYY@]: 'Format_Year4'. 4-digit years. +-- [@M@]: 'Format_Month'. Months (@1@ to @12@). +-- [@MM@]: 'Format_Month2'. Months padded to 2 characters (@01@ to @12@). +-- [@Mon@]: 'Format_MonthName_Short'. Short name of the month (@Jan@, @Feb@, +-- ..). +-- [@JJJ@]: 'Format_DayYear3'. Day of the year padded to 3 characters (@001@ +-- to @365@, @366@ for leap years). +-- [@DD@]: 'Format_Day2'. Day of the month padded to 2 characters (@01@ to +-- @31@). +-- [@H@]: 'Format_Hour'. Hours padded to 2 characters (@00@ to @23@). +-- [@MI@]: 'Format_Minute'. Minutes padded to 2 characters (@00@ to @59@). +-- [@S@]: 'Format_Second'. Seconds padded to 2 characters (@00@ to @59@, +-- @60@ for leap seconds). +-- [@EPOCH@]: 'Format_UnixSecond'. Number of non-leap seconds since the Unix +-- epoch (1970-01-01 00:00:00 UTC). +-- [@ms@]: 'Format_MilliSecond'. The millisecond component only, padded to 3 +-- characters (@000@ to @999@). See @us@/@μ@ and @ns@ for other named +-- sub-second components. +-- [@us@]: 'Format_MicroSecond'. The microseconds component only, padded to 3 +-- characters (@000@ to @999@). See @ms@ and @ns@ for other named +-- sub-second components. +-- [@μ@]: 'Format_MicroSecond'. As above. +-- [@ns@]: 'Format_NanoSecond'. The nanoseconds component only, padded to 3 +-- characters (@000@ to @999@). See @ms@ and @us@/@μ@ for other named +-- sub-second components. +-- [@p\<n\>@]: 'Format_Precision' @\<n\>@. Sub-second display with a precision +-- of @\<n\>@ digit(s), where @\<n\>@ is @1@ to @9@. +-- [@TZH:M@]: 'Format_TzHM_Colon'. Timezone offset with colon (for example, +-- @02:00@, @+02:00@ or @-02:00@). +-- [@TZHM@]: 'Format_TzHM'. Timezone offset without colon (for example, +-- @0200@, @+0200@ or @-0200@). +-- [@TZOFS@]: 'Format_Tz_Offset'. Timezone offset in minutes (for example, +-- @120@, @+120@ or @-120@). +-- [@\<space\>@]: 'Format_Spaces'. One or more space-like characters. +-- [@\\\<character\>@]: 'Format_Text' @\<character\>@. A verbatim character. +-- [@\<character\>@]: 'Format_Text' @\<character\>@. A verbatim character. +-- +-- For example: +-- +-- >>> let mDateTime = timeParse ("ms \\ms us \\us ns \\ns") "123 ms 456 us 789 ns" +-- >>> timeGetNanoSeconds <$> mDateTime +-- Just 123456789ns +-- +-- >>> timePrint "ms \\ms us \\us ns \\ns" <$> mDateTime +-- Just "123 ms 456 us 789 ns" class TimeFormat format where toFormat :: format -> TimeFormatString @@ -147,6 +222,8 @@ instance TimeFormat TimeFormatString where toFormat = id +-- | For information about this instance, see the documentation for +-- 'TimeFormat'. instance TimeFormat String where toFormat = TimeFormatString . toFormatElem where @@ -157,6 +234,7 @@ toFormatElem ('M':'o':'n':r) = Format_MonthName_Short : toFormatElem r toFormatElem ('M':'I':r) = Format_Minute : toFormatElem r toFormatElem ('M':r) = Format_Month : toFormatElem r + toFormatElem ('J':'J':'J':r) = Format_DayYear3 : toFormatElem r toFormatElem ('D':'D':r) = Format_Day2 : toFormatElem r toFormatElem ('H':r) = Format_Hour : toFormatElem r toFormatElem ('S':r) = Format_Second : toFormatElem r @@ -186,16 +264,17 @@ instance TimeFormat ISO8601_Date where toFormat _ = - TimeFormatString [Format_Year,dash,Format_Month2,dash,Format_Day2] + TimeFormatString [Format_Year, dash, Format_Month2, dash, Format_Day2] where dash = Format_Text '-' instance TimeFormat ISO8601_DateAndTime where toFormat _ = TimeFormatString - [ Format_Year,dash,Format_Month2,dash,Format_Day2 -- date + [ Format_Year, dash, Format_Month2, dash, Format_Day2 -- date , Format_Text 'T' - , Format_Hour,colon,Format_Minute,colon,Format_Second -- time - , Format_TzHM_Colon_Z -- zero UTC offset (Z) or timezone offset with colon +HH:MM + , Format_Hour, colon, Format_Minute, colon, Format_Second -- time + , Format_TzHM_Colon_Z + -- Either timezone offset with colon (±HH:MM) or UTC zero offset (Z). ] where dash = Format_Text '-' @@ -221,10 +300,10 @@ printWith :: (TimeFormat format, Timeable t) => format - -> TimezoneOffset -> t + -> TimezoneOffset -> String -printWith fmt tzOfs@(TimezoneOffset tz) t = concatMap fmtToString fmtElems +printWith fmt t tzOfs@(TimezoneOffset tz) = concatMap fmtToString fmtElems where fmtToString Format_Year = show (dateYear date) fmtToString Format_Year4 = pad4 (dateYear date) @@ -232,6 +311,8 @@ fmtToString Format_Month2 = pad2 (fromEnum (dateMonth date) + 1) fmtToString Format_Month = show (fromEnum (dateMonth date) + 1) fmtToString Format_MonthName_Short = take 3 $ show (dateMonth date) + fmtToString Format_DayYear = show (getDayOfTheYear date) + fmtToString Format_DayYear3 = pad3 (getDayOfTheYear date) fmtToString Format_Day2 = pad2 (dateDay date) fmtToString Format_Day = show (dateDay date) fmtToString Format_Hour = pad2 (fromIntegral (todHour tm) :: Int) @@ -244,7 +325,7 @@ | n >= 1 && n <= 9 = padN n (ns `div` (10 ^ (9 - n))) | otherwise = error "invalid precision format" fmtToString Format_UnixSecond = show unixSecs - fmtToString Format_TimezoneName = "" -- + fmtToString Format_TimezoneName = timezoneName $ TimezoneMinutes tz fmtToString Format_Tz_Offset = show tz fmtToString Format_TzHM = show tzOfs fmtToString Format_TzHM_Colon_Z @@ -256,36 +337,92 @@ in sign ++ pad2 tzH ++ ":" ++ pad2 tzM fmtToString Format_Spaces = " " fmtToString (Format_Text c) = [c] - fmtToString f = error ("unimplemented printing format: " ++ show f) + fmtToString (Format_Fct tff) = timeFormatPrint tff dateTime tzOfs (TimeFormatString fmtElems) = toFormat fmt (Elapsed (Seconds unixSecs)) = timeGetElapsed t - (DateTime date tm) = timeGetDateTimeOfDay t + dateTime@(DateTime date tm) = timeGetDateTimeOfDay t (NanoSeconds ns) = timeGetNanoSeconds t -- | Given the specified format, pretty print the given local time. +-- +-- A v'Format_TimezoneName' will print using the ±HH:MM format, where @0@ +-- offset is printed as @-00:00@. +-- +-- A v'Format_TzHM' will print using the ±HHMM format, where @0@ offset is +-- printed as @+0000@. +-- +-- A v'Format_TzHM_Colon' will print using the ±HH:MM format, where @0@ offset +-- is printed as @+00:00@. +-- +-- A v'Format_TzHM_Colon_Z' will print using the ±HH:MM format, but where @0@ +-- offset is printed as @Z@. +-- +-- A v'Format_Tz_Offset' will print non-negative offsets without using an +-- initial @+@. +-- +-- A v'Format_Spaces' will print a single space character. localTimePrint :: (TimeFormat format, Timeable t) => format -- ^ The format to use for printing. -> LocalTime t -- ^ The local time to print. -> String localTimePrint fmt lt = - localTimeUnwrap $ fmap (printWith fmt (localTimeGetTimezone lt)) lt + printWith fmt (localTimeUnwrap lt) (localTimeGetTimezone lt) --- | Given the specified format, pretty print the given time. +-- | Like 'localTimePrint' but the time zone of the time to print will be taken +-- to be UTC. timePrint :: (TimeFormat format, Timeable t) => format -- ^ The format to use for printing. -> t -- ^ The time to print. -> String -timePrint fmt = printWith fmt timezone_UTC +timePrint fmt t = printWith fmt t timezone_UTC --- | Given the specified format, try to parse the given string as time value. +-- | Given the specified format, try to parse the given string as +-- a t'LocalTime' t'DateTime' value. -- --- On failure, the parsing function returns the reason of the failure. +-- On failure, yields a 'Left' value with a pair of the +-- current t'TimeFormatElem' value and the reason for the failure. -- --- If successful, yield the parsed value and the remaining unparsed string. +-- If successful, yields a 'Right' value with a pair of the parsed value and +-- the remaining unparsed string. +-- +-- The default parsed t'LocalTime' t'DateTime' value is \'all zeros'\. For +-- example: +-- +-- >>> let zeroDate = Date 0 January 0 +-- >>> let zeroTime = TimeOfDay 0 0 0 0 +-- >>> let zeroLocalTime = localTime timezone_UTC (DateTime zeroDate zeroTime) +-- >>> localTimeParseE "" "" == Right (zeroLocalTime, "") +-- True +-- +-- Later t'TimeFormatElem' values can modify the result of earlier +-- t'TimeFormatElem' values. For example: +-- +-- >>> let toYear = dateYear . dtDate . localTimeUnwrap . fst +-- >>> toYear <$> (localTimeParseE "YYYY YYYY" "2025 2024") +-- Right 2024 +-- +-- A v'Format_DayYear' or v'Format_DayYear3' value interprets the day of year +-- based on the previously parsed year or, by default, a leap year. For example: +-- +-- >>> let toMonth = dateMonth . dtDate . localTimeUnwrap . fst +-- >>> let format1 = [Format_Year4, Format_Spaces, Format_DayYear] +-- >>> let format2 = [Format_DayYear, Format_Spaces, Format_Year4] +-- >>> toMonth <$> (localTimeParseE format1 "2025 60") +-- Right March +-- >>> toMonth <$> (localTimeParseE format2 "60 2025") +-- Right February +-- +-- A v'Format_TimezoneName' will parse one or more non-white space characters +-- but will not modify the previously parsed, or default, date and time. +-- +-- A v'Format_Month', v'Format_DayYear', v'Format_DayYear3', v'Format_Day' +-- and v'Format_Tz_Offset' will check that the parsed number is within bounds. +-- However, 'localTimeParseE' does not check that any resulting date or time is +-- a valid one. localTimeParseE :: TimeFormat format => format -- ^ The format to use for parsing. @@ -304,10 +441,15 @@ Left err -> Left (x, err) Right (nacc, s') -> loop nacc xs s' + processOne :: + (DateTime, TimezoneOffset) + -> TimeFormatElem + -> [Char] + -> Either String ((DateTime, TimezoneOffset), [Char]) processOne _ _ [] = Left "empty" processOne acc (Format_Text c) (x:xs) | c == x = Right (acc, xs) - | otherwise = Left ("unexpected char, got: " ++ show c) + | otherwise = Left ("unexpected char, got: " ++ show x) processOne acc Format_Year s = onSuccess (\y -> modDate (setYear y) acc) $ isNumber s @@ -318,12 +460,55 @@ in modDate (setYear year) acc ) $ getNDigitNum 2 s + processOne acc Format_Month s = + let result = isNumber s :: Either String (Int, String) + in case result of + Left err -> Left err + Right (m, s') + | m > 0 && m <= 12 -> + Right (modDate (setMonth (toEnum (m - 1))) acc, s') + | otherwise -> Left ("month invalid, got: " <> show m) processOne acc Format_Month2 s = onSuccess ( \m -> modDate (setMonth $ toEnum ((fromIntegral m - 1) `mod` 12)) acc ) $ getNDigitNum 2 s processOne acc Format_MonthName_Short s = onSuccess (\m -> modDate (setMonth m) acc) $ getMonth s + processOne acc Format_DayYear s = + let y = (dateYear . dtDate . fst) acc + result = isNumber s :: Either String (Int, String) + in case result of + Left err -> Left err + Right (d, s') + -- We can't be more helpful because we may not yet know the + -- intended year + | d > 0 && d <= 366 -> + let p = Period 0 0 (d - 1) + startOfYear = Date y January 1 + in Right (modDate (const (dateAddPeriod startOfYear p)) acc, s') + | otherwise -> Left ("day of year invalid, got: " <> show d) + processOne acc Format_DayYear3 s = + let y = (dateYear . dtDate . fst) acc + result = getNDigitNum 3 s :: Either String (Int64, String) + in case result of + Left err -> Left err + Right (d, s') + -- We can't be more helpful because we may not yet know the + -- intended year + | d > 0 && d <= 366 -> + let p = Period 0 0 (fromIntegral d - 1) + startOfYear = Date y January 1 + in Right (modDate (const (dateAddPeriod startOfYear p)) acc, s') + | otherwise -> Left ("day of year invalid, got: " <> show d) + processOne acc Format_Day s = + let result = isNumber s :: Either String (Int, String) + in case result of + Left err -> Left err + Right (d, s') + -- We can't be more helpful because we may not yet know the + -- intended month and year + | d > 0 && d <= 31 -> Right (modDate (setDay d) acc, s') + | otherwise -> Left ("day of month invalid, got: " <> show d) processOne acc Format_Day2 s = onSuccess (\d -> modDate (setDay d) acc) $ getNDigitNum 2 s processOne acc Format_Hour s = @@ -340,6 +525,9 @@ onSuccess (\ns -> modTime (setNsMask (0,3) ns) acc) $ getNDigitNum 3 s processOne acc (Format_Precision p) s = onSuccess (\num -> modTime (setNS num) acc) $ getNDigitNum p s + processOne acc Format_TimezoneName s = case break isSpace s of + ("", _) -> Left ("no non-white space at start of: " <> s) + (_, s2) -> Right (acc, s2) processOne acc Format_UnixSecond s = onSuccess (\sec -> let newDate = @@ -352,10 +540,27 @@ parseHMSign True acc c s processOne acc Format_TzHM (c:s) = parseHMSign False acc c s - - processOne acc Format_Spaces (' ':s) = Right (acc, s) - -- catch all for unimplemented format. - processOne _ f _ = error ("unimplemented parsing format: " ++ show f) + processOne acc Format_Tz_Offset s@(c:cs) = case c of + '-' -> process True (12 * 60) cs + '+' -> process False (14 * 60) cs + _ -> process False (14 * 60) s + where + (dt, _) = acc + process isNeg limit s' = + let result = isNumber s' :: Either String (Int, String) + in case result of + Left err -> Left err + Right (mins, rest) + | mins >= 0 && mins <= limit -> + let mins' = if isNeg then negate mins else mins + in Right ((dt, TimezoneOffset mins'), rest) + | otherwise -> + let sign = if isNeg then "-" else "+" + in Left ("offset invalid, got: " <> sign <> show mins) + processOne acc Format_Spaces s = case span isSpace s of + ("", _) -> Left ("no white space at start of: " <> s) + (_, s2) -> Right (acc, s2) + processOne acc (Format_Fct tff) s = timeFormatParse tff acc s parseHMSign expectColon acc signChar afterSign = case signChar of @@ -366,11 +571,11 @@ parseHM isNeg True (h1:h2:':':m1:m2:xs) acc | allDigits [h1,h2,m1,m2] = let tz = toTZ isNeg h1 h2 m1 m2 in Right (modTZ (const tz) acc, xs) - | otherwise = Left ("not digits chars: " ++ show [h1,h2,m1,m2]) + | otherwise = Left ("non-digit char(s) in: " ++ show [h1,h2,m1,m2]) parseHM isNeg False (h1:h2:m1:m2:xs) acc | allDigits [h1,h2,m1,m2] = let tz = toTZ isNeg h1 h2 m1 m2 in Right (modTZ (const tz) acc, xs) - | otherwise = Left ("not digits chars: " ++ show [h1,h2,m1,m2]) + | otherwise = Left ("non-digit char(s) in: " ++ show [h1,h2,m1,m2]) parseHM _ _ _ _ = Left "invalid timezone format" toTZ isNeg h1 h2 m1 m2 = TimezoneOffset ((if isNeg then negate else id) minutes) @@ -383,7 +588,7 @@ isNumber :: Num a => String -> Either String (a, String) isNumber s = case span isDigit s of - ("", s2) -> Left ("no digits chars:" ++ s2) + ("", s2) -> Left ("no digits at start of:" ++ s2) (s1, s2) -> Right (toInt s1, s2) getNDigitNum :: Int -> String -> Either String (Int64, String) @@ -391,7 +596,7 @@ case getNChar n s of Left err -> Left err Right (s1, s2) - | not (allDigits s1) -> Left ("not a digit chars in " ++ show s1) + | not (allDigits s1) -> Left ("non-digit char(s) in " ++ show s1) | otherwise -> Right (toInt s1, s2) getMonth :: String -> Either String (Month, String) @@ -437,13 +642,10 @@ s = 10 ^ shift m = 10 ^ mask --- | Given the specified format, try to parse the given string as time value. --- --- On failure, returns 'Nothing'. --- --- If successful, yields 'Just' the parsed value. +-- | Like 'localTimeParseE', but with simpler handing of failure. Does not yield +-- the remaining unparsed string on success. -- --- For more elaborate needs use 'localTimeParseE'. +-- On failure, returns 'Nothing'. If successful, yields 'Just' the parsed value. localTimeParse :: TimeFormat format => format -- ^ The format to use for parsing.
src/Time/LocalTime.hs view
@@ -24,6 +24,7 @@ , localTimeConvert ) where +import Text.Read ( Read (..), readListPrecDefault ) import Time.Diff ( elapsedTimeAddSecondsP ) import Time.Time ( Time, Timeable (..), timeConvert ) import Time.Types ( TimezoneOffset (..), timezoneOffsetToSeconds ) @@ -36,10 +37,18 @@ -- ^ The timezone offset. } --- FIXME add instance Read too. - +-- | Show the 'localTimeUnwrap' field and then the 'localTimeGetTimezone' field. instance Show t => Show (LocalTime t) where show (LocalTime t tz) = show t ++ show tz + +-- | Read a 'LocalTime'. Read the 'localTimeUnwrap' field and then the +-- 'localTimeGetTimezone' field. +instance Read t => Read (LocalTime t) where + readPrec = LocalTime + <$> readPrec + <*> readPrec + + readListPrec = readListPrecDefault instance Eq t => Eq (LocalTime t) where LocalTime t1 tz1 == LocalTime t2 tz2 = tz1 == tz2 && t1 == t2
src/Time/System.hs view
@@ -28,13 +28,12 @@ import Time.Time ( timeGetDateTimeOfDay ) import Time.Types ( DateTime, Elapsed, ElapsedP, TimezoneOffset ) --- | Get the current number of seconds elapsed since the start of the Unix --- epoch. +-- | Get the current number of non-leap seconds elapsed since the Unix epoch. timeCurrent :: IO Elapsed timeCurrent = systemGetElapsed --- | Get the current number of seconds and nanoseconds since the start of the --- Unix epoch (1970-01-01 00:00:00 UTC). +-- | Get the current number of non-leap seconds and nanoseconds since the Unix +-- epoch (1970-01-01 00:00:00 UTC). timeCurrentP :: IO ElapsedP timeCurrentP = systemGetElapsedP
src/Time/Time.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -Wno-unused-imports #-} {-# LANGUAGE ExistentialQuantification #-} {- | @@ -38,9 +39,9 @@ ) import Time.Internal ( dateTimeFromUnixEpoch, dateTimeFromUnixEpochP ) import Time.Types - ( Date, DateTime (..), Elapsed (..), ElapsedP (..) - , NanoSeconds (..), Seconds (..), TimeInterval (..) - , TimeOfDay (..) + ( Date (..), DateTime (..), Elapsed (..), ElapsedP (..) + , Month (..), NanoSeconds (..), Seconds (..) + , TimeInterval (..), TimeOfDay (..) ) -- | A type class promising functionality for: @@ -52,12 +53,12 @@ -- question (should yield @0@ when the type is less precise than seconds). -- class Timeable t where - -- | Convert the given value to the number of elapsed seconds and nanoseconds - -- since the start of the Unix epoch (1970-01-01 00:00:00 UTC). + -- | Convert the given value to the number of non-leap seconds and + -- nanoseconds elapsed since the Unix epoch (1970-01-01 00:00:00 UTC). timeGetElapsedP :: t -> ElapsedP - -- | Convert the given value to the number of elapsed seconds since the start - -- of the Unix epoch (1970-01-01 00:00:00 UTC). + -- | Convert the given value to the number of non-leap seconds elapsed since + -- the Unix epoch (1970-01-01 00:00:00 UTC). -- -- Defaults to 'timeGetElapsedP'. timeGetElapsed :: t -> Elapsed @@ -80,12 +81,12 @@ -- | A type class promising functionality for converting t'ElapsedP' values -- and t'Elapsed' values to values of the type in question. class Timeable t => Time t where - -- | Convert from a number of elapsed seconds and nanoseconds since the start - -- of the Unix epoch (1970-01-01 00:00:00 UTC). + -- | Convert from a number of non-leap seconds and nanoseconds elapsed since + -- the Unix epoch (1970-01-01 00:00:00 UTC). timeFromElapsedP :: ElapsedP -> t - -- | Convert from a number of elapsed seconds since the start of the Unix - -- epoch (1970-01-01 00:00:00 UTC). + -- | Convert from a number of non-leap seconds elapsed since the Unix epoch + -- (1970-01-01 00:00:00 UTC). -- -- Defaults to 'timeFromElapsedP'. timeFromElapsed :: Elapsed -> t @@ -132,7 +133,9 @@ instance Timeable DateTime where timeGetElapsedP d = ElapsedP (dateTimeToUnixEpoch d) (timeGetNanoSeconds d) + timeGetElapsed = dateTimeToUnixEpoch + timeGetNanoSeconds (DateTime _ (TimeOfDay _ _ _ ns)) = ns instance Time DateTime where @@ -181,16 +184,30 @@ {-# RULES "timeGetDateTimeOfDay/ID" timeGetDateTimeOfDay = id #-} {-# RULES "timeGetDateTimeOfDay/Date" timeGetDateTimeOfDay = flip DateTime (TimeOfDay 0 0 0 0) #-} --- | Add the given period of time to the given value for a point time. +-- | Add the given period of time to the given value for a point in time, +-- assuming that the period of time is equated with a number of non-leap +-- seconds. -- -- Example: -- -- > t1 `timeAdd` mempty { durationHours = 12 } +-- +-- Example: +-- +-- >>> startDate = Date 2016 December 31 -- Date of last leap second +-- >>> preLeapSecond = TimeOfDay 23 59 59 0 +-- >>> startDateTime = DateTime startDate preLeapSecond +-- >>> oneNonleapSecond = Duration 0 0 1 0 -- Assume non-leap seconds +-- >>> nextDate = Date 2017 January 1 +-- >>> firstSecond = TimeOfDay 0 0 0 0 +-- >>> endDateTime = DateTime nextDate firstSecond +-- >>> timeAdd startDateTime oneNonleapSecond == endDateTime +-- True timeAdd :: (Time t, TimeInterval ti) => t -> ti -> t timeAdd t ti = timeFromElapsedP $ elapsedTimeAddSecondsP (timeGetElapsedP t) (toSeconds ti) --- | For the two given points in time, yields the difference in seconds +-- | For the two given points in time, yields the difference in non-leap seconds -- between them. -- -- Effectively: @@ -199,8 +216,8 @@ timeDiff :: (Timeable t1, Timeable t2) => t1 -> t2 -> Seconds timeDiff t1 t2 = sec where (Elapsed sec) = timeGetElapsed t1 - timeGetElapsed t2 --- | For the two given points in time, yields the difference in seconds and --- nanoseconds between them. +-- | For the two given points in time, yields the difference in non-leap seconds +-- and nanoseconds between them. -- -- Effectively: --
src/Time/Timezone.hs view
@@ -24,7 +24,8 @@ -- | The name of the timezone. -- - -- Default implementation is an +-HH:MM encoding of the 'timezoneOffset'. + -- The default implementation is an ±HH:MM encoding of the 'timezoneOffset', + -- with an offset of @0@ encoded as @-00:00@. timezoneName :: tz -> String timezoneName = tzMinutesPrint . timezoneOffset @@ -45,13 +46,26 @@ instance Timezone TimezoneMinutes where timezoneOffset (TimezoneMinutes minutes) = minutes --- | Print a minute offset in format: (+-)HH:MM. +-- | Print a minute offset in format: ±HH:MM. An offset of @0@ is encoded as +-- @-00:00@. tzMinutesPrint :: Int -> String tzMinutesPrint offset = - (if offset > 0 then '+' else '-') - : (pad0 h ++ ":" ++ pad0 m) + sign : (pad0 h ++ ":" ++ pad0 m) where (h, m) = abs offset `divMod` 60 pad0 v | v < 10 = '0':show v | otherwise = show v + sign = if offset > 0 + then + '+' + else + -- This may be following the 'Unknown Local Offset Convention' in section + -- 4.3 of RFC 3339 'Date and Time on the Internet: Timestamps', namely: + -- + -- If the time in UTC is known, but the offset to local time is + -- unknown, this can be represented with an offset of "-00:00". This + -- differs semantically from an offset of "Z" or "+00:00", which imply + -- that UTC is the preferred reference point for the specified time. + -- RFC2822 describes a similar convention for email. + '-'
src/Time/Types.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NumericUnderscores #-} {- | Module : Time.Types @@ -22,9 +24,13 @@ , Month (..) , WeekDay (..) -- * Points in time - -- ** Elapsed time since the start of the Unix epoch + -- ** Precise amounts of seconds + , fromRationalSecondsP + -- ** Elapsed time since the Unix epoch , Elapsed (..) , ElapsedP (..) + , mkElapsedP + , fromRationalElapsedP -- ** Date, time, and date and time , Date (..) , TimeOfDay (..) @@ -38,9 +44,12 @@ ) where import Control.DeepSeq ( NFData (..) ) +import Data.Char ( digitToInt, isDigit ) import Data.Data ( Data ) import Data.Int ( Int64 ) import Data.Ratio ( (%) ) +import Text.Read + ( Read (..), ReadPrec, get, pfail, readListPrecDefault ) import Time.Utils ( pad2 ) -- | Type class promising functionality for: @@ -50,28 +59,34 @@ -- * converting a t'Seconds' value to a pair of a value of the type in question -- and a remaining number of seconds. class TimeInterval i where - -- | For the given value, yield a corresponding number of seconds. + -- | For the given value, yield a corresponding number of seconds (non-leap or + -- all). toSeconds :: i -> Seconds - -- | For the given number of seconds, yield a pair of the corresponding value - -- of the type in queston and a remaining number of seconds. + -- | For the given number of seconds (non-leap or all), yield a pair of the + -- corresponding value of the type in queston and a remaining number of + -- seconds. fromSeconds :: Seconds -> (i, Seconds) -- | Type representing numbers of nanoseconds. newtype NanoSeconds = NanoSeconds Int64 deriving (Data, Eq, NFData, Num, Ord, Read) +-- | Show the number of nanoseconds followed by @ns@. (Note: the 'Read' instance +-- is derived.) instance Show NanoSeconds where show (NanoSeconds v) = shows v "ns" instance TimeInterval NanoSeconds where - toSeconds (NanoSeconds ns) = Seconds (ns `div` 1000000000) - fromSeconds (Seconds s) = (NanoSeconds (s * 1000000000), 0) + toSeconds (NanoSeconds ns) = Seconds (ns `div` 1_000_000_000) + fromSeconds (Seconds s) = (NanoSeconds (s * 1_000_000_000), 0) --- | Type representing numbers of seconds. +-- | Type representing numbers of seconds (non-leap or all). newtype Seconds = Seconds Int64 deriving (Data, Eq, Enum, Integral, NFData, Num, Ord, Read, Real) +-- | Show the number of seconds followed by @s@. (Note: the 'Read' instance +-- is derived.) instance Show Seconds where show (Seconds s) = shows s "s" @@ -79,10 +94,23 @@ toSeconds = id fromSeconds s = (s, 0) +-- | Given a precise amount of seconds yield a corresponding pair of +-- t'Seconds' and t'NanoSeconds' values. +-- +-- If the precise amount of seconds is negative the number of nanoseconds will +-- not be positive. This can be contrasted with a normalised t'ElapsedP' value. +fromRationalSecondsP :: Rational -> (Seconds, NanoSeconds) +fromRationalSecondsP s = + let (sWhole, sFrac) = properFraction s + ns = truncate $ sFrac * 1_000_000_000 + in (Seconds sWhole, NanoSeconds ns) + -- | Type representing numbers of minutes. newtype Minutes = Minutes Int64 deriving (Data, Eq, Enum, Integral, NFData, Num, Ord, Read, Real) +-- | Show the number of minutes followed by @m@. (Note: the 'Read' instance is +-- derived.) instance Show Minutes where show (Minutes s) = shows s "m" @@ -96,6 +124,8 @@ newtype Hours = Hours Int64 deriving (Data, Eq, Enum, Integral, NFData, Num, Ord, Read, Real) +-- | Show the number of hours followed by @h@. (Note: the 'Read' instance is +-- derived.) instance Show Hours where show (Hours s) = shows s "h" @@ -105,19 +135,30 @@ where (h, s') = s `divMod` 3600 --- | Type representing numbers of seconds elapsed since the start of the Unix --- epoch (1970-01-01 00:00:00 UTC). +-- | Type representing numbers of non-leap seconds elapsed since the Unix epoch +-- (1970-01-01 00:00:00 UTC). +-- +-- Points in time before the Unix epoch are represented by a negative number of +-- seconds. newtype Elapsed = Elapsed Seconds deriving (Data, Eq, NFData, Num, Ord, Read) +-- | Show the number of seconds followed by @s@. (Note: the 'Read' instance +-- is derived.) instance Show Elapsed where show (Elapsed s) = show s --- | Type representing numbers of seconds and nanoseconds elapsed since the --- start of the Unix epoch (1970-01-01 00:00:00 UTC). -data ElapsedP = ElapsedP {-# UNPACK #-} !Elapsed {-# UNPACK #-} !NanoSeconds +-- | Type representing numbers of non-leap seconds and nanoseconds elapsed since +-- the Unix epoch (1970-01-01 00:00:00 UTC). +data ElapsedP = ElapsedP + {-# UNPACK #-} !Elapsed + {-# UNPACK #-} !NanoSeconds + -- ^ A normalised t'ElapsedP' value has a nanoseconds field that is + -- non-negative and fewer than 1,000,000,000 nanoseconds (being 1 second). deriving (Data, Eq, Ord, Read) +-- | Show the number of seconds followed by @s@, @.@, and the number of +-- nanoseconds followed by @ns@. (Note: the 'Read' instance is derived.) instance Show ElapsedP where show (ElapsedP e ns) = shows e ('.' : show ns) @@ -139,10 +180,27 @@ fromInteger i = ElapsedP (Elapsed (fromIntegral i)) 0 +-- | A constructor of an t'ElapsedP' value. +-- +-- The t'ElapsedP' value will be normalised. That is, the nanoseconds field will +-- be non-negative and fewer than 1,000,000,000 nanoseconds (being 1 second). +mkElapsedP :: Seconds -> NanoSeconds -> ElapsedP +mkElapsedP (Seconds notNormalizedS) (NanoSeconds notNormalizedNS) = + let (s, ns) = notNormalizedNS `divMod` 1_000_000_000 + in ElapsedP (Elapsed $ Seconds $ notNormalizedS + s) (NanoSeconds ns) + +-- | Given a precise amount of non-leap seconds elapsed since the Unix epoch, +-- yield the corresponding t'ElapsedP' value. +-- +-- The t'ElapsedP' value will be normalised. That is, the nanoseconds field will +-- be non-negative and fewer than 1,000,000,000 nanoseconds (being 1 second). +fromRationalElapsedP :: Rational -> ElapsedP +fromRationalElapsedP = uncurry mkElapsedP . fromRationalSecondsP + addElapsedP :: ElapsedP -> ElapsedP -> ElapsedP addElapsedP (ElapsedP e1 (NanoSeconds ns1)) (ElapsedP e2 (NanoSeconds ns2)) = let notNormalizedNS = ns1 + ns2 - (retainedNS, ns) = notNormalizedNS `divMod` 1000000000 + (retainedNS, ns) = notNormalizedNS `divMod` 1_000_000_000 in ElapsedP (e1 + e2 + Elapsed (Seconds retainedNS)) (NanoSeconds ns) subElapsedP :: ElapsedP -> ElapsedP -> ElapsedP @@ -153,7 +211,7 @@ then ElapsedP (notNormalizedS - oneSecond) - (NanoSeconds (1000000000 + notNormalizedNS)) + (NanoSeconds (1_000_000_000 + notNormalizedNS)) else ElapsedP notNormalizedS (NanoSeconds notNormalizedNS) where @@ -165,7 +223,7 @@ fromIntegral s toRational (ElapsedP (Elapsed (Seconds s)) (NanoSeconds ns)) = - fromIntegral s + (fromIntegral ns % 1000000000) + fromIntegral s + (fromIntegral ns % 1_000_000_000) -- | Type representing months of the Julian or Gregorian year. data Month = @@ -216,12 +274,46 @@ timezoneOffsetToSeconds :: TimezoneOffset -> Seconds timezoneOffsetToSeconds (TimezoneOffset ofs) = Seconds (fromIntegral ofs * 60) +-- | Show the time zone offset in the format @±HHMM@. instance Show TimezoneOffset where show (TimezoneOffset tz) = concat [if tz < 0 then "-" else "+", pad2 tzH, pad2 tzM] where (tzH, tzM) = abs tz `divMod` 60 +-- | Read a time zone offset. Accepts the format @±HHMM@ and valid values +-- between @-1200@ and @+1400@. +instance Read TimezoneOffset where + readPrec = get >>= \case + '+' -> parseOffset 1 + '-' -> parseOffset (-1) + _ -> pfail + where + parseOffset :: Int -> ReadPrec TimezoneOffset + parseOffset sign = do + d1 <- getDigit + d2 <- getDigit + d3 <- getDigit + d4 <- getDigit + let hours = d1 * 10 + d2 + mins = d3 * 10 + d4 + offset = sign * (hours * 60 + mins) + if mins > 59 || offset > 14 * 60 || offset < -(12 * 60) + then + pfail + else + pure $ TimezoneOffset offset + + getDigit = do + c <- get + if isDigit c + then + pure (digitToInt c) + else + pfail + + readListPrec = readListPrecDefault + -- | The UTC timezone. -- -- > timezoneOffsetToMinutes timezone_UTC == 0 -- True @@ -250,9 +342,9 @@ , todMin :: {-# UNPACK #-} !Minutes -- ^ Minutes, between 0 and 59. , todSec :: {-# UNPACK #-} !Seconds - -- ^ Seconds, between 0 and 59. 60 when having leap second. + -- ^ Seconds, between 0 and 59. A leap second is represented by 60. , todNSec :: {-# UNPACK #-} !NanoSeconds - -- ^ Nanoseconds, between 0 and 999999999. + -- ^ Nanoseconds, between 0 and 999,999,999. } deriving (Data, Eq, Ord, Read, Show)
src/Time/Utils.hs view
@@ -8,28 +8,44 @@ Some padding / formatting functions. -} -module Time.Utils where +module Time.Utils + ( pad2 + , pad3 + , pad4 + , padN + ) where --- | Pad a number to 2 digits. +-- | Pad a non-negative integer modulo 100 to 2 digits. pad2 :: (Show a, Ord a, Num a, Integral a) => a -> String pad2 v - | v >= 100 = pad2 (v `mod` 100) - | v >= 10 = show v - | otherwise = '0' : show v + | v < 0 = undefined + | v >= 100 = pad2 (v `mod` 100) + | v >= 10 = show v + | otherwise = '0' : show v --- | Pad a number to 4 digits. +-- | Pad a non-negative integer less than 1,000 to 3 digits. +pad3 :: (Show a, Ord a, Num a, Integral a) => a -> String +pad3 v + | v >= 1000 = undefined + | v >= 100 = show v + | v >= 10 = '0' : show v + | otherwise = '0':'0': show v + +-- | Pad a non-negative integer to at least 4 digits. pad4 :: (Show a, Ord a, Num a, Integral a) => a -> String pad4 v - | v >= 1000 = show v - | v >= 100 = '0' : show v - | v >= 10 = '0':'0' : show v - | otherwise = '0':'0':'0': show v + | v < 0 = undefined + | v >= 1000 = show v + | v >= 100 = '0' : show v + | v >= 10 = '0':'0' : show v + | otherwise = '0':'0':'0': show v --- | Pad a number to at least N digits. +-- | Pad a non-negative integer to at least N digits. -- -- If the number is greater, no truncation happens. padN :: (Show a, Ord a, Num a, Integral a) => Int -> a -> String padN n v + | v < 0 = undefined | vlen >= n = vs | otherwise = replicate (n - vlen) '0' ++ vs where
src/unix/Time/Internal.hs view
@@ -35,11 +35,13 @@ , TimezoneOffset (..) ) --- | Convert a Unix epoch precise to t'DateTime'. +-- | Given a number of non-leap seconds and nanoseconds elapsed since the Unix +-- epoch, yield the corresponding t'DateTime' value. dateTimeFromUnixEpochP :: ElapsedP -> DateTime dateTimeFromUnixEpochP (ElapsedP e ns) = fromCP ns $ rawGmTime e --- | Convert a Unix epoch to t'DateTime'. +-- | Given a number of non-leap seconds elapsed since the Unix epoch, yield the +-- corresponding t'DateTime' value. dateTimeFromUnixEpoch :: Elapsed -> DateTime dateTimeFromUnixEpoch e = fromC $ rawGmTime e @@ -48,7 +50,8 @@ systemGetTimezone = TimezoneOffset . fromIntegral . flip div 60 <$> localTime 0 -------------------------------------------------------------------------------- --- | Return the current elapsedP. +-- | Return the current number of non-leap seconds and nanoseconds elapsed since +-- the Unix epoch. systemGetElapsedP :: IO ElapsedP systemGetElapsedP = allocaBytesAligned sofTimespec 8 $ \ptr -> do c_clock_get ptr @@ -61,7 +64,7 @@ toElapsedP (CTime sec) nsec = ElapsedP (Elapsed $ Seconds (fromIntegral sec)) (fromIntegral nsec) --- | Return the current elapsed. +-- | Return the current number of non-leap seconds elapsed since the Unix epoch. systemGetElapsed :: IO Elapsed systemGetElapsed = allocaBytesAligned sofTimespec 8 $ \ptr -> do c_clock_get ptr @@ -80,8 +83,8 @@ foreign import ccall unsafe "localtime_r" c_localtime_r :: Ptr CTime -> Ptr CTm -> IO (Ptr CTm) --- | Return a global time's struct tm based on the number of elapsed second --- since the start of the Unix epoch (1970-01-01 00:00:00 UTC). +-- | Given a number of non-leap seconds elapsed since the Unix epoch +-- (1970-01-01 00:00:00 UTC), yield the corresponding global time's struct tm. rawGmTime :: Elapsed -> CTm rawGmTime (Elapsed (Seconds s)) = unsafePerformIO callTime where @@ -96,6 +99,53 @@ ctime = fromIntegral s {-# NOINLINE rawGmTime #-} +-- | Represent the beginning of @struct tm@. +-- +-- > struct tm +-- > { +-- > int tm_sec; /* Seconds. [0-60] (1 leap second) */ +-- > int tm_min; /* Minutes. [0-59] */ +-- > int tm_hour; /* Hours. [0-23] */ +-- > int tm_mday; /* Day. [1-31] */ +-- > int tm_mon; /* Month. [0-11] */ +-- > int tm_year; /* Year - 1900. */ +-- > int tm_wday; /* Day of week. [0-6] */ +-- > int tm_yday; /* Days in year.[0-365] */ +-- > int tm_isdst; /* DST. [-1/0/1]*/ +-- > +-- > int tm_mon_length; +-- > int tm_year_length; +-- > }; +data CTm = CTm + { ctmSec :: CInt + , ctmMin :: CInt + , ctmHour :: CInt + , ctmMDay :: CInt + , ctmMon :: CInt + , ctmYear :: CInt + } + deriving (Eq, Show) + +instance Storable CTm where + alignment _ = 8 + sizeOf _ = 60 -- account for 9 ints, alignment + 2 unsigned long at end. + peek ptr = do + CTm <$> peekByteOff intPtr 0 + <*> peekByteOff intPtr 4 + <*> peekByteOff intPtr 8 + <*> peekByteOff intPtr 12 + <*> peekByteOff intPtr 16 + <*> peekByteOff intPtr 20 + where + intPtr = castPtr ptr + poke ptr (CTm f0 f1 f2 f3 f4 f5) = do + mapM_ + (uncurry (pokeByteOff intPtr)) + [(0, f0), (4, f1), (8, f2), (12, f3), (16, f4), (20, f5)] + --pokeByteOff (castPtr ptr) 36 f9 + where + intPtr = castPtr ptr + -- | Return a local time's gmtoff (seconds east of UTC). -- -- Use the ill-defined gmtoff (at offset 40) that might or might not be @@ -114,17 +164,6 @@ else peekByteOff ctmPtr 40 ctime = fromIntegral s --- | Represent the beginning of struct tm. -data CTm = CTm - { ctmSec :: CInt - , ctmMin :: CInt - , ctmHour :: CInt - , ctmMDay :: CInt - , ctmMon :: CInt - , ctmYear :: CInt - } - deriving (Eq, Show) - -- | Convert a C structure to a DateTime structure. fromC :: CTm -> DateTime fromC ctm = DateTime date time @@ -146,23 +185,3 @@ fromCP ns ctm = DateTime d (t { todNSec = ns }) where (DateTime d t) = fromC ctm - -instance Storable CTm where - alignment _ = 8 - sizeOf _ = 60 -- account for 9 ints, alignment + 2 unsigned long at end. - peek ptr = do - CTm <$> peekByteOff intPtr 0 - <*> peekByteOff intPtr 4 - <*> peekByteOff intPtr 8 - <*> peekByteOff intPtr 12 - <*> peekByteOff intPtr 16 - <*> peekByteOff intPtr 20 - where - intPtr = castPtr ptr - poke ptr (CTm f0 f1 f2 f3 f4 f5) = do - mapM_ - (uncurry (pokeByteOff intPtr)) - [(0, f0), (4, f1), (8, f2), (12, f3), (16, f4), (20, f5)] - --pokeByteOff (castPtr ptr) 36 f9 - where - intPtr = castPtr ptr
src/win/Time/Internal.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE NumericUnderscores #-} + {- | Module : Time.Internal License : BSD-style @@ -33,24 +35,24 @@ ) unixDiff :: Int64 -unixDiff = 11644473600 +unixDiff = 11_644_473_600 toFileTime :: Elapsed -> FILETIME toFileTime (Elapsed (Seconds s)) = FILETIME val where - val = fromIntegral (s + unixDiff) * 10000000 + val = fromIntegral (s + unixDiff) * 10_000_000 toElapsedP :: FILETIME -> ElapsedP toElapsedP (FILETIME w) = ElapsedP (Elapsed $ Seconds s) (NanoSeconds ns) where - (sWin, hundredNs) = w `divMod` 10000000 + (sWin, hundredNs) = w `divMod` 10_000_000 ns = fromIntegral (hundredNs * 100) s = fromIntegral sWin - unixDiff toElapsed :: FILETIME -> Elapsed toElapsed (FILETIME w) = Elapsed (Seconds s) where - s = fromIntegral (fst (w `divMod` 10000000)) - unixDiff + s = fromIntegral (fst (w `divMod` 10_000_000)) - unixDiff callSystemTime :: Elapsed -> SYSTEMTIME callSystemTime e = unsafePerformIO (fileTimeToSystemTime (toFileTime e))
+ tests/CustomTimeFormatFct.hs view
@@ -0,0 +1,65 @@+module CustomTimeFormatFct + ( customTimeFormatFct + ) where + +import Data.Char ( isDigit, ord ) +import Data.Hourglass + ( Date (..), DateTime (..), Month (..), Period (..) + ,TimeFormatFct (..), dateAddPeriod, getDayOfTheYear + ) +import Data.Int ( Int64 ) + +-- | To test 'Format_Fct', we provide a custom time format function that will +-- replicate @Format_DayYear3@. +customTimeFormatFct :: TimeFormatFct +customTimeFormatFct = TimeFormatFct + { timeFormatFctName = "customTimeFormatFct" + , timeFormatParse = customTimeFormatParse + , timeFormatPrint = customTimeFormatPrint + } + where + customTimeFormatParse acc s = + let y = (dateYear . dtDate . fst) acc + result = getNDigitNum 3 s + in case result of + Left err -> Left err + Right (d, s') + -- We can't be more helpful because we may not yet know the + -- intended year + | d >= 1 && d <= 366 -> + let p = Period 0 0 (fromIntegral d - 1) + startOfYear = Date y January 1 + in Right (modDate (const (dateAddPeriod startOfYear p)) acc, s') + | otherwise -> Left ("day of year invalid, got: " <> show d) + + customTimeFormatPrint date _ = pad3 (getDayOfTheYear $ dtDate date) + + getNDigitNum :: Int -> String -> Either String (Int64, String) + getNDigitNum n s = + case getNChar n s of + Left err -> Left err + Right (s1, s2) + | not (allDigits s1) -> Left ("non-digit char(s) in " ++ show s1) + | otherwise -> Right (toInt s1, s2) + + getNChar :: Int -> String -> Either String (String, String) + getNChar n s + | length s1 < n = + Left ("not enough chars: expecting " ++ show n ++ " got " ++ show s1) + | otherwise = Right (s1, s2) + where + (s1, s2) = splitAt n s + + toInt :: Num a => String -> a + toInt = foldl (\acc w -> acc * 10 + fromIntegral (ord w - ord '0')) 0 + + -- | Pad a number to 3 digits. + pad3 :: (Show a, Ord a, Num a, Integral a) => a -> String + pad3 v + | v >= 100 = show v + | v >= 10 = '0' : show v + | otherwise = '0':'0': show v + + modDate f (DateTime d tp, tz) = (DateTime (f d) tp, tz) + + allDigits = all isDigit
tests/Tests.hs view
@@ -1,6 +1,7 @@ {-# OPTIONS_GHC -Wno-orphans #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE ScopedTypeVariables #-} module Main @@ -9,16 +10,18 @@ import qualified Control.Exception as E import Control.Monad ( when ) +import CustomTimeFormatFct ( customTimeFormatFct ) import Data.Hourglass ( Date (..), DateTime (..), Duration (..), Elapsed (..) , ElapsedP (..), Hours (..), ISO8601_Date (..) , ISO8601_DateAndTime (..), LocalTime, Minutes (..) , Month (..), NanoSeconds (..), Period (..), Seconds (..) - , Time, TimeFormat (..), TimeOfDay (..), TimezoneOffset (..) - , WeekDay, dateAddPeriod, daysInMonth, getWeekDay, localTime - , localTimeFromGlobal, localTimeGetTimezone, localTimeParseE - , localTimeSetTimezone, localTimeToGlobal, timeConvert - , timeGetDateTimeOfDay, timeGetElapsed, timeParseE, timePrint + , Time, TimeFormat (..), TimeFormatElem (..), TimeOfDay (..) + , TimezoneOffset (..), WeekDay, dateAddPeriod, daysInMonth, fromRationalElapsedP + , getWeekDay, localTime, localTimeFromGlobal + , localTimeGetTimezone, localTimeParseE, localTimeSetTimezone + , localTimeToGlobal, timeConvert, timeGetDateTimeOfDay + , timeGetElapsed, timeParseE, timePrint ) import Data.Int ( Int64 ) import Data.Ratio ( (%) ) @@ -31,7 +34,10 @@ ( Assertion, (@=?), assertEqual, assertFailure, testCase ) import Test.Tasty.QuickCheck ( Arbitrary (..), choose, elements, testProperty ) -import Time.Epoch ( ElapsedSince, WindowsEpoch ) +import Time.Epoch + ( ElapsedSince (..), ElapsedSinceP (..), UnixEpoch + , WindowsEpoch + ) import TimeDB ( parseTimeConv ) import TimeRange ( dateRange, hiElapsed, loElapsed ) @@ -66,17 +72,20 @@ | otherwise = error "internal error" instance Arbitrary Minutes where - arbitrary = Minutes <$> choose (-1125899906842624, 1125899906842624) + arbitrary = Minutes <$> choose (-1_125_899_906_842_624, 1_125_899_906_842_624) instance Arbitrary Hours where - arbitrary = Hours <$> choose (-1125899906842, 1125899906842) + arbitrary = Hours <$> choose (-1_125_899_906_842, 1_125_899_906_842) instance Arbitrary NanoSeconds where - arbitrary = NanoSeconds <$> choose (0, 100000000) + arbitrary = NanoSeconds <$> choose (0, 100_000_000) instance Arbitrary Elapsed where arbitrary = Elapsed <$> arbitrary +instance Arbitrary ElapsedP where + arbitrary = ElapsedP <$> arbitrary <*> arbitrary + instance Arbitrary TimezoneOffset where arbitrary = TimezoneOffset <$> choose (-(11*60), 11*60) @@ -85,9 +94,9 @@ instance Arbitrary Period where arbitrary = Period - <$> choose (-29,29) - <*> choose (-27,27) - <*> choose (-400,400) + <$> choose (-29, 29) + <*> choose (-27, 27) + <*> choose (-400, 400) instance Arbitrary Month where arbitrary = elements [January ..] @@ -103,8 +112,8 @@ instance Arbitrary TimeOfDay where arbitrary = (TimeOfDay . Hours <$> choose (0, 23)) - <*> (Minutes <$> choose (0,59)) - <*> (Seconds <$> choose (0,59)) + <*> (Minutes <$> choose (0, 59)) + <*> (Seconds <$> choose (0, 59)) <*> arbitrary instance (Time t, Arbitrary t) => Arbitrary (LocalTime t) where @@ -133,9 +142,9 @@ arithmeticTestAddRef :: [(ElapsedP, ElapsedP, ElapsedP)] arithmeticTestAddRef = map testRefToElapsedP - [ ((1, 090000000), (2, 090000000), (3, 180000000)) - , ((1, 900000000), (1, 200000000), (3, 100000000)) - , ((1, 000000001), (0, 999999999), (2, 000000000)) + [ ((1, 090_000_000), (2, 090_000_000), (3, 180_000_000)) + , ((1, 900_000_000), (1, 200_000_000), (3, 100_000_000)) + , ((1, 000_000_001), (0, 999_999_999), (2, 000_000_000)) ] arithmeticTestSubRef :: [(ElapsedP, ElapsedP, ElapsedP)] @@ -146,7 +155,7 @@ , ((1, ms 100), (2, ms 400), (-2, ms 700)) ] where - ms v = v * 1000000 + ms v = v * 1_000_000 testRefToElapsedP :: ((Int64, Int64), (Int64, Int64), (Int64, Int64)) @@ -174,6 +183,8 @@ let e2 = timeConvert e :: ElapsedSince WindowsEpoch in timePrint ISO8601_DateAndTime e `eq` timePrint ISO8601_DateAndTime e2 + , testProperty "precise seconds" $ \(ep :: ElapsedP) -> + fromRationalElapsedP (toRational ep) `eq` ep ] , testGroup "localtime" [ testProperty "eq" $ \(l :: LocalTime Elapsed) -> @@ -253,16 +264,42 @@ test_property_format ("YYYY-MM-DDTH:MI:S.msusns" :: String) , testProperty "custom-2" $ test_property_format ("Mon DD\\t\\h YYYY at HH\\hMI\\mS\\s.p9\\n\\s" :: String) + , testProperty "customTimeFormatFct" $ + test_property_format + [ Format_Year4 + , Format_Spaces + , Format_Fct customTimeFormatFct + , Format_Spaces + , Format_Hour + , Format_Spaces + , Format_Minute + , Format_Spaces + , Format_Second + , Format_Spaces + , Format_Precision 9 + ] ] , testGroup "Regression Tests" - [ testCase "Real instance of ElapsedP (#33)" $ + [ testCase "Real instance of ElapsedP (#33)" $ let res = toRational (ElapsedP (Elapsed $ Seconds 0) (NanoSeconds 0)) ref = toRational (0 :: Int) :: Rational in assertEqual "failed equality" ref res - , testCase "Real instance of ElapsedP (#33) (2)" $ + , testCase "Real instance of ElapsedP (#33) (2)" $ let res = toRational - (ElapsedP (Elapsed $ Seconds 100) (NanoSeconds 1000000)) - ref = toRational (100 :: Int) + (1 % 1000) :: Rational + (ElapsedP (Elapsed $ Seconds 100) (NanoSeconds 1_000_000)) + ref = toRational (100 :: Int) + (1 % 1_000) :: Rational + in assertEqual "failed equality" ref res + , testCase "Real instance of ElapsedSinceP, zero ns" $ + let start = ElapsedSince (Seconds 0) :: ElapsedSince UnixEpoch + startP = ElapsedSinceP start (NanoSeconds 0) :: ElapsedSinceP UnixEpoch + res = toRational startP + ref = toRational (0 :: Int) :: Rational + in assertEqual "failed equality" ref res + , testCase "Real instance of ElapsedSinceP, non zero ns" $ + let later = ElapsedSince (Seconds 100) :: ElapsedSince UnixEpoch + laterP = ElapsedSinceP later (NanoSeconds 1_000_000) :: ElapsedSinceP UnixEpoch + res = toRational laterP + ref = toRational (100 :: Int) + (1 % 1_000) :: Rational in assertEqual "failed equality" ref res ] ]
time-hourglass.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: time-hourglass -version: 0.2.14 +version: 0.3.0 synopsis: A simple and efficient time library description: A simple and efficient time library. . @@ -36,10 +36,6 @@ library exposed-modules: Data.Hourglass - Data.Hourglass.Compat - Data.Hourglass.Epoch - Data.Hourglass.Types - System.Hourglass Time.Compat Time.Epoch Time.System @@ -54,9 +50,15 @@ Time.Utils hs-source-dirs: src + other-extensions: + DeriveDataTypeable + ExistentialQuantification + FlexibleInstances + GeneralizedNewtypeDeriving + NumericUnderscores ghc-options: -Wall build-depends: - base >=4.11 && <5 + base >=4.12 && <5 , deepseq default-language: Haskell2010 if os(windows) @@ -71,6 +73,8 @@ Time.Internal hs-source-dirs: src/unix + other-extensions: + ForeignFunctionInterface c-sources: cbits/unix.c @@ -79,11 +83,17 @@ main-is: Tests.hs other-modules: TimeDB + CustomTimeFormatFct hs-source-dirs: tests + other-extensions: + BangPatterns + FlexibleInstances + NumericUnderscores + ScopedTypeVariables ghc-options: -Wall build-depends: - base >=4.11 && <5 + base >=4.12 && <5 , deepseq , tasty , tasty-hunit @@ -107,9 +117,11 @@ main-is: Bench.hs hs-source-dirs: benchmarks + other-extensions: + BangPatterns ghc-options: -Wall build-depends: - base >=4.11 && <5 + base >=4.12 && <5 , deepseq , tasty-bench , time