packages feed

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;
        };
    };
}