packages feed

timezone-olson-0.1.0: hzdump.hs

-- | A clone in Haskell of the zdump(8) command, including most of
-- the bugs, that takes paths to Olson files instead of timezone
-- names. This is useful for testing the Haskell implementation
-- of the Olson timezone parser and renderer against the reference
-- implementation in C. But less useful than it might seem at first,
-- because Haskell rounds historical solar mean timezones to the
-- nearest minute, whereas the C implementation rounds to the nearest
-- second.
module Main where

import Data.Time.LocalTime.TimeZone.Series
import Data.Time.LocalTime.TimeZone.Olson
import Data.Time
import Data.Maybe (listToMaybe)
import System.Environment (getArgs)
import System.Exit (exitWith, exitSuccess, ExitCode(ExitFailure))
import System.Locale (defaultTimeLocale)
import Control.Monad (guard)

version :: IO a
version = do
  putStrLn "hzdump version 0.1, clone of zdump 1.7  -c option fixed,"
  putStrLn "takes zone file paths instead of zone specifications"
  exitSuccess

usage :: IO a
usage = do
  putStrLn "usage: hzdump [--version] [-v] [-c cutoff] zone-file-path ..."
  putStrLn "  where cutoff is lo-year,hi-year or hi-year"
  exitWith $ ExitFailure 1

illegalOpt :: Char -> IO a -- sic
illegalOpt opt = do
  putStrLn $ "hzdump: illegal option -- " ++ [opt]
  usage

data Option = Version | Illegal Char |
              Verbose (Maybe Integer, Maybe Integer) | Now
  deriving (Eq, Ord, Show)

main = do
    (opts, zones) <- fmap parseArgs getArgs
    (getTimes, displayTime) <- case opts of
      Version     -> version
      Illegal opt -> illegalOpt opt
      Verbose rng -> return (transitionTimes rng, displayVerbose)
      _           -> do now <- getCurrentTime
                        return (const [now], displayConcise)
    tzss <- mapM getZone zones
    putStr . unlines . concat $
      zipWith (displayZone getTimes displayTime) zones tzss
  where
    getZone "-" = return utcTZ -- sic
    getZone z   = getTimeZoneSeriesFromOlsonFile z

    displayZone getTimes displayTime zone tzs =
      map ((zone ++) . ("  " ++) . displayTime tzs) $ getTimes tzs

parseArgs :: [String] -> (Option, [FilePath])
parseArgs args | "--version" `elem` args = (Version, [])
parseArgs args = getOpts False (Nothing, Nothing) args
  where
    getOpts _ cutoff  ("-v":args)   = getOpts True cutoff args
    getOpts v _       ("-c":c:args) = maybe (Illegal 'c', [])
                                      (\y -> getOpts v y args) $
                                      parseCutoff c
    getOpts v cutoff  ("--":zones ) = (opts v cutoff, zones)
    getOpts _ _       (('-':x:_):_) = (Illegal x, [])
    getOpts v cutoff  zones         = (opts v cutoff, zones)

    opts v cutoff = if v then Verbose cutoff else Now

    parseCutoff c = let p0  = listToMaybe $ reads c
                        los = fmap fst p0
                        his = do str <- fmap snd p0
                                 let (comma, str1) = splitAt 1 str
                                 guard $ comma == ","
                                 maybeRead str1
                    in los >> Just (maybe (Nothing, los) ((,) los . Just) his)

displayConcise :: TimeZoneSeries -> UTCTime -> String
displayConcise tzs t = formatTime defaultTimeLocale format $
                       ZoneSeriesTime t tzs
  where
    format = "%a %b %e %T %Y %Z"

displayVerbose :: TimeZoneSeries -> UTCTime -> String
displayVerbose tzs t = concat [displayConcise utcTZ t, " = ",
    displayConcise tzs t, " isdst=", if isdst then "1" else "0"]
  where
    isdst = timeZoneSummerOnly $ timeZoneFromSeries tzs t

transitionTimes :: (Maybe Integer, Maybe Integer) -> TimeZoneSeries ->
                   [UTCTime]
transitionTimes (lo, hi) =
    maybe id (takeWhile . (>)) (fmap happyNewYear hi) .
    maybe id (dropWhile . (>)) (fmap happyNewYear lo) .
    addBugs . addPrevSecond . actualTransitions
  where
    happyNewYear y = UTCTime (fromGregorian y 1 1) 0
    actualTransitions (TimeZoneSeries d cs) =
      let rs = reverse cs
      in map snd . filter fst $
         zipWith (\(t, tz) prevTz -> (tz /= prevTz, t))
                 rs
                 (d : map snd rs)
    addPrevSecond = concatMap (\t -> [(-1) `addUTCTime` t, t])
    addBugs = (bug0 ++) . (++ bug1) -- sic
    bug0 = [UTCTime (fromGregorian 1901 12 13) (20*3600+45*60+52),
            UTCTime (fromGregorian 1901 12 14) (20*3600+45*60+52)]
    bug1 = [UTCTime (fromGregorian 2038  1 18) ( 3*3600+14*60+ 7),
            UTCTime (fromGregorian 2038  1 19) ( 3*3600+14*60+ 7)]

utcTZ :: TimeZoneSeries
utcTZ = TimeZoneSeries utc []

maybeRead :: Read a => String -> Maybe a
maybeRead = fmap fst . listToMaybe . filter (null . snd) . reads