ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【ツール作成】プログレスバークラスを作る

前置き

プログレスバーのクラスを作ります。

どうやって作る?

作りはとっても単純です。

  • 全体、実行済みの値から、進捗度を計算
  • 進捗度からバーの状態を生成
  • キャリッジリターン \r を使って、コンソール行を上書きする。

行全体を上書きしますが、一部だけ変わっているので、その行でバーが動いているように見えます。

バーのフォーマットと動き

バーは、メモリをつけた形をベースにして、src_dir ディレクトリの処理が進んでいくと、=> の記号が伸びていき、100% で完了します。

src_dir |---------+---------+---------+---------| 0% ( done / total ) copying_filename 
↓
src_dir |===================>---------+---------| 50% ( done / total ) copying_filename 
↓
src_dir |=======================================| 100% ( done / total ) copying_filename 【完了】

バーの部分は、ベースの文字列(|--+--| の部分)を作っておいて、進捗度を計算し、進捗部分(==> の部分)を作って、ベース文字列の部分を差し替えるように作っています。

今回は、コピー元ディレクトリを複数登録できるようにしていて、コピー元ディレクトリ毎に、プログレスバーオブジェクトを生成するようにしました。流れとしては、

  • コピー元ディレクトリの総ファイルサイズをtotalの値として生成
  • ファイルを一つコピーするごとに、コピーしたファイルのファイルサイズを done に加算
  • 都度、進捗割合を計算し、プログレスバー文字列を作成
  • 前に表示した内容を消して、新しいプログレスバー文字列を表示

といった感じです。

実装コード

class ProgressBar

    # 定数定義 
    EDGE = "|".freeze                   # バーの両端
    SCALE = ("---------+" * 4).freeze   # バーの目盛り部分
    BAR = ((EDGE + SCALE).chop + EDGE).freeze   # バーのベース文字列
    DONE = "=".freeze                   # 済み記号
    ARROW = ">".freeze                  # 済み記号の先端

    #
    # initialize    初期化
    #   引数
    #       title   :   プログレスバーのタイトル名
    #       total   :   プログレスバーの 100%の値
    #
    def initialize(title, total)
        # インスタンス変数 定義
        @title = title      # 進捗表示する処理名(フォルダー名とか渡す)
        @total = total      # 100% 時の値(単位は何でもいい。)
        @done = 0           # 完了した値
        @showed_length = 0  # 直前に表示した文字列の長さ(上書きの際に使用する。)
    end
    
    #
    # show 
    # プログレスバーの表示更新
    #   引数
    #       info    :   バーに表示する情報文字列
    #       size    :   進捗する値   
    #
    def show(info, size)

        # 処理したファイルサイズを加算し、進捗割合を計算
        @done += size        
        progress = get_progress(@total, @done)

        bar = BAR.dup     # BAR の内容を複製(こうしないと BAR の値が変わってしまう。。)

        if progress >= 100 then                 # 進捗が 100% 以上の場合
            bar[1..-2] = DONE*(BAR.length-2)  # 両端を除く全てを DONE 記号で埋める。
        else
            tmp = EDGE + (DONE*(progress*(SCALE.length)/100).floor).chop + ARROW    # 進捗部分を生成して、
            bar[0..tmp.length-1] = tmp                                            # 進捗部分を置き換える。
        end

        # 前回表示した行を消去
        print "\r".ljust(@showed_length)

        # 情報表示
        line = "\r" + sprintf("%s %s %s%% (%s/%s) %s...", @title[0,10], bar, progress, get_size_str(@done), get_size_str(@total), info[0,10])
        print line
        @showed_length = line.length
    end
    
    #
    # reset
    # 進捗をリセットする。
    #
    def reset
        @done = 0
        @showed_length = 0
    end

    private

    #
    # get_progress
    # 進捗度を割合計算し、100分率(%)で返す。
    #   引数
    #       total   :   100% となるサイズ
    #       done    :   実行済みのサイズ
    #   戻り値
    #       進捗度(100分率の整数値。端数は四捨五入)
    #
    def get_progress(total, done)
        if done <= 0    then return 0   end     # done が 0 以下の場合は、0 を返す。
        if total <= 0   then return 100 end     # total が 0 以下 : 100% として返す。
        if total < done then return 101 end     # total が done よりも小さい : 101% として返す。

        return ((done*100)/total).round     # 計算して返す。値は四捨五入で返す。
    end

    #
    # get_size_str      バイト数を KiB, MiB, GiB のうち、最適な単位に変換して返す。
    #   引数
    #       bytes   :   バイト数
    #   戻り値
    #       最適な単位にしたバイト数と単位を付与した文字列
    #
    def get_size_str(bytes)
        unit = ["Bytes", "KiB", "MiB", "GiB"]
        block = 1024

        # KiB, MiB, GiB かどうかを判定する。
        for n in 1..3 do
            if bytes.between?(block**n, block**(n+1)) then
                return ((bytes/block**n).floor).to_s + unit[n]
            end
        end

        # どの単位にも合致しない場合は、そのまま Bytes で返す。
        return bytes.to_s + unit[0]
    end
end

実装のポイント

コメントを多めに入れてるので、読んでいただければそのまま分かると思いますが、、、

ベースとなる 0%状態のバー文字列は、定数で最初に宣言しています。そして、加工のためにbar変数に文字列の内容を複製しているのですが、理解されている方には分かり切ったことですが、代入では値の複製にならない、必ずdupメソッドで値の複製をすること!です。

dup メソッドの後、進捗部分の文字列を置き換える処理で、bar[1..-2] のような書き方をしていますが、これが破壊的メソッドなので、line = BAR のような代入をしてしまうと、BAR の値も同時に書き換わってしまいます。

また、新しいバーをそのまま上書きしてしまうと、もし前回表示した行の方が長かった場合、長かった分の文字列が残ってしまいます。ですので、@showed_length 変数を定義して表示した文字列の長さを取っておき、新たに表示するときにその長さの空白で上書きして、確実に消えるようにしています。(もちろん、\r 付きで上書きです)

後は、バイトの表示単位を変換するget_size_str メソッドを作ったりしてるくらいですか。

例外処理については、ワザと変な引数(nil とか型違いとか)を入れない限り、ロジック上で例外が発生する可能性がないため、このクラスでは行っていません。

次は・・・設定関係のクラスかな・・・