-- | -- Module : Data.Time.Exts.UTC -- Copyright : 2013-2017 Enzo Haussecker -- License : BSD3 -- Maintainer : Enzo Haussecker -- Stability : Stable -- -- A native implementation of Coordinated Universal Time. {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS -fno-warn-name-shadowing #-} module Data.Time.Exts.UTC ( -- * Timestamps UTCDate(..) , UTCDateTime(..) , UTCDateTimeNanos(..) -- * Create , createUTCDate , createUTCDateTime , createUTCDateTimeNanos -- * Get , getCurrentUTCDate , getCurrentUTCDateTime , getCurrentUTCDateTimeNanos -- * Parse , parseUTCDate , parseUTCDateTime , parseUTCDateTimeNanos ) where import Control.Arrow ((***), first) import Control.DeepSeq (NFData) import Control.Monad (join) import Data.Data (Data, Typeable) import Data.Int (Int32, Int64) import Data.Text (Text) import Data.Time.Zones (utcTZ) import Foreign.Ptr (plusPtr) import Foreign.Storable (Storable(..)) import GHC.Generics (Generic) import Lens.Simple (over) import System.Locale (TimeLocale) import System.Random (Random(..)) import Text.Printf (printf) import Data.Time.Exts.Base import Data.Time.Exts.Format import Data.Time.Exts.Lens import Data.Time.Exts.Parser import Data.Time.Exts.Unix import Data.Time.Exts.Util -- | -- Days since Unix epoch. newtype UTCDate cal = UTCDate (UnixDate cal) deriving (Data, Eq, Generic, NFData, Ord, Storable, Typeable) -- | -- Seconds since Unix epoch (including leap seconds). newtype UTCDateTime (cal :: Calendar) = UTCDateTime Int64 deriving (Data, Eq, Generic, NFData, Ord, Storable, Typeable) -- | -- Nanoseconds since Unix epoch (including leap seconds). data UTCDateTimeNanos (cal :: Calendar) = UTCDateTimeNanos {-# UNPACK #-} !Int64 {-# UNPACK #-} !Int32 deriving (Data, Eq, Generic, Ord, Typeable) deriving instance Bounded (UTCDate 'Gregorian) instance Bounded (UTCDateTime 'Gregorian) where -- 12:00:00 AM Thu Jan 01 1970 UTC. minBound = UTCDateTime 0 -- 11:59:59 PM Fri Dec 31 9999 UTC. maxBound = UTCDateTime 253402300827 instance Bounded (UTCDateTimeNanos 'Gregorian) where -- 12:00:00.000000000 AM Thu Jan 01 1970 UTC. minBound = UTCDateTimeNanos 0 0 -- 11:59:59.999999999 PM Fri Dec 31 9999 UTC. maxBound = UTCDateTimeNanos 253402300827 999999999 deriving instance Enum (UTCDate 'Gregorian) instance Enum (UTCDateTime 'Gregorian) where -- Next second. succ = flip plus (Second 1) -- Previous second. pred = flip plus (- Second 1) -- Denumerate a UTC timestamp. fromEnum (UTCDateTime base) = fromIntegral base -- Enumerate a UTC timestamp. toEnum base = if minBound <= time && time <= maxBound then time else error "toEnum{UTCDateTime 'Gregorian}: out of bounds" where time = UTCDateTime $ fromIntegral base instance Human (UTCDate 'Gregorian) where -- Define the Gregorian components of a UTC datestamp. type Components (UTCDate 'Gregorian) = DateStruct 'Gregorian -- Pack a UTC datestamp from Gregorian components. pack DateStruct {..} = createUTCDate _d_year _d_mon _d_mday -- Unpack a UTC datestamp to Gregorian components. unpack (UTCDate date) = unpack date instance Human (UTCDateTime 'Gregorian) where -- Define the Gregorian components of a UTC timestamp. type Components (UTCDateTime 'Gregorian) = DateTimeStruct 'Gregorian -- Pack a UTC timestamp from Gregorian components. pack DateTimeStruct {..} = createUTCDateTime _dt_year _dt_mon _dt_mday _dt_hour _dt_min sec where sec = round _dt_sec -- Unpack a UTC timestamp to Gregorian components. unpack (UTCDateTime base) = over dt_sec (+ leap) (unpack time) where time = UnixDateTime unix :: UnixDateTime 'Gregorian (,) unix leap = baseUTCToUnix base instance Human (UTCDateTimeNanos 'Gregorian) where -- Define the Gregorian components of a UTC timestamp with nanosecond granularity. type Components (UTCDateTimeNanos 'Gregorian) = DateTimeStruct 'Gregorian -- Pack a UTC timestamp with nanosecond granularity from Gregorian components. pack DateTimeStruct {..} = createUTCDateTimeNanos _dt_year _dt_mon _dt_mday _dt_hour _dt_min sec nsec where (,) sec nsec = properFracNanos _dt_sec -- Unpack a UTC timestamp with nanosecond granularity to Gregorian components. unpack (UTCDateTimeNanos base nsec) = over dt_sec (+ leap) (unpack time) where time = UnixDateTimeNanos unix nsec :: UnixDateTimeNanos 'Gregorian (,) unix leap = baseUTCToUnix base instance Math (UTCDate 'Gregorian) Day where -- Compute the day duration between two UTC datestamps. duration (UTCDate old) (UTCDate new) = duration old new -- Add days to a UTC datestamp. plus (UTCDate date) days = UTCDate (plus date days) instance Math (UTCDateTime 'Gregorian) Second where -- Compute the second duration between two UTC timestamps. duration (UTCDateTime old) (UTCDateTime new) = fromIntegral (new - old) -- Add seconds to a UTC timestamp. plus (UTCDateTime base) seconds = if minBound <= time && time <= maxBound then time else error "plus{UTCDateTime 'Gregorian, Second}: out of bounds" where time = UTCDateTime (base + fromIntegral seconds) instance Math (UTCDateTimeNanos 'Gregorian) Second where -- Compute the second duration between two UTC timestamps with nanosecond granularity. duration (UTCDateTimeNanos old _) (UTCDateTimeNanos new _) = fromIntegral (new - old) -- Add seconds to a UTC timestamp with nanosecond granularity. plus (UTCDateTimeNanos base nsec) seconds = if minBound <= time && time <= maxBound then time else error "plus{UTCDateTimeNanos 'Gregorian, Second}: out of bounds" where time = UTCDateTimeNanos (base + fromIntegral seconds) nsec instance Math (UTCDateTimeNanos 'Gregorian) Millis where -- Compute the millisecond duration between two UTC timestamps with nanosecond granularity. duration old new = fold new - fold old where fold (UTCDateTimeNanos base nsec) = fromIntegral base * 1000 + fromIntegral (div nsec 1000000) -- Add milliseconds to a UTC timestamp with nanosecond granularity. plus (UTCDateTimeNanos base nsec) millis = if minBound <= time && time <= maxBound then time else error "plus{UTCDateTimeNanos 'Gregorian, Millis}: out of bounds" where nsum = fromIntegral nsec + fromIntegral millis * 1000000 time = uncurry UTCDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) instance Math (UTCDateTimeNanos 'Gregorian) Micros where -- Compute the microsecond duration between two UTC timestamps with nanosecond granularity. duration old new = fold new - fold old where fold (UTCDateTimeNanos base nsec) = fromIntegral base * 1000000 + fromIntegral (div nsec 1000) -- Add microseconds to a UTC timestamp with nanosecond granularity. plus (UTCDateTimeNanos base nsec) micros = if minBound <= time && time <= maxBound then time else error "plus{UTCDateTimeNanos 'Gregorian, Micros}: out of bounds" where nsum = fromIntegral nsec + fromIntegral micros * 1000 time = uncurry UTCDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) instance Math (UTCDateTimeNanos 'Gregorian) Nanos where -- Compute the nanosecond duration between two UTC timestamps with nanosecond granularity. duration old new = if toInteger (minBound :: Int64) <= res && toInteger (maxBound :: Int64) >= res then fromInteger res else error "duration{UTCDateTimeNanos 'Gregorian, Nanos}: integer overflow" where res = fold new - fold old fold (UTCDateTimeNanos base nsec) = toInteger base * 1000000000 + toInteger nsec -- Add nanoseconds to a UTC timestamp with nanosecond granularity. plus (UTCDateTimeNanos base nsec) nanos = if minBound <= time && time <= maxBound then time else error "plus{UTCDateTimeNanos 'Gregorian, Nanos}: out of bounds" where nsum = fromIntegral nsec + fromIntegral nanos time = uncurry UTCDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) instance NFData (UTCDateTimeNanos cal) deriving instance Random (UTCDate 'Gregorian) instance Random (UTCDateTime 'Gregorian) where -- Generate a random UTC timestamp. random = first toEnum . randomR (fromEnum a, fromEnum b) where a = minBound :: UTCDateTime 'Gregorian b = maxBound :: UTCDateTime 'Gregorian -- Generate a random UTC timestamp uniformly distributed on the closed interval. randomR (a, b) = first toEnum . randomR (fromEnum a, fromEnum b) instance Random (UTCDateTimeNanos 'Gregorian) where -- Generate a random UTC timestamp with nanosecond granularity. random = first toNano . randomR (fromNano a, fromNano b) where a = minBound :: UTCDateTimeNanos 'Gregorian b = maxBound :: UTCDateTimeNanos 'Gregorian -- Generate a random UTC timestamp with nanosecond granularity uniformly distributed on the closed interval. randomR (a, b) = first toNano . randomR (fromNano a, fromNano b) instance Show (UTCDate 'Gregorian) where -- Show a UTC datestamp. show (unpack -> DateStruct {..}) = printf "%.3s %.3s %02d %4d UTC" wday mon _d_mday _d_year where wday = show _d_wday mon = show _d_mon instance Show (UTCDateTime 'Gregorian) where -- Show a UTC timestamp. show (unpack -> DateTimeStruct {..}) = printf "%02d:%02d:%02d %s %.3s %.3s %02d %4d UTC" hour _dt_min sec ampm wday mon _dt_mday _dt_year where wday = show _dt_wday mon = show _dt_mon sec = round _dt_sec :: Second (,) ampm hour = getPeriod _dt_hour instance Show (UTCDateTimeNanos 'Gregorian) where -- Show a UTC timestamp with nanosecond granularity. show (unpack -> DateTimeStruct {..}) = printf "%02d:%02d:%02d.%09d %s %.3s %.3s %02d %4d UTC" hour _dt_min sec nsec ampm wday mon _dt_mday _dt_year where wday = show _dt_wday mon = show _dt_mon (,) sec nsec = properFracNanos _dt_sec (,) ampm hour = getPeriod _dt_hour instance Storable (UTCDateTimeNanos cal) where -- Size of UTC timestamp with nanosecond granularity. sizeOf = const 12 -- Alignment of UTC timestamp with nanosecond granularity. alignment = sizeOf -- Read a UTC timestamp with nanosecond granularity from memory. peekElemOff ptr n = do let off = 12 * n base <- peek (plusPtr ptr (off + 0)) nsec <- peek (plusPtr ptr (off + 8)) return $! UTCDateTimeNanos base nsec -- Write a UTC timestamp with nanosecond granularity to memory. pokeElemOff ptr n (UTCDateTimeNanos base nsec) = do let off = 12 * n poke (plusPtr ptr (off + 0)) base poke (plusPtr ptr (off + 8)) nsec -- | -- Create a UTC datestamp. createUTCDate :: Year -> Month 'Gregorian -> Day -> UTCDate 'Gregorian createUTCDate year mon mday = UTCDate $ createUnixDate year mon mday -- | -- Create a UTC timestamp. createUTCDateTime :: Year -> Month 'Gregorian -> Day -> Hour -> Minute -> Second -> UTCDateTime 'Gregorian createUTCDateTime year mon mday hour min sec = if (minBound :: UTCDateTime 'Gregorian) <= time && time <= (maxBound :: UTCDateTime 'Gregorian) then time else error "createUTCDateTime: out of bounds" where UnixDateTime unix = createUnixDateTime year mon mday hour min 0 time = UTCDateTime base base = baseUnixToUTC unix + fromIntegral sec -- | -- Create a UTC timestamp with nanosecond granularity. createUTCDateTimeNanos :: Year -> Month 'Gregorian -> Day -> Hour -> Minute -> Second -> Nanos -> UTCDateTimeNanos 'Gregorian createUTCDateTimeNanos year mon mday hour min sec nanos = if (minBound :: UTCDateTimeNanos 'Gregorian) <= time && time <= (maxBound :: UTCDateTimeNanos 'Gregorian) then time else error "createUTCDateTimeNanos: out of bounds" where UnixDateTime unix = createUnixDateTime year mon mday hour min 0 time = UTCDateTimeNanos base nsec base = baseUnixToUTC unix + fromIntegral sec + extra (,) extra nsec = (***) fromIntegral fromIntegral (divMod nanos 1000000000) -- | -- Get the current UTC datestamp from the system clock. getCurrentUTCDate :: IO (UTCDate 'Gregorian) getCurrentUTCDate = UTCDate <$> getCurrentUnixDate -- | -- Get the current UTC timestamp from the system clock. getCurrentUTCDateTime :: IO (UTCDateTime 'Gregorian) getCurrentUTCDateTime = do UnixDateTime unix <- getCurrentUnixDateTime return $! UTCDateTime $ baseUnixToUTC unix -- | -- Get the current UTC timestamp with nanosecond granularity from the system clock. Any observed leap second will be spread out over the day to ensure nanosecond continuity at midnight. getCurrentUTCDateTimeNanos :: IO (UTCDateTimeNanos 'Gregorian) getCurrentUTCDateTimeNanos = do UnixDateTimeNanos unix nsec <- getCurrentUnixDateTimeNanos let date = UnixDate (fromIntegral (div base 86400)) base = baseUnixToUTC unix time = UTCDateTimeNanos base nsec leap = round (11574.074074074073 * realToFrac (mod unix 86400) :: Double) :: Nanos return $! case nextLeap of Just ((==) date -> True) -> time `plus` leap _ -> time -- | -- Parse a UTC datestamp. parseUTCDate :: TimeLocale -> Format -> Text -> Either String (UTCDate 'Gregorian) parseUTCDate locale format input = join $ build <$> runParser locale tzdata defaultParserState format input where tzdata = Just utcTZ build ParserState {..} = _ps_zone 0 *> do return $! createUTCDate _ps_year _ps_mon _ps_mday -- | -- Parse a UTC timestamp. parseUTCDateTime :: TimeLocale -> Format -> Text -> Either String (UTCDateTime 'Gregorian) parseUTCDateTime locale format input = join $ build <$> runParser locale tzdata defaultParserState format input where tzdata = Just utcTZ build ParserState {..} = _ps_zone 0 *> do return $! createUTCDateTime _ps_year _ps_mon _ps_mday hour _ps_min sec where hour = _ps_ampm _ps_hour sec = truncate _ps_sec -- | -- Parse a UTC timestamp with nanosecond granularity. parseUTCDateTimeNanos :: TimeLocale -> Format -> Text -> Either String (UTCDateTimeNanos 'Gregorian) parseUTCDateTimeNanos locale format input = join $ build <$> runParser locale tzdata defaultParserState format input where tzdata = Just utcTZ build ParserState {..} = _ps_zone 0 *> do return $! createUTCDateTimeNanos _ps_year _ps_mon _ps_mday hour _ps_min sec nsec where hour = _ps_ampm _ps_hour (,) sec nsec = properFracNanos $ _ps_frac _ps_sec -- | -- Convert an integer into a UTC timestamp with nanosecond granularity. toNano :: Integer -> UTCDateTimeNanos 'Gregorian toNano = uncurry UTCDateTimeNanos . (***) fromInteger fromInteger . flip divMod 1000000000 -- | -- Convert a UTC timestamp with nanosecond granularity into an integer. fromNano :: UTCDateTimeNanos 'Gregorian -> Integer fromNano (UTCDateTimeNanos base nsec) = toInteger base * 1000000000 + toInteger nsec -- | -- The next leap second insertion date. nextLeap :: Maybe (UnixDate 'Gregorian) nextLeap = Just (UnixDate 17166)