hodatime-0.2.1.1: src/Data/HodaTime/ZonedDateTime.hs
-----------------------------------------------------------------------------
-- |
-- Module : Data.HodaTime.Instant
-- Copyright : (C) 2017 Jason Johnson
-- License : BSD-style (see the file LICENSE)
-- Maintainer : Jason Johnson <jason.johnson.081@gmail.com>
-- Stability : experimental
-- Portability : POSIX, Windows
--
-- This is the module for 'ZonedDateTime'. A 'ZonedDateTime' is a universal time, it represents the same moment anywhere in the world and is completely
-- unambiguous. Each instance of a 'ZonedDateTime' corresponds to exactly one point on a continuous time line.
----------------------------------------------------------------------------
module Data.HodaTime.ZonedDateTime
(
-- * Types
ZonedDateTime
-- * Constructors
,fromCalendarDateTimeLeniently
,fromCalendarDateTimeStrictly
,fromInstant
-- * Math
-- * Conversion
,toCalendarDateTime
,toCalendarDate
,toLocalTime
-- * Accessors
,inDst
,zoneAbbreviation
-- * Special constructors
,fromCalendarDateTimeAll
,resolve
-- * Exceptions
,DateTimeDoesNotExistException(..)
,DateTimeAmbiguousException(..)
)
where
import Data.HodaTime.ZonedDateTime.Internal
import Data.HodaTime.CalendarDateTime.Internal (CalendarDateTime(..), CalendarDate(..), IsCalendarDateTime(..), IsCalendar(..), LocalTime)
import Data.HodaTime.LocalTime.Internal (second)
import Data.HodaTime.Instant.Internal (Instant)
import Data.HodaTime.Offset.Internal (Offset(..), adjustInstant)
import Data.HodaTime.TimeZone.Internal (TimeZone, TransitionInfo(..), calDateTransitionsFor, aroundCalDateTransition, activeTransitionFor)
import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow, throwM)
import Data.Typeable (Typeable)
-- exceptions
-- TODO: find a way to get the offending CalendarDateTime into the exception so that if this is thrown in deeply nested code users can figure out
-- TODO: which date caused it. The current problem is that "instance Exception" doesn't work if there is a type variable, even if the data type
-- TODO: itself is typeable
data DateTimeDoesNotExistException = DateTimeDoesNotExistException
deriving (Typeable, Show)
instance Exception DateTimeDoesNotExistException
data DateTimeAmbiguousException = DateTimeAmbiguousException
deriving (Typeable, Show)
instance Exception DateTimeAmbiguousException
-- constructors
-- | Returns the mapping of this 'CalendarDateTime' within the given 'TimeZone', with "lenient" rules applied such that ambiguous values map to the earlier of the alternatives,
-- and "skipped" values are shifted forward by the duration of the "gap".
fromCalendarDateTimeLeniently :: (IsCalendar cal, IsCalendarDateTime cal) => CalendarDateTime cal -> TimeZone -> ZonedDateTime cal
fromCalendarDateTimeLeniently = resolve ambiguous skipped
where
ambiguous zdt _ = zdt
skipped (ZonedDateTime _ _ (TransitionInfo (Offset bOff) _ _)) (ZonedDateTime cdt tz ti@(TransitionInfo (Offset aOff) _ _)) = ZonedDateTime cdt' tz ti
where
cdt' = modify (\s -> s + aOff - bOff) second cdt
modify f l = head . l ((:[]) . f) -- TODO: We may want to break down and define the 3 lens primitives we need somewhere
-- | Returns the mapping of this 'CalendarDateTime' within the given 'TimeZone', with "strict" rules applied such that ambiguous or skipped date times
-- return the requested failure response (e.g. Nothing, Left, exception, etc.)
fromCalendarDateTimeStrictly :: (MonadThrow m, IsCalendarDateTime cal) => CalendarDateTime cal -> TimeZone -> m (ZonedDateTime cal)
fromCalendarDateTimeStrictly cdt = go . fromCalendarDateTimeAll cdt
where
go [] = throwM $ DateTimeDoesNotExistException
go [zdt] = return zdt
go _ = throwM $ DateTimeAmbiguousException
-- | Return all 'ZonedDateTime' entries for a specific 'CalendarDateTime' in a 'TimeZone'. Normally this would be one, but in the case that a time
-- occurs twice in a zone (i.e. due to daylight savings time change) both would be returned. Also, if the time does not occur at all, an empty list
-- will be returned. This method allows the user to choose exactly what to do in the case of ambigiuty.
fromCalendarDateTimeAll :: IsCalendarDateTime cal => CalendarDateTime cal -> TimeZone -> [ZonedDateTime cal]
fromCalendarDateTimeAll cdt tz = zdts
where
instant = toUnadjustedInstant cdt
zdts = fmap mkZdt . calDateTransitionsFor instant $ tz
mkZdt = ZonedDateTime cdt tz
-- | Returns the 'ZonedDateTime' represented by the passed 'Instant' within the given 'TimeZone'. This is always an unambiguous conversion.
fromInstant :: IsCalendarDateTime cal => Instant -> TimeZone -> ZonedDateTime cal
fromInstant instant tz = ZonedDateTime cdt tz ti
where
ti = activeTransitionFor instant tz
offset = tiUtcOffset ti
instant' = adjustInstant offset instant
cdt = fromAdjustedInstant instant'
-- | Takes two functions to determine how to resolve a 'CalendarDateTime' to a 'ZonedDateTime' in the case of ambiguity or skipped times. The first
-- function is for the ambigous case and is past the first matching 'ZonedDateTime', followed by the second match. The second function is for the case
-- that the 'CalendarDateTime' doesn't exist in the 'TimeZone' (e.g. in a spring-forward situation, there will be a missing hour), the first
-- 'ZonedDateTime' will be the the last time before the gap and the second will be the first time after the gap.
resolve ::
IsCalendarDateTime cal =>
(ZonedDateTime cal -> ZonedDateTime cal -> ZonedDateTime cal) ->
(ZonedDateTime cal -> ZonedDateTime cal -> ZonedDateTime cal) ->
CalendarDateTime cal ->
TimeZone ->
ZonedDateTime cal -- TODO: This function should probably allow failure
resolve ambiguous skipped cdt tz = go . fmap mkZdt . calDateTransitionsFor instant $ tz
where
instant = toUnadjustedInstant cdt
(before, after) = aroundCalDateTransition instant $ tz
mkZdt = ZonedDateTime cdt tz
go [] = skipped (mkZdt before) (mkZdt after)
go [zdt] = zdt
go (zdt1:zdt2:[]) = ambiguous zdt1 zdt2
go _ = error "misconfiguration: more than 2 dates returns from calDateTransitionsFor"
-- conversion
-- | Return the 'CalendarDateTime' represented by this 'ZonedDateTime'.
toCalendarDateTime :: ZonedDateTime cal -> CalendarDateTime cal
toCalendarDateTime (ZonedDateTime cdt _ _) = cdt
-- | Return the 'CalendarDate' represented by this 'ZonedDateTime'.
toCalendarDate :: ZonedDateTime cal -> CalendarDate cal
toCalendarDate (ZonedDateTime (CalendarDateTime cd _) _ _) = cd
-- | Return the 'LocalTime' represented by this 'ZonedDateTime'.
toLocalTime :: ZonedDateTime cal -> LocalTime
toLocalTime (ZonedDateTime (CalendarDateTime _ lt) _ _) = lt
-- Accessors
-- | Return a 'Bool' specifying if this 'ZonedDateTime' is currently in Daylight savings time.
inDst :: ZonedDateTime cal -> Bool
inDst (ZonedDateTime _ _ (TransitionInfo _ isInDst _)) = isInDst
-- | Return a 'String' representing the abbreviation for the TimeZone this 'ZonedDateTime' is currently in.
zoneAbbreviation :: ZonedDateTime cal -> String
zoneAbbreviation (ZonedDateTime _ _ (TransitionInfo _ _ abbr)) = abbr