packages feed

hodatime-0.2.1.1: platform/windows/Data/HodaTime/TimeZone/Platform.hs

{-# LANGUAGE ForeignFunctionInterface #-}
module Data.HodaTime.TimeZone.Platform
(
   loadUTC
  ,loadLocalZone
  ,loadTimeZone
)
where

import Data.HodaTime.TimeZone.Internal
import Data.HodaTime.Instant.Internal (Instant(..), bigBang, minus)
import Data.HodaTime.Offset.Internal (Offset(..))
import Data.HodaTime.Duration.Internal (fromNanoseconds)
import Data.HodaTime.Calendar.Gregorian.Internal (yearMonthDayToDays)

import Data.Char (isDigit)
import Data.List (sortOn, foldl')
import Control.Monad (forM)
import Control.Exception (bracket)
import System.Win32.Types (LONG, HKEY)
import System.Win32.Registry
import System.Win32.Time (SYSTEMTIME(..))
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Storable (sizeOf, Storable(..))
import Foreign.Ptr (castPtr)

data REG_TZI_FORMAT = REG_TZI_FORMAT
  {
     _tziBias :: LONG
    ,_tziStandardBias :: LONG
    ,_tziDaylightBias :: LONG
    ,_tziStandardDate :: SYSTEMTIME
    ,_tziDaylightDate :: SYSTEMTIME
  }
  deriving (Show,Eq,Ord)

instance Storable REG_TZI_FORMAT where
  sizeOf _ = sizeOf (undefined :: LONG) * 3 + sizeOf (undefined :: SYSTEMTIME) * 2
  alignment _ = 4

  poke _ _ = error "poke not implemented"

  peek buf = REG_TZI_FORMAT
    <$> peekByteOff buf 0
    <*> peekByteOff buf 4
    <*> peekByteOff buf 8
    <*> peekByteOff buf 12
    <*> peekByteOff buf (12 + sizeOf (undefined :: SYSTEMTIME))

loadUTC :: IO (UtcTransitionsMap, CalDateTransitionsMap)
loadUTC = loadTimeZone "UTC"

loadLocalZone :: IO (UtcTransitionsMap, CalDateTransitionsMap, String)
loadLocalZone = do
  zone <- readLocalZoneName
  (utcM, calDateM) <- loadTimeZone zone
  return (utcM, calDateM, zone)

loadTimeZone :: String -> IO (UtcTransitionsMap, CalDateTransitionsMap)
loadTimeZone "UTC" = return (utcM, calDateM)
  where
    (utcM, calDateM, _) = fixedOffsetZone "UTC" (Offset 0)
loadTimeZone zone = do
  (stdAbbr, dstAbbr, tzi) <- readTziForZone zone
  dynTzis <- readDynamicDstForZone zone
  return $ mkZoneMaps stdAbbr dstAbbr tzi dynTzis

-- conversion from Windows types

mkZoneMaps :: String -> String -> REG_TZI_FORMAT -> [(Int, REG_TZI_FORMAT)] -> (UtcTransitionsMap, CalDateTransitionsMap)
mkZoneMaps stdAbbr dstAbbr defaultTzi dynTzis = (utcMap, calDateMap')
  where
    dynTzis' = sortOn fst dynTzis
    getInitialExpr [] = tziToExprInfo defaultTzi
    getInitialExpr ((_, tzi):_) = tziToExprInfo tzi
    tl [] = []
    tl (_:xs) = xs
    initialExpr = getInitialExpr dynTzis'
    initialUtcM = addUtcTransitionExpression bigBang initialExpr emptyUtcTransitions
    calDateMap' = addCalDateTransitionExpression lastEntry Largest lastExpr calDateMap
    (utcMap, calDateMap, lastEntry, lastExpr) = foldl' go (initialUtcM, emptyCalDateTransitions, Smallest, initialExpr) $ tl dynTzis'
    go (utcM, calDateM, prevEntry, prevExpr) (y, tzi) = (utcM', calDateM', Entry tran, expr)
      where
        expr = tziToExprInfo tzi
        m = toEnum 0
        tran = Instant (fromIntegral $ yearMonthDayToDays y m 1) 0 0
        utcM' = addUtcTransitionExpression tran expr utcM
        calDateM' = addCalDateTransitionExpression prevEntry before prevExpr calDateM
        before = Entry . flip minus (fromNanoseconds 1) $ tran
    tziToExprInfo (REG_TZI_FORMAT bias stdBias dstBias end start) = TransitionExpressionInfo startExpr endExpr stdTI dstTI
      where
        startExpr = systemTimeToNthDayExpression start stdOffSecs
        endExpr = systemTimeToNthDayExpression end dstOffSecs
        stdTI = TransitionInfo (Offset stdOffSecs) False stdAbbr
        dstTI = TransitionInfo (Offset dstOffSecs) True dstAbbr
        stdOffSecs = 60 * (negate . fromIntegral $ bias + stdBias)
        dstOffSecs = stdOffSecs + 60 * (negate . fromIntegral $ dstBias)

systemTimeToNthDayExpression :: SYSTEMTIME -> Int -> TransitionExpression
systemTimeToNthDayExpression (SYSTEMTIME _ m d nth h mm s _) offsetSecs = NthDayExpression (fromIntegral m - 1) (adjust . fromIntegral $ nth) (fromIntegral d) s''
  where
    adjust 5 = -1                     -- In the registry, 5 actually means last which is -1 for us
    adjust n = n - 1                  -- Switch start nth to zero based
    s'' = s' - offsetSecs             -- Windows times are always local, so convert back to UTC
    s' = h' + mm' + fromIntegral s
    h' = fromIntegral h * 60 * 60
    mm' = fromIntegral mm * 60

readLocalZoneName :: IO String
readLocalZoneName =
  bracket op regCloseKey $ \key ->
  regQueryValue key (Just "TimeZoneKeyName")
    where
      op = regOpenKeyEx hKEY_LOCAL_MACHINE hive kEY_QUERY_VALUE
      hive = "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation"

readTziForZone :: String -> IO (String, String, REG_TZI_FORMAT)
readTziForZone zone =
  bracket op regCloseKey $ \key -> do
    std <- regQueryValue key (Just "Std")
    dst <- regQueryValue key (Just "Dlt")
    tzi <- readTzi key "TZI"
    return (std, dst, tzi)
    where
      op = regOpenKeyEx hKEY_LOCAL_MACHINE hive kEY_QUERY_VALUE
      hive = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\" ++ zone

readDynamicDstForZone :: String -> IO [(Int, REG_TZI_FORMAT)]
readDynamicDstForZone zone = do
  hasDyn <- hasDynDst
  if hasDyn
  then 
    bracket (op dynDstHive) regCloseKey $ \dstKey -> do
      vals <- regEnumKeyVals dstKey
      let years = foldr toYear [] vals
      forM years $ \y -> do
        tzi <- readTzi dstKey y
        return (read y, tzi)
  else return []
  where
    hasDynDst = bracket (op tzHive) regCloseKey $ \key -> do
                  dyn <- regEnumKeys key
                  return $ length dyn == 1 && dyn !! 0 == "Dynamic DST"
    toYear (y, _, _) xs | all isDigit y = y:xs
                        | otherwise = xs
    op hive = regOpenKeyEx hKEY_LOCAL_MACHINE hive kEY_READ
    tzHive = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\" ++ zone
    dynDstHive = tzHive ++ "\\Dynamic DST"

readTzi :: HKEY -> String -> IO REG_TZI_FORMAT
readTzi key p =
  allocaBytes sz $ \ptr -> do
    rvt <- regQueryValueEx key p ptr sz
    verifyAndPeak rvt ptr
  where
    sz = sizeOf (undefined :: REG_TZI_FORMAT)
    verifyAndPeak rvt ptr
        | rvt == rEG_BINARY = peek . castPtr $ ptr
        | otherwise         = error $ "registry corrupt: TZI variable was non-binary type: " ++ show rvt