packages feed

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 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