http-date-0.0.13: Network/HTTP/Date/Converter.hs
module Network.HTTP.Date.Converter (
epochTimeToHTTPDate,
httpDateToUTC,
httpDateToUTCValid,
utcToHTTPDate,
) where
import Data.Time
import Data.Time.Calendar.WeekDate
import Foreign.C.Types (CTime (..))
import Network.HTTP.Date.Types
import System.Posix.Types
-- |
-- Translating 'EpochTime' to 'HTTPDate'.
--
-- >>> epochTimeToHTTPDate 784887151
-- HTTPDate {hdYear = 1994, hdMonth = 11, hdDay = 15, hdHour = 8, hdMinute = 12, hdSecond = 31, hdWkday = 2}
epochTimeToHTTPDate :: EpochTime -> HTTPDate
epochTimeToHTTPDate (CTime i) =
defaultHTTPDate
{ hdYear = y
, hdMonth = m
, hdDay = d
, hdHour = h
, hdMinute = n
, hdSecond = s
, hdWkday = w
}
where
(days, secs) = fromIntegral i `divMod` 86400
-- 1970/1/1 is Thu (4)
w = (days + 3) `mod` 7 + 1
(y, m, d) = civilFromDays days
(hhmm, s) = secs `quotRem` 60
(h, n) = hhmm `quotRem` 60
-- Howard Hinnant's civil_from_days: days since 1970-01-01 to (y, m, d)
-- in constant time, for any sign. See:
-- http://howardhinnant.github.io/date_algorithms.html#civil_from_days
civilFromDays :: Int -> (Int, Int, Int)
civilFromDays z0 = (y + if m <= 2 then 1 else 0, m, d)
where
z = z0 + 719468
era = z `div` 146097
doe = z - era * 146097
yoe = (doe - doe `quot` 1460 + doe `quot` 36524 - doe `quot` 146096) `quot` 365
y = yoe + era * 400
doy = doe - (365 * yoe + yoe `quot` 4 - yoe `quot` 100)
mp = (5 * doy + 2) `quot` 153
d = doy - (153 * mp + 2) `quot` 5 + 1
m = if mp < 10 then mp + 3 else mp - 9
-- | Translating 'HTTPDate' to 'UTCTime'.
--
-- Out-of-range fields are clipped to the nearest valid value, as
-- 'fromGregorian' does. 'hdWkday' is ignored. A second of 60 is kept
-- as a leap second only at 23:59. Use 'httpDateToUTCValid' to reject
-- invalid values instead.
--
-- >>> httpDateToUTC defaultHTTPDate {hdMonth = 2, hdDay = 31, hdHour = 25}
-- 1970-02-28 23:00:00 UTC
--
-- Since 0.0.7.
httpDateToUTC :: HTTPDate -> UTCTime
httpDateToUTC x = UTCTime (fromGregorian y m d) (secondsToDiffTime s)
where
y = fromIntegral $ hdYear x
m = hdMonth x
d = hdDay x
h = clip 0 23 $ hdHour x
n = clip 0 59 $ hdMinute x
s0 = clip 0 60 $ hdSecond x
s0'
| s0 == 60 && (h /= 23 || n /= 59) = 59
| otherwise = s0
s = fromIntegral $ h * 3600 + n * 60 + s0'
clip lo hi v = max lo $ min hi v
-- | Translating 'HTTPDate' to 'UTCTime' if all fields except
-- 'hdWkday' are valid. A second of 60 is accepted only at 23:59.
--
-- >>> httpDateToUTCValid defaultHTTPDate {hdMonth = 2, hdDay = 31}
-- Nothing
--
-- Since 0.0.13.
httpDateToUTCValid :: HTTPDate -> Maybe UTCTime
httpDateToUTCValid x
| validTime (hdHour x) (hdMinute x) (hdSecond x) = do
day <- fromGregorianValid (fromIntegral $ hdYear x) (hdMonth x) (hdDay x)
return $ UTCTime day $ secondsToDiffTime s
| otherwise = Nothing
where
s = fromIntegral $ hdHour x * 3600 + hdMinute x * 60 + hdSecond x
validTime :: Int -> Int -> Int -> Bool
validTime h n s =
0 <= h
&& h <= 23
&& 0 <= n
&& n <= 59
&& 0 <= s
&& (s <= 59 || s == 60 && h == 23 && n == 59)
-- | Translating 'UTCTime' to 'HTTPDate'.
--
-- Since 0.0.7.
utcToHTTPDate :: UTCTime -> HTTPDate
utcToHTTPDate x =
defaultHTTPDate
{ hdYear = fromIntegral y
, hdMonth = m
, hdDay = d
, hdHour = h
, hdMinute = n
, hdSecond = truncate s
, hdWkday = w
}
where
(y, m, d) = toGregorian day
(h, n, s) = (todHour tod, todMin tod, todSec tod)
(_, _, w) = toWeekDate day
day = localDay time
tod = localTimeOfDay time
time = utcToLocalTime utc x