timezone-unix-1.0: src/Data/Time/LocalTime/TimeZone/Unix.hs
-- | UNIX-specific handling of time data.
module Data.Time.LocalTime.TimeZone.Unix
(
-- * Time Zone
getCurrentTimeZoneSeries,
getTimeZoneSeriesForZone,
-- * Country Information
CountryCode,
getCountryCodes,
-- * Zone Information
TimeZoneSpec,
ZoneDescription(..),
getZoneDescriptions,
-- * OlsonData
getCurrentOlsonData,
getOlsonDataForZone,
-- * Leap Seconds
getLeapSecondList,
) where
{
import Data.Maybe;
import Data.List;
import System.Environment;
import System.Directory;
import System.FilePath.Posix;
import Data.Time;
import Data.Time.LocalTime.TimeZone.Series;
import Data.Time.LocalTime.TimeZone.Olson;
import Data.Time.Clock.LeapSeconds;
separate :: Char -> String -> [String];
separate sep s = let
{
(val,rest) = break ((==) sep) s;
vals = case rest of
{
[] -> [];
_:s' -> separate sep s';
};
} in val : vals;
findPath :: [FilePath] -> IO (Maybe FilePath);
findPath [] = return Nothing;
findPath (path:paths) = do
{
exists <- doesFileExist path;
if exists then return (Just path) else findPath paths;
};
-- http://www.pathname.com/fhs/pub/fhs-2.3.html#SPECIFICOPTIONS15
-- http://askubuntu.com/questions/118015/where-is-usr-share-zoneinfo-specified
zoneFilePath :: String -> FilePath;
zoneFilePath filename = "/usr/share/zoneinfo" </> filename;
readZoneInfoFile :: ([String] -> Maybe a) -> [String] -> IO [a];
readZoneInfoFile toValues filenames = do
{
let
{
filepaths = fmap zoneFilePath filenames;
};
mfilepath <- findPath filepaths;
filepath <- case mfilepath of
{
Just path -> return path;
Nothing -> fail $ "could not find " ++ intercalate " " filepaths;
};
text <- readFile filepath;
let
{
readLine ('#':_) = return Nothing;
readLine "" = return Nothing;
readLine s = case toValues $ separate '\t' s of
{
Just a -> return $ Just a;
Nothing -> fail $ "unexpected line in "++filepath++": " ++ s;
};
};
mstrs <- traverse readLine $ lines text;
return $ catMaybes mstrs;
};
-- | ISO 3166 alpha-2 country code (i.e., two capital letters).
;
type CountryCode = String;
-- | Get the country codes and names found in @iso3166.tab@.
-- Note that earlier versions of zoneinfo don't include this file, in which case this will throw exception matching 'isDoesNotExistError'.
;
getCountryCodes :: IO [(CountryCode,String)];
getCountryCodes = let
{
toCountryCode [code,country] = Just (code,country);
toCountryCode _ = Nothing;
} in readZoneInfoFile toCountryCode ["iso3166.tab"];
-- | Time-zone specification as used in the TZ environment variable, e.g. \"America/Los_Angeles\".
;
type TimeZoneSpec = String;
-- | Information about a zone.
;
data ZoneDescription = MkZoneDescription
{
-- | The countries that overlap the zone.
zoneCountries :: [CountryCode],
-- | (Latitude, longitude, accuracy) of principal location in degrees, + is north, east.
-- Accuracy indicates that the location is specified to arcseconds rather than arcminutes.
zoneLocation :: (Rational,Rational,Bool),
-- | Zone name.
zoneName :: TimeZoneSpec,
-- | Comments; present if and only if a country has multiple zones.
zoneComment :: String
};
getSign :: Char -> Maybe Rational;
getSign '+' = Just 1;
getSign '-' = Just (-1);
getSign _ = Nothing;
getDigit :: Char -> Maybe Rational;
getDigit c | c < '0' = Nothing;
getDigit c | c > '9' = Nothing;
getDigit c = Just $ toRational $ (fromEnum c) - (fromEnum '0');
getDigit2 :: Char -> Char -> Maybe Rational;
getDigit2 c1 c2 = do
{
r1 <- getDigit c1;
r2 <- getDigit c2;
return $ r1 * 10 + r2;
};
getDigit3 :: Char -> Char -> Char -> Maybe Rational;
getDigit3 c1 c2 c3 = do
{
r1 <- getDigit2 c1 c2;
r2 <- getDigit c3;
return $ r1 * 10 + r2;
};
parseISO6709 :: String -> Maybe (Rational,Rational,Bool);
parseISO6709 [xsn,xd1,xd2,xm1,xm2,ysn,yd1,yd2,yd3,ym1,ym2] = do
{
xsgn <- getSign xsn;
xdeg <- getDigit2 xd1 xd2;
xmin <- getDigit2 xm1 xm2;
ysgn <- getSign ysn;
ydeg <- getDigit3 yd1 yd2 yd3;
ymin <- getDigit2 ym1 ym2;
return (xsgn * (xdeg + xmin / 60),ysgn * (ydeg + ymin / 60),False);
};
parseISO6709 [xsn,xd1,xd2,xm1,xm2,xs1,xs2,ysn,yd1,yd2,yd3,ym1,ym2,ys1,ys2] = do
{
xsgn <- getSign xsn;
xdeg <- getDigit2 xd1 xd2;
xmin <- getDigit2 xm1 xm2;
xsec <- getDigit2 xs1 xs2;
ysgn <- getSign ysn;
ydeg <- getDigit3 yd1 yd2 yd3;
ymin <- getDigit2 ym1 ym2;
ysec <- getDigit2 ys1 ys2;
return (xsgn * (xdeg + xmin / 60 + xsec / 3600),ysgn * (ydeg + ymin / 60 + ysec / 3600),True);
};
parseISO6709 _ = Nothing;
getISO6709 :: String -> (Rational,Rational,Bool);
getISO6709 location = case parseISO6709 location of
{
Just loc -> loc;
Nothing -> error $ "bad IS06709: " ++ location;
};
-- | Get the zone descriptions found in @zone1970.tab@ (or @zone.tab@).
;
getZoneDescriptions :: IO [ZoneDescription];
getZoneDescriptions = let
{
toZoneDescription [countries,location,name] = Just $ MkZoneDescription
{
zoneCountries = separate ',' countries,
zoneLocation = getISO6709 location,
zoneName = name,
zoneComment = ""
};
toZoneDescription [countries,location,name,comment] = Just $ MkZoneDescription
{
zoneCountries = separate ',' countries,
zoneLocation = getISO6709 location,
zoneName = name,
zoneComment = comment
};
toZoneDescription _ = Nothing;
} in readZoneInfoFile toZoneDescription ["zone1970.tab","zone.tab"];
existingZoneNamePath :: String -> IO (Maybe FilePath);
existingZoneNamePath name = do
{
let
{
path = zoneFilePath name;
};
exists <- doesFileExist path;
return $ if exists then Just path else Nothing;
};
getZoneNamePath :: Maybe TimeZoneSpec -> IO (Maybe FilePath);
getZoneNamePath Nothing = return $ Just "/etc/localtime";
getZoneNamePath (Just (':':name)) = existingZoneNamePath name;
getZoneNamePath (Just name) = existingZoneNamePath name;
defaultTimeZoneSeries :: TimeZoneSeries;
defaultTimeZoneSeries = TimeZoneSeries utc [];
-- | Get the 'TimeZoneSeries' for this 'TimeZoneSpec' (or for the system default), defaulting to UTC if the name is not found.
;
getTimeZoneSeriesForZone :: Maybe TimeZoneSpec -> IO TimeZoneSeries;
getTimeZoneSeriesForZone mname = do
{
mpath <- getZoneNamePath mname;
case mpath of
{
Just path -> getTimeZoneSeriesFromOlsonFile path;
Nothing -> return $ fromMaybe defaultTimeZoneSeries $ olsonToTimeZoneSeries $ mempty {olsonPosixTZ=mname};
};
};
-- | Get the current 'TimeZoneSeries' (as specifed by @TZ@ env-var or else the system default).
;
getCurrentTimeZoneSeries :: IO TimeZoneSeries;
getCurrentTimeZoneSeries = do
{
mtzvar <- lookupEnv "TZ";
getTimeZoneSeriesForZone mtzvar;
};
-- | Obtain an 'OlsonData' file for this 'TimeZoneSpec' (or for the system default).
-- If the file is not in the database, return an empty 'OlsonData' with the name in 'olsonPosixTZ'.
;
getOlsonDataForZone :: Maybe TimeZoneSpec -> IO OlsonData;
getOlsonDataForZone mname = do
{
mpath <- getZoneNamePath mname;
case mpath of
{
Just path -> getOlsonFromFile path;
Nothing -> return $ mempty {olsonPosixTZ=mname};
};
};
-- | Get the current 'OlsonData' (as specifed by @TZ@ env-var or else the system default).
;
getCurrentOlsonData :: IO OlsonData;
getCurrentOlsonData = do
{
mtzvar <- lookupEnv "TZ";
getOlsonDataForZone mtzvar;
};
-- | Get the leap-second list found in @leap-seconds.list@.
-- Note that earlier versions of zoneinfo don't include this file, in which case this will throw exception matching 'isDoesNotExistError'.
;
getLeapSecondList :: IO LeapSecondList;
getLeapSecondList = do
{
let
{
filepath = zoneFilePath "leap-seconds.list";
};
text <- readFile filepath;
case parseNISTLeapSecondList text of
{
Just lsl -> return lsl;
Nothing -> fail $ "failed to parse " ++ filepath;
};
};
}