packages feed

hodatime-1.0.0.0: platform/windows/Data/HodaTime/Locale/Platform.hsc

{-# LANGUAGE ForeignFunctionInterface #-}

-- | Windows implementation of the locale reader, via @GetLocaleInfoEx@.
--
--   The name fields (month\/weekday names and AM\/PM designators, which power @pMMMM'@, @pddd'@, @ppp'@ and the reader)
--   come straight from the OS.  The @raw*Format@ fields are the Windows /picture/ strings (e.g. @dd.MM.yyyy@) translated
--   to POSIX @strftime@ by 'windowsPictureToStrftime', so the layout-compiling patterns in
--   "Data.HodaTime.Pattern.Locale" (@localeDatePattern@ and friends) work on a Windows-read locale just as on POSIX.
module Data.HodaTime.Locale.Platform
(
  loadCurrentLocale
 ,loadLocaleByName
)
where

import Data.HodaTime.Locale.Internal (Locale(..), windowsPictureToStrftime)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.C.Types (CInt(..), CWchar)
import Foreign.Marshal.Array (allocaArray, peekArray, withArray0)
import Data.Bits ((.&.))
import Data.Char (chr, ord)

#include <windows.h>

-- @int GetLocaleInfoEx(LPCWSTR lpLocaleName, LCTYPE LCType, LPWSTR lpLCData, int cchData)@.  (Windows CI is x86-64,
-- where @ccall@ is the sole calling convention; a 32-bit build would need @stdcall@.)
foreign import ccall unsafe "GetLocaleInfoEx"
  c_GetLocaleInfoEx :: Ptr CWchar -> CInt -> Ptr CWchar -> CInt -> IO CInt

-- LCTYPE bases (the numbered families are consecutive in @winnls.h@, so 'enumFrom' walks them).
lOCALE_SMONTHNAME1, lOCALE_SABBREVMONTHNAME1, lOCALE_SDAYNAME1, lOCALE_SABBREVDAYNAME1 :: CInt
lOCALE_SMONTHNAME1       = #{const LOCALE_SMONTHNAME1}
lOCALE_SABBREVMONTHNAME1 = #{const LOCALE_SABBREVMONTHNAME1}
lOCALE_SDAYNAME1         = #{const LOCALE_SDAYNAME1}
lOCALE_SABBREVDAYNAME1   = #{const LOCALE_SABBREVDAYNAME1}

lOCALE_S1159, lOCALE_S2359, lOCALE_SSHORTDATE, lOCALE_STIMEFORMAT :: CInt
lOCALE_S1159       = #{const LOCALE_S1159}
lOCALE_S2359       = #{const LOCALE_S2359}
lOCALE_SSHORTDATE  = #{const LOCALE_SSHORTDATE}
lOCALE_STIMEFORMAT = #{const LOCALE_STIMEFORMAT}
#if defined(LOCALE_SNAME)
lOCALE_SNAME :: CInt
lOCALE_SNAME       = #{const LOCALE_SNAME}
#endif

-- | Query one @LCTYPE@ from the given locale (a pointer to a wide name, or 'nullPtr' for the current locale) and decode
--   it as a 'String' (UTF-16, BMP — locale strings never use surrogate pairs).
getInfo :: Ptr CWchar -> CInt -> IO String
getInfo loc lctype = do
  n <- c_GetLocaleInfoEx loc lctype nullPtr 0
  if n <= 0
    then return ""
    else allocaArray (fromIntegral n) $ \buf -> do
      _  <- c_GetLocaleInfoEx loc lctype buf n
      ws <- peekArray (fromIntegral n) buf
      return . map (chr . (0xFFFF .&.) . fromIntegral) . takeWhile (/= 0) $ ws

-- | Run an action with a wide, null-terminated locale name, or with 'nullPtr' for the current locale.
withLocaleName :: Maybe String -> (Ptr CWchar -> IO a) -> IO a
withLocaleName Nothing  act = act nullPtr
withLocaleName (Just s) act = withArray0 0 (map (fromIntegral . ord) s) act

-- | POSIX @\"C\"@\/@\"POSIX\"@ (and the empty name) map to the Windows /invariant/ locale (@L\"\"@).
normalizeName :: String -> String
normalizeName n
  | n `elem` ["C", "POSIX", ""] = ""
  | otherwise                   = n

buildLocale :: String -> Ptr CWchar -> IO Locale
buildLocale lid loc = do
  monsFull    <- mapM (getInfo loc) (take 12 [lOCALE_SMONTHNAME1 ..])
  monsAbbr    <- mapM (getInfo loc) (take 12 [lOCALE_SABBREVMONTHNAME1 ..])
  daysMon     <- mapM (getInfo loc) (take 7  [lOCALE_SDAYNAME1 ..])        -- Windows is Monday-first
  daysAbbrMon <- mapM (getInfo loc) (take 7  [lOCALE_SABBREVDAYNAME1 ..])
  am    <- getInfo loc lOCALE_S1159
  pm    <- getInfo loc lOCALE_S2359
  sdate <- windowsPictureToStrftime <$> getInfo loc lOCALE_SSHORTDATE
  stime <- windowsPictureToStrftime <$> getInfo loc lOCALE_STIMEFORMAT
  return Locale
    { localeId          = lid
    , monthNames        = monsFull
    , monthNamesShort   = monsAbbr
    , dayNames          = sundayFirst daysMon
    , dayNamesShort     = sundayFirst daysAbbrMon
    , amName            = am
    , pmName            = pm
    , rawDateFormat     = sdate
    , rawTimeFormat     = stime
    , rawDateTimeFormat = sdate ++ " " ++ stime
    }
  where
    sundayFirst xs = last xs : init xs   -- [Mon .. Sun] -> [Sun, Mon .. Sat]

-- | Read a specific locale by (Windows or normalized POSIX) name; 'Nothing' if it is not a valid locale.
loadLocaleByName :: String -> IO (Maybe Locale)
loadLocaleByName name = withLocaleName (Just (normalizeName name)) $ \loc -> do
  n <- c_GetLocaleInfoEx loc lOCALE_SMONTHNAME1 nullPtr 0   -- validity probe
  if n <= 0 then return Nothing else Just <$> buildLocale name loc

-- | Read the current (user default) locale.
loadCurrentLocale :: IO Locale
loadCurrentLocale = do
#if defined(LOCALE_SNAME)
  lid <- withLocaleName Nothing (`getInfo` lOCALE_SNAME)
  withLocaleName Nothing (buildLocale (if null lid then "C" else lid))
#else
  withLocaleName Nothing (buildLocale "C")
#endif