-- | -- Module : Data.Time.Exts.Unix -- Copyright : 2013-2017 Enzo Haussecker -- License : BSD3 -- Maintainer : Enzo Haussecker -- Stability : Stable -- -- A native implementation of Unix Time. {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS -fno-warn-name-shadowing #-} module Data.Time.Exts.Unix ( -- * Timestamps UnixDate(..) , UnixDateTime(..) , UnixDateTimeNanos(..) -- * Create , createUnixDate , createUnixDateTime , createUnixDateTimeNanos -- * Get , getCurrentUnixDate , getCurrentUnixDateTime , getCurrentUnixDateTimeNanos -- * Parse , parseUnixDate , parseUnixDateTime , parseUnixDateTimeNanos ) where import Control.Arrow ((***), first) import Control.DeepSeq (NFData) import Data.Data (Data, Typeable) import Data.Int (Int32, Int64) import Data.Text (Text) import Foreign.C.Time (C'timeval(..), getTimeOfDay) import Foreign.C.Types (CLong(..)) 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.Util -- | -- Days since Unix epoch. newtype UnixDate (cal :: Calendar) = UnixDate Int32 deriving (Data, Eq, Generic, NFData, Ord, Storable, Typeable) -- | -- Seconds since Unix epoch (excluding leap seconds). newtype UnixDateTime (cal :: Calendar) = UnixDateTime Int64 deriving (Data, Eq, Generic, NFData, Ord, Storable, Typeable) -- | -- Nanoseconds since Unix epoch (excluding leap seconds). data UnixDateTimeNanos (cal :: Calendar) = UnixDateTimeNanos {-# UNPACK #-} !Int64 {-# UNPACK #-} !Int32 deriving (Data, Eq, Generic, Ord, Typeable) instance Bounded (UnixDate 'Gregorian) where -- Thu Jan 01 1970. minBound = UnixDate 0 -- Fri Dec 31 9999. maxBound = UnixDate 2932896 instance Bounded (UnixDateTime 'Gregorian) where -- 12:00:00 AM Thu Jan 01 1970. minBound = UnixDateTime 0 -- 11:59:59 PM Fri Dec 31 9999. maxBound = UnixDateTime 253402300799 instance Bounded (UnixDateTimeNanos 'Gregorian) where -- 12:00:00.000000000 AM Thu Jan 01 1970. minBound = UnixDateTimeNanos 0 0 -- 11:59:59.999999999 PM Fri Dec 31 9999. maxBound = UnixDateTimeNanos 253402300799 999999999 instance Enum (UnixDate 'Gregorian) where -- Next day. succ = flip plus (Day 1) -- Previous day. pred = flip plus (- Day 1) -- Denumerate a Unix datestamp. fromEnum (UnixDate base) = fromIntegral base -- Enumerate a Unix datestamp. toEnum base = if minBound <= date && date <= maxBound then date else error "toEnum{UnixDate 'Gregorian}: out of bounds" where date = UnixDate $ fromIntegral base instance Enum (UnixDateTime 'Gregorian) where -- Next second. succ = flip plus (Second 1) -- Previous second. pred = flip plus (- Second 1) -- Denumerate a Unix timestamp. fromEnum (UnixDateTime base) = fromIntegral base -- Enumerate a Unix timestamp. toEnum base = if minBound <= time && time <= maxBound then time else error "toEnum{UnixDateTime 'Gregorian}: out of bounds" where time = UnixDateTime $ fromIntegral base instance Human (UnixDate 'Gregorian) where -- Define the Gregorian components of a Unix datestamp. type Components (UnixDate 'Gregorian) = DateStruct 'Gregorian -- Pack a Unix datestamp from Gregorian components. pack DateStruct {..} = createUnixDate _d_year _d_mon _d_mday -- Unpack a Unix datestamp to Gregorian components. unpack (UnixDate base) = rec 1970 (Day base) where rec !year !day = if day >= size then rec (year + 1) (day - size) else DateStruct year mon mday wday where wday = toEnum (1 + mod (fromIntegral base + 4) 7) leap = isLeapYear year size = if leap then 366 else 365 (mon, mday) = if leap then if day >= 182 then if day >= 274 then if day >= 335 then (December, day - 334) else if day >= 305 then (November, day - 304) else (October, day - 273) else if day >= 244 then (September, day - 243) else if day >= 213 then (August, day - 212) else (July, day - 181) else if day >= 091 then if day >= 152 then (June, day - 151) else if day >= 121 then (May, day - 120) else (April, day - 090) else if day >= 060 then (March, day - 059) else if day >= 031 then (February, day - 030) else (January, day + 001) else if day >= 181 then if day >= 273 then if day >= 334 then (December, day - 333) else if day >= 304 then (November, day - 303) else (October, day - 272) else if day >= 243 then (September, day - 242) else if day >= 212 then (August, day - 211) else (July, day - 180) else if day >= 090 then if day >= 151 then (June, day - 150) else if day >= 120 then (May, day - 119) else (April, day - 089) else if day >= 059 then (March, day - 058) else if day >= 031 then (February, day - 030) else (January, day + 001) instance Human (UnixDateTime 'Gregorian) where -- Define the Gregorian components of a Unix timestamp. type Components (UnixDateTime 'Gregorian) = DateTimeStruct 'Gregorian -- Pack a Unix timestamp from Gregorian components. pack DateTimeStruct {..} = createUnixDateTime _dt_year _dt_mon _dt_mday _dt_hour _dt_min sec where sec = round _dt_sec -- Unpack a Unix timestamp to Gregorian components. unpack (UnixDateTime base) = DateTimeStruct _d_year _d_mon _d_mday _d_wday hour min sec where DateStruct {..} = unpack (UnixDate day :: UnixDate 'Gregorian) (day, hms) = fromIntegral *** fromIntegral $ divMod base 86400 (hour, ms) = fromIntegral <$> divMod hms 3600 (min, sec) = realToFrac <$> divMod ms 0060 instance Human (UnixDateTimeNanos 'Gregorian) where -- Define the Gregorian components of a Unix timestamp with nanosecond granularity. type Components (UnixDateTimeNanos 'Gregorian) = DateTimeStruct 'Gregorian -- Pack a Unix timestamp with nanosecond granularity from Gregorian components. pack DateTimeStruct {..} = createUnixDateTimeNanos _dt_year _dt_mon _dt_mday _dt_hour _dt_min sec nsec where (sec, nsec) = properFracNanos _dt_sec -- Unpack a Unix timestamp with nanosecond granularity to Gregorian components. unpack (UnixDateTimeNanos base nsec) = over dt_sec (+ frac) $ unpack time where time = UnixDateTime base :: UnixDateTime 'Gregorian frac = realToFrac nsec / 1000000000 instance Math (UnixDate 'Gregorian) Day where -- Compute the day duration between two Unix datestamps. duration (UnixDate old) (UnixDate new) = fromIntegral (new - old) -- Add days to a Unix datestamp. plus (UnixDate base) days = if minBound <= date && date <= maxBound then date else error "plus{UnixDate 'Gregorian, Day}: out of bounds" where date = UnixDate (base + fromIntegral days) instance Math (UnixDateTime 'Gregorian) Day where -- Compute the day duration between two Unix timestamps. duration (UnixDateTime old) (UnixDateTime new) = fromIntegral (div (new - old) 86400) -- Add days to a Unix timestamp. plus (UnixDateTime base) days = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTime 'Gregorian, Day}: out of bounds" where time = UnixDateTime (base + fromIntegral days * 86400) instance Math (UnixDateTime 'Gregorian) Hour where -- Compute the hour duration between two Unix timestamps. duration (UnixDateTime old) (UnixDateTime new) = fromIntegral (div (new - old) 3600) -- Add hours to a Unix timestamp. plus (UnixDateTime base) hours = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTime 'Gregorian, Hour}: out of bounds" where time = UnixDateTime (base + fromIntegral hours * 3600) instance Math (UnixDateTime 'Gregorian) Minute where -- Compute the minute duration between two Unix timestamps. duration (UnixDateTime old) (UnixDateTime new) = fromIntegral (div (new - old) 60) -- Add minutes to a Unix timestamp. plus (UnixDateTime base) minutes = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTime 'Gregorian, Minute}: out of bounds" where time = UnixDateTime (base + fromIntegral minutes * 60) instance Math (UnixDateTime 'Gregorian) Second where -- Compute the second duration between two Unix timestamps. duration (UnixDateTime old) (UnixDateTime new) = fromIntegral (new - old) -- Add seconds to a Unix timestamp. plus (UnixDateTime base) seconds = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTime 'Gregorian, Second}: out of bounds" where time = UnixDateTime (base + fromIntegral seconds) instance Math (UnixDateTimeNanos 'Gregorian) Day where -- Compute the day duration between two Unix timestamps with nanosecond granularity. duration (UnixDateTimeNanos old _) (UnixDateTimeNanos new _) = fromIntegral (div (new - old) 86400) -- Add days to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) days = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Day}: out of bounds" where time = UnixDateTimeNanos (base + fromIntegral days * 86400) nsec instance Math (UnixDateTimeNanos 'Gregorian) Hour where -- Compute the hour duration between two Unix timestamps with nanosecond granularity. duration (UnixDateTimeNanos old _) (UnixDateTimeNanos new _) = fromIntegral (div (new - old) 3600) -- Add hours to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) hours = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Hour}: out of bounds" where time = UnixDateTimeNanos (base + fromIntegral hours * 3600) nsec instance Math (UnixDateTimeNanos 'Gregorian) Minute where -- Compute the minute duration between two Unix timestamps with nanosecond granularity. duration (UnixDateTimeNanos old _) (UnixDateTimeNanos new _) = fromIntegral (div (new - old) 60) -- Add minutes to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) minutes = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Minute}: out of bounds" where time = UnixDateTimeNanos (base + fromIntegral minutes * 60) nsec instance Math (UnixDateTimeNanos 'Gregorian) Second where -- Compute the second duration between two Unix timestamps with nanosecond granularity. duration (UnixDateTimeNanos old _) (UnixDateTimeNanos new _) = fromIntegral (new - old) -- Add seconds to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) seconds = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Second}: out of bounds" where time = UnixDateTimeNanos (base + fromIntegral seconds) nsec instance Math (UnixDateTimeNanos 'Gregorian) Millis where -- Compute the millisecond duration between two Unix timestamps with nanosecond granularity. duration old new = fold new - fold old where fold (UnixDateTimeNanos base nsec) = fromIntegral base * 1000 + fromIntegral (div nsec 1000000) -- Add milliseconds to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) millis = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Millis}: out of bounds" where nsum = fromIntegral nsec + fromIntegral millis * 1000000 time = uncurry UnixDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) instance Math (UnixDateTimeNanos 'Gregorian) Micros where -- Compute the microsecond duration between two Unix timestamps with nanosecond granularity. duration old new = fold new - fold old where fold (UnixDateTimeNanos base nsec) = fromIntegral base * 1000000 + fromIntegral (div nsec 1000) -- Add microseconds to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) micros = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Micros}: out of bounds" where nsum = fromIntegral nsec + fromIntegral micros * 1000 time = uncurry UnixDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) instance Math (UnixDateTimeNanos 'Gregorian) Nanos where -- Compute the nanosecond duration between two Unix timestamps with nanosecond granularity. duration old new = if toInteger (minBound :: Int64) <= res && toInteger (maxBound :: Int64) >= res then fromInteger res else error "duration{UnixDateTimeNanos 'Gregorian, Nanos}: integer overflow" where res = fold new - fold old fold (UnixDateTimeNanos base nsec) = toInteger base * 1000000000 + toInteger nsec -- Add nanoseconds to a Unix timestamp with nanosecond granularity. plus (UnixDateTimeNanos base nsec) nanos = if minBound <= time && time <= maxBound then time else error "plus{UnixDateTimeNanos 'Gregorian, Nanos}: out of bounds" where nsum = fromIntegral nsec + fromIntegral nanos time = uncurry UnixDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) instance NFData (UnixDateTimeNanos cal) instance Random (UnixDate 'Gregorian) where -- Generate a random Unix datestamp. random = first toEnum . randomR (fromEnum a, fromEnum b) where a = minBound :: UnixDate 'Gregorian b = maxBound :: UnixDate 'Gregorian -- Generate a random Unix datestamp uniformly distributed on the closed interval. randomR (a, b) = first toEnum . randomR (fromEnum a, fromEnum b) instance Random (UnixDateTime 'Gregorian) where -- Generate a random Unix timestamp. random = first toEnum . randomR (fromEnum a, fromEnum b) where a = minBound :: UnixDateTime 'Gregorian b = maxBound :: UnixDateTime 'Gregorian -- Generate a random Unix timestamp uniformly distributed on the closed interval. randomR (a, b) = first toEnum . randomR (fromEnum a, fromEnum b) instance Random (UnixDateTimeNanos 'Gregorian) where -- Generate a random Unix timestamp with nanosecond granularity. random = first toNano . randomR (fromNano a, fromNano b) where a = minBound :: UnixDateTimeNanos 'Gregorian b = maxBound :: UnixDateTimeNanos 'Gregorian -- Generate a random Unix timestamp with nanosecond granularity uniformly distributed on the closed interval. randomR (a, b) = first toNano . randomR (fromNano a, fromNano b) instance Show (UnixDate 'Gregorian) where -- Show a Unix datestamp. show (unpack -> DateStruct {..}) = printf "%.3s %.3s %02d %4d" (show _d_wday) (show _d_mon) _d_mday _d_year instance Show (UnixDateTime 'Gregorian) where -- Show a Unix timestamp. show (unpack -> DateTimeStruct {..}) = printf "%02d:%02d:%02d %s %.3s %.3s %02d %4d" 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 (UnixDateTimeNanos 'Gregorian) where -- Show a Unix timestamp with nanosecond granularity. show (unpack -> DateTimeStruct {..}) = printf "%02d:%02d:%02d.%09d %s %.3s %.3s %02d %4d" 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 (UnixDateTimeNanos cal) where -- Size of Unix timestamp with nanosecond granularity. sizeOf = const 12 -- Alignment of Unix timestamp with nanosecond granularity. alignment = sizeOf -- Read a Unix 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 $! UnixDateTimeNanos base nsec -- Write a Unix timestamp with nanosecond granularity to memory. pokeElemOff ptr n (UnixDateTimeNanos base nsec) = do let off = 12 * n poke (plusPtr ptr (off + 0)) base poke (plusPtr ptr (off + 8)) nsec -- | -- Create a Unix datestamp. createUnixDate :: Year -> Month 'Gregorian -> Day -> UnixDate 'Gregorian createUnixDate year mon mday = if (minBound :: UnixDate 'Gregorian) <= date && date <= (maxBound :: UnixDate 'Gregorian) then date else error "createUnixDate: out of bounds" where Day base = unsafeEpochToDate year mon mday date = UnixDate base -- | -- Create a Unix timestamp. createUnixDateTime :: Year -> Month 'Gregorian -> Day -> Hour -> Minute -> Second -> UnixDateTime 'Gregorian createUnixDateTime year mon mday hour min sec = if (minBound :: UnixDateTime 'Gregorian) <= time && time <= (maxBound :: UnixDateTime 'Gregorian) then time else error "createUnixDateTime: out of bounds" where Second base = unsafeEpochToDateTime year mon mday hour min sec time = UnixDateTime base -- | -- Create a Unix timestamp with nanosecond granularity. createUnixDateTimeNanos :: Year -> Month 'Gregorian -> Day -> Hour -> Minute -> Second -> Nanos -> UnixDateTimeNanos 'Gregorian createUnixDateTimeNanos year mon mday hour min sec nanos = if (minBound :: UnixDateTimeNanos 'Gregorian) <= time && time <= (maxBound :: UnixDateTimeNanos 'Gregorian) then time else error "createUnixDateTimeNanos: out of bounds" where Second base = unsafeEpochToDateTime year mon mday hour min sec nsum = fromIntegral nanos time = uncurry UnixDateTimeNanos ((***) (+ base) fromIntegral (divMod nsum 1000000000)) -- | -- Get the current Unix datestamp from the system clock. getCurrentUnixDate :: IO (UnixDate 'Gregorian) getCurrentUnixDate = getTimeOfDay >>= \ (C'timeval (CLong base) _) -> return $! UnixDate (fromIntegral (div base 86400)) -- | -- Get the current Unix timestamp from the system clock. getCurrentUnixDateTime :: IO (UnixDateTime 'Gregorian) getCurrentUnixDateTime = getTimeOfDay >>= \ (C'timeval (CLong base) _) -> return $! UnixDateTime base -- | -- Get the current Unix timestamp with nanosecond granularity from the system clock. getCurrentUnixDateTimeNanos :: IO (UnixDateTimeNanos 'Gregorian) getCurrentUnixDateTimeNanos = getTimeOfDay >>= \ (C'timeval (CLong base) (CLong usec)) -> return $! UnixDateTimeNanos base (fromIntegral usec * 1000) -- | -- Parse a Unix datestamp. parseUnixDate :: TimeLocale -> Format -> Text -> Either String (UnixDate 'Gregorian) parseUnixDate locale format input = build <$> runParser locale Nothing defaultParserState format input where build ParserState {..} = createUnixDate _ps_year _ps_mon _ps_mday -- | -- Parse a Unix timestamp. parseUnixDateTime :: TimeLocale -> Format -> Text -> Either String (UnixDateTime 'Gregorian) parseUnixDateTime locale format input = build <$> runParser locale Nothing defaultParserState format input where build ParserState {..} = createUnixDateTime _ps_year _ps_mon _ps_mday hour _ps_min sec where hour = _ps_ampm _ps_hour sec = truncate _ps_sec -- | -- Parse a Unix timestamp with nanosecond granularity. parseUnixDateTimeNanos :: TimeLocale -> Format -> Text -> Either String (UnixDateTimeNanos 'Gregorian) parseUnixDateTimeNanos locale format input = build <$> runParser locale Nothing defaultParserState format input where build ParserState {..} = createUnixDateTimeNanos _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 Unix timestamp with nanosecond granularity. toNano :: Integer -> UnixDateTimeNanos 'Gregorian toNano = uncurry UnixDateTimeNanos . (***) fromInteger fromInteger . flip divMod 1000000000 -- | -- Convert a Unix timestamp with nanosecond granularity into an integer. fromNano :: UnixDateTimeNanos 'Gregorian -> Integer fromNano (UnixDateTimeNanos base nsec) = toInteger base * 1000000000 + toInteger nsec -- | -- Check if the given year is a leap year. isLeapYear :: Year -> Bool isLeapYear year = mod year 400 == 0 || (mod year 100 /= 0 && mod year 4 == 0) -- | -- Calculate the number of days that have elapsed between Unix epoch and the given year without performing any bounds check. unsafeEpochToYear :: Year -> Day unsafeEpochToYear Year {..} = Day (365 * (getYear - 1970) + div (getYear - 1969) 004 - div (getYear - 1901) 100 + div (getYear - 1601) 400) -- | -- Calculate the number of days that have elapsed between Unix epoch and the given Unix datestamp without performing any bounds check. unsafeEpochToDate :: Year -> Month 'Gregorian -> Day -> Day unsafeEpochToDate year mon mday = unsafeEpochToYear year + yearToMonth leap mon + mday - 1 where leap = isLeapYear year -- | -- Calculate the number of seconds that have elapsed between Unix epoch and the given Unix timestamp without performing any bounds check. unsafeEpochToDateTime :: Year -> Month 'Gregorian -> Day -> Hour -> Minute -> Second -> Second unsafeEpochToDateTime year mon mday hour min sec = fromIntegral day * 86400 + fromIntegral hour * 3600 + fromIntegral min * 60 + sec where day = unsafeEpochToDate year mon mday -- | -- Calculate the number of days that have elapsed between January 1st and the given month. yearToMonth :: Bool -> Month 'Gregorian -> Day yearToMonth leap = if leap then \ case January -> 000 February -> 031 March -> 060 April -> 091 May -> 121 June -> 152 July -> 182 August -> 213 September -> 244 October -> 274 November -> 305 December -> 335 else \ case January -> 000 February -> 031 March -> 059 April -> 090 May -> 120 June -> 151 July -> 181 August -> 212 September -> 243 October -> 273 November -> 304 December -> 334