プログレスバーをテキストで表示する関数

プログレスバーをテキストで表示する関数を作ってみました。getProgressBarを呼ぶと関数が返ってきて、その関数に現在の進捗を与えるとプログレスバーを表示します。時刻に関連するIOは、getProgressBarの中で閉じるようにしています。

module Main (main) where

import Control.Concurrent (threadDelay)
import System.IO (stderr)
import System.Random (getStdGen, randomRs)
import System.Time (getClockTime, diffClockTimes, 
                    ClockTime, tdYear, tdMonth, tdHour, tdMin, tdSec)
import Text.Printf (printf, hPrintf)

getProgressBar :: String -> Int -> IO (Int -> IO ())
getProgressBar title total = return . f =<< getClockTime
  where
    f start n
      | n > total = f' start 100 >> putStrLn ""
      | otherwise = f' start $ n * 100 `div` total

    f' :: ClockTime -> Int -> IO ()
    f' start percent = do
      now <- getClockTime
      hPrintf stderr "\r%-15s %3d%% |%s| Time: %s" title percent bar $ time now
      where
        bar =
          let max = 40
              n   = percent * max `div` 100
          in (replicate n 'o') ++ (replicate (max - n) ' ')

        time :: ClockTime -> String
        time now =
          let elapsed = diffClockTimes now start
              hour    = (tdYear elapsed * 365 * 24) * 
                          (tdMonth elapsed * 30) * 
                          (tdHour elapsed)
              min     = tdMin elapsed
              sec     = tdSec elapsed
          in printf "%02d:%02d:%02d" hour min sec

main :: IO ()
main = do
  pf <- getProgressBar "test:" 200
  loop pf 0 . randomRs (1, 20) =<< getStdGen
  where
    loop _  _ [] = error "loop: not reach here."
    loop pf n (x : xs)
      | n >= 200  = pf n >> return ()
      | otherwise = pf n >> threadDelay (500 * 1000) >> loop pf (n + x) xs

実行例:

$ ./progtest.exe
test:            27% |oooooooooo                              | Time: 00:00:02

参考: Ruby/ProgressBar: A Text Progress Bar Library for Ruby