packages feed

hodatime-1.1.0.0: src/Data/HodaTime/ZonedDateTime/Internal.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.HodaTime.ZonedDateTime.Internal
(
   ZonedDateTime(..)
  ,fromInstant
  ,year
  ,month
  ,day
  ,hour
  ,minute
  ,second
  ,nanosecond
)
where

import Data.HodaTime.CalendarDateTime.Internal (CalendarDateTime, IsCalendarDateTime, IsCalendar, fromAdjustedInstant, toUnadjustedInstant)
import qualified Data.HodaTime.CalendarDateTime.Internal as CDT
import qualified Data.HodaTime.LocalTime.Internal as LT
import Data.HodaTime.TimeZone.Internal (TimeZone, TZIdentifier(..), TransitionInfo, activeTransitionFor, tiUtcOffset, zoneName)
import Data.HodaTime.Offset.Internal (Offset(..), adjustInstant)
import Data.HodaTime.Instant.Internal (Instant)
import Data.Hashable (Hashable(..))

-- | A CalendarDateTime in a specific time zone. A 'ZonedDateTime' is global and maps directly to a single 'Instant'.
data ZonedDateTime cal = ZonedDateTime { zdtCalendarDateTime :: CalendarDateTime cal, zdtTimeZone :: TimeZone, zdtActiveTransition :: TransitionInfo }

deriving instance Eq (CDT.Date cal) => Eq (ZonedDateTime cal)

-- | Renders a 'ZonedDateTime' as the 'fromInstant' call that reconstructs it from its physical 'Instant' and zone.
instance IsCalendarDateTime cal => Show (ZonedDateTime cal) where
  showsPrec p (ZonedDateTime cdt tz ti) = showParen (p > 10) $
      showString "fromInstant " . showsPrec 11 inst . showChar ' ' . showsPrec 11 tz
    where
      inst = adjustInstant (negateOffset (tiUtcOffset ti)) (toUnadjustedInstant cdt)
      negateOffset (Offset s) = Offset (negate s)

-- | Orders 'ZonedDateTime's by the 'Instant' they represent (their global\/UTC position on the time line), falling
--   back to the zone identifier as a tie-break so that two zones observing the same instant still have a total order.
--   NOTE: this compares by physical time, not by the local wall-clock 'CalendarDateTime'.
instance (IsCalendarDateTime cal, Eq (CDT.Date cal)) => Ord (ZonedDateTime cal) where
  compare a b = compare (instantOf a) (instantOf b) <> compare (zid a) (zid b)
    where
      instantOf (ZonedDateTime cdt _ ti) = adjustInstant (negateOffset (tiUtcOffset ti)) (toUnadjustedInstant cdt)
      negateOffset (Offset s) = Offset (negate s)
      zid (ZonedDateTime _ tz _) = case zoneName tz of
        UTC    -> "UTC"
        Zone n -> n

-- | Hashes a 'ZonedDateTime' by its identity: the local 'CalendarDateTime', the zone (by identifier) and the active
--   transition.  Consistent with '(==)', which compares those same components.
-- NOTE: no 'NFData' instance is provided because 'ZonedDateTime' embeds a 'TimeZone', whose fingertree-based
-- transition maps cannot be forced (see 'Data.HodaTime.TimeZone.Internal').
instance Hashable (CDT.Date cal) => Hashable (ZonedDateTime cal) where
  hashWithSalt s (ZonedDateTime cdt tz ti) = s `hashWithSalt` cdt `hashWithSalt` tz `hashWithSalt` ti


-- | 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'

-- | Accessor for the Year of a 'ZonedDateTime'.
year :: IsCalendar cal => ZonedDateTime cal -> CDT.Year
year (ZonedDateTime cdt _ _) = CDT.year cdt

-- | Accessor for the Month of a 'ZonedDateTime'.
month :: IsCalendar cal => ZonedDateTime cal -> CDT.Month cal
month (ZonedDateTime cdt _ _) = CDT.month cdt

-- | Accessor for the Day of a 'ZonedDateTime'.
day :: IsCalendar cal => ZonedDateTime cal -> CDT.DayOfMonth
day (ZonedDateTime cdt _ _) = CDT.day cdt

-- | Accessor for the Hour of a 'ZonedDateTime'.
hour :: IsCalendar cal => ZonedDateTime cal -> LT.Hour
hour (ZonedDateTime cdt _ _) = LT.hour cdt

-- | Accessor for the Minute of a 'ZonedDateTime'.
minute :: IsCalendar cal => ZonedDateTime cal -> LT.Minute
minute (ZonedDateTime cdt _ _) = LT.minute cdt

-- | Accessor for the Second of a 'ZonedDateTime'.
second :: IsCalendar cal => ZonedDateTime cal -> LT.Second
second (ZonedDateTime cdt _ _) = LT.second cdt

-- | Accessor for the Nanosecond of a 'ZonedDateTime'.
nanosecond :: IsCalendar cal => ZonedDateTime cal -> LT.Nanosecond
nanosecond (ZonedDateTime cdt _ _) = LT.nanosecond cdt

-- helper functions

-- TODO: We need functions that help construct this type.  Some of those functions probably need to be in OffsetDateTime so we can hide details of
-- TODO: CalendarDateTime from this module.  What we're trying to do is make sure the OffsetDateTime has the time set to the local time zone
-- TODO: and that the offset part tells us how far we are from UTC.  Nanos tell us how far we are into the current day