読者です 読者をやめる 読者になる 読者になる

ハウテレビジョン開発者ブログ

『外資就活ドットコム』を日夜開発している技術陣がプログラミングネタ・業務改善ネタ・よしなしごとについて記していきます。

作業時間管理と報告書の生成〜あるいはBDDの成果

時間が無い、時間が無いと毎日うわ言のようにつぶやいている @artifactsauce です。皆さんも毎日お忙しいですよね。今回は時間管理とその補助ツール、そしてその補助ツールの開発についてのお話です。

長い前置き

報告書 Qiita:Team

弊社では情報共有にQiita:Teamを利用しているのですが、日報/週報にも用いています。設定によってはメンションが含まれたページをメールで配信してくれるため、同僚のその日の作業内容を帰りの電車の中で確認することができ、メンバー間のコミュニケーションの促進に貢献しています。日報はその他にも、その日に発生した問題や現在抱えている悩みなどを共有するための重要な文書となっており、弊社にとっては欠かせないツールとなっています。

タイムトラッキング Toggl

一方で日報などの報告書は自分のための振り返りの材料でもあります。どの作業に何時間くらい使っていたのかを記載しておけば、ボトルネックを知ることにより、さらなる効率化を図る事ができるでしょう。推測するな計測せよという言葉が示している通り、作業時間を計測しておくことは必要です。私は最近、Togglというタイムトラッキングサービスを用いて作業時間を計測しています。TogglはWebやデスクトップアプリ、モバイルアプリでトラッキング操作ができる優れた環境を提供してくれており、お勧めです。

日報に作業時間を記載

計測した結果を日報に書いてみましょう。最初は百分率(%)で作業時間を記載していたのですが、振り返る際、日報の粒度では実際に何時間作業したのかを知りたくなりました。しかし一方で、週単位では何時間作業したのかよりも、俯瞰するために割合を把握して見たいと思うようになりました。そこで私は毎日作業時間を集計して日報に記載し、週末にはその週の全体の作業時間から各作業の割合を手計算して週報に記載していました。

しかし、その作業のあまりの煩雑さ、そして作業の単純さに次第にイライラが募ってきました。あまりのイライラに計測をやめる日もありました。しかしそれでは本来の目的が達成できません。楽をするためならどんな苦労もいとわないのがプログラマーの美徳です。ちょうどこの開発者ブログを書く時期も近づいていたので、ブログ駆動開発(BDD)でレポート出力プログラムを作成したいと思い立ちました。さっそく自宅に戻り、ビールを1本(500ml)ひっかけ、ビール駆動開発(BDD)で最初のバージョンを実装しました。

開発

Toggl API

最近の多くのイケてるサービス同様、TogglにもAPIがあります。TogglのAPIドキュメントはGitHubのリポジトリとしてホストされています。日報/週報を作成するためには期間を指定してレコードを取得できる必要がありますが、ちゃんとあるようですね。記録した作業内容と時間をAPIから取得して日報を作成することにしましょう。

Ruby & Thor

日報を作成するきっかけは自分で「今日はもう帰ろう」と思うことですよね。時刻(例えば定時)によって単純に決められるものでもないでしょう。今回はコマンドラインツールとして実装し、レポート作成プログラムを手動で実行することにします。私の母国語はPerlなのですが、今回、プログラミング言語として採用したのはRubyです。ちょっとRubyに慣れたかったというのが主な理由です。BDDですから、大した理由は必要ありません。コマンドラインツール作成のフレームワークとしては Thor を採用しました。

operating_report

gemとしてインストールできるように作成します。名前は operating_report で良いでしょうかね?まずはgemの骨組みを作成します。下記の通りにコマンドを実行します。

$ bundle gem -t minitest -b -V operating_report
create  operating_report/Gemfile
create  operating_report/Rakefile
create  operating_report/LICENSE.txt
create  operating_report/README.md
create  operating_report/.gitignore
create  operating_report/operating_report.gemspec
create  operating_report/lib/operating_report.rb
create  operating_report/lib/operating_report/version.rb
create  operating_report/bin/operating_report
create  operating_report/test/minitest_helper.rb
create  operating_report/test/test_operating_report.rb
create  operating_report/.travis.yml
Initializing git repo in /Users/hoge/work/operating_report

これでテストと実行コマンドを備えたgemの骨組みが作成できました。

それでは、プログラムを実装していきましょう。コマンドラインのフロントとなる部分です。

bin/operating_report

#!/usr/bin/env ruby

require 'operating_report'

OperatingReport::CLI.start(ARGV)

スケルトンに最後の一行を足すだけです。呼び出される先のクラス OperatingReport::CLI はThorを継承しています。

このファイルでrequireされているファイルにはどんな処理が書かれているのでしょうか?

lib/operating_report.rb

require "operating_report/version"
require "operating_report/cli"

module OperatingReport
end

このファイルはversionを記載するモジュールを呼ぶことと、実態となるクラスファイル lib/operating_report/cli.rb を呼ぶことが責務であるという認識で良いでしょう。

それではいよいよ、処理の本体なるクラス OperatingReport::CLI を見てみましょう。

lib/operating_report/cli.rb

# coding: utf-8
require "thor"
require "yaml"
require "operating_report/tracker/api/toggl"

module OperatingReport
    class CLI < Thor
        def initialize(*args)
            super
            @config_file = ENV['HOME'] + '/.report'
            @config = _load_config(@config_file)
        end

        desc "init", "create a config file."
        def init
            puts "Create a configuration file at #{@config_file}."
            if File.exist?(@config_file) then
                is_overwritable = false;
                print "Sure you want to overwrite it? [y/n] "
                answer = STDIN.gets.chomp
                if /^(?:y)(?:es)?$/i =~ answer then
                    is_overwritable = true;
                end

                unless is_overwritable then
                    abort("Cofiguration file aleready exists.")
                end
            end

            config = Hash.new();

            print "Toggl API Token: "
            api_token = STDIN.gets.chomp

            config['tracker'] = {
                'api' => {
                    'token' => api_token
                }
            }

            File.open(@config_file, 'w') do |f|
                f.write(YAML.dump(config))
            end
        end

        desc "create [PERIOD]", "create a report. (parameter required)"
        def create(period)
            t = Time.now

            case period
            when 'daily' then
                start_date = Time.new(t.year, t.mon, t.day, 0, 0, 0)
                end_date = Time.new(t.year, t.mon, t.day, 23, 59, 59)
            else
                abort("Undefined period.")
            end

            tog = OperatingReport::Tracker::Api::Toggl.new(
                'token' => @config['tracker']['api']['token']
            )
            response = tog.get_time_entries(start_date, end_date)

            body = {}
                response.each do |x|
                body[x['description']] = {} unless body[x['description']]
                body[x['description']]['start'] = x['start'] unless body[x['description']]['start']
                body[x['description']]['duration'] = 0 unless body[x['description']]['duration']
                body[x['description']]['duration'] += x['duration'].to_i
            end

            body.each do |x, y|
                dur = y['duration'].quo(60 * 60)
                printf "- %s (%.1fh)\n", x, dur
            end
        end

        private
        def _load_config(config_file)
            unless File.exist?(config_file) then
                init()
            end
            return YAML.load_file(config_file)
        end
    end
end

設定ファイルの処理とレポート作成のための時間の処理、取得した情報の出力処理などを記載しています。即席で作った臭いがプンプンしますが、そこはこらえてください。今後少しずつリファクタリングしていきますから。対話的データ取得の部分は何か良いモジュールを使いたいですね。

次にAPIアクセス部分です。

lib/operating_report/tracker/api/toggl.rb

# coding: utf-8
require 'uri'
require 'net/http'
require 'openssl'
require 'json'

module OperatingReport
    module Tracker
        module Api
            class Toggl
                def initialize(args)
                    @token = args['token']
                end

                def get_time_entries(start_date, end_date)
                    return _fetch_via_api(
                        'time_entries', {
                            'start_date' => start_date.round(0).iso8601(0),
                            'end_date' => end_date.round(0).iso8601(0),
                        }
                    )
                end

                private
                def _fetch_via_api(path, queries)
                    @base = 'https://www.toggl.com'
                    uri = "#{@base}/api/v8/#{path}"
                    uri += '?' + URI.encode_www_form(queries) if queries
                    uri = URI.parse(uri)
                    response = _fetch(uri, 10)
                    return JSON.parse(response.body)
                end

                def _fetch(uri, limit = 10)
                    raise ArgumentError, 'HTTP redirect too deep' if limit == 0

                    request = Net::HTTP::Get.new(uri.request_uri)
                    request.basic_auth @token, 'api_token'

                    http = Net::HTTP.new(uri.host, uri.port)
                    http.use_ssl = true
                    http.verify_mode = OpenSSL::SSL::VERIFY_NONE

                    response = nil
                    http.start do |h|
                        response = h.request(request)
                    end

                    case response
                    when Net::HTTPSuccess
                        response
                    when Net::HTTPRedirection
                        _fetch(URI.parse(@base + response['location']), limit - 1)
                    else
                        response.value
                    end
                end
            end
        end
    end
end

手習いとして書いたのものですのでいろいろなところを大目に見てください。とは言え、Redirectがあった場合にも地味に対応しています。

それでは実行してみましょう。gemをインストールした状態ではなく、 git cloneしてきた状態を想定してます。トラッキングデータを取得する前に、設定ファイルを作成します。とは言え、現在はAPI Tokenを保存するだけです。

$ bundle exec ./bin/operating_report init
Toggl API Token: 

対話形式で入力します。成功すると $HOME/.report というファイルが生成されていると思います。汎用的すぎる名前ですね。もうちょっと考えるべきでした。

さて、やっとデータの取得を行います。

$ bundle exec ./bin/operating_report create daily
- 会議 (0.5h)
- 機能1の実装 (4.2h)
- 打ち合わせ (1.2h)
- デイリースクラム (0.2h)
- 機能2の実装(1.5h)
- 機能3の設計 (2.5h)

やったね!これで、本日分の作業内容が、作業時間を含めてリストされました。Markdownのリストになっているので、これをQiitaのページにコピー&ペーストして日報の骨組みは完成です。あとは細かく肉付けしていきましょう。

公開

今回のプログラムはGitHubで公開しています。

少しずつ修正/機能追加していく予定ですので乞うご期待です。何?テストが書いてない?今日はTDDの話じゃなくてBDDの話なので、悪しからず。年末に改修するときに書きます。

終わりに

実装してみると、日報全体を生成したいと思ったり、カテゴリー/タグを駆使したいと思ったり、週報も生成したいと思ったりと要望がどんどん膨らんでくるのですが、時間の都合で今回はここまで。また折を見て開発を進めたいと思います。

今回の私の収穫としては下記のような点でしょうか?

  • Rubyの基本的な書き方がわかった。
  • Thorというモジュールがコマンドラインツールの作成に使えることがわかった。
  • Net::HTTPは基本的に使えることがわかった。
  • Togglというサービスが作業時間のトラッキングに使えることを伝えられた。
  • Togglに作業時間をトラッキングしておくといいことがあると伝えられた。

できれば Qiita API を使って下書きを投稿するところくらいまではやりたいのですが、Qiitaテンプレートの独自タグを展開する方法が見つかりませんでした。ご存知の方がいらっしゃいましたら、Twitterの私のアカウントへメンションを飛ばしてください。

それからもう1つ。BDDはお勧めです。BDDのBがBehaviorであろうと、Blogであろうと、Beerであろうとね。