時間が無い、時間が無いと毎日うわ言のようにつぶやいている @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であろうとね。