----------------------------------------------------------------------------- -- | -- Module : Data.HodaTime.Instant -- Copyright : (C) 2017 Jason Johnson -- License : BSD-style (see the file LICENSE) -- Maintainer : Jason Johnson -- 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