Goで日報をパースして稼働時間の集計をする
これはGo Advent Calendar 2022の9日目の記事です。
副業やフリーランスなどでいくつものプロジェクトを同時並行でお手伝いしていると、今どのプロジェクトに対してどれくらい稼働を割いたのか把握するのが難しくなってきます。
これに関して、Google Calendar などに勤怠情報を記録して後から GAS で集計するなどのアイデアがあります[1]。
自分は普段 Private wiki (個人 esa.io) に色々なメモを残しており、特にTemplate 機能を使って稼働の日報みたいなメモをよく書いています。
Google Calendar で管理する方法も良いのですが、その日に何をしたのかを記しているメモがあるので、そっちに稼働時間を記入し集計できないかなと思いました。
実装
世の中には Markdown にメタデータを付与できるフォーマットがいくつか存在します。
今回は zenn にも使われている、Frontmatter を使って Markdown 形式の日報に稼働時間を記録したメタデータを記し、Go でそれを集計するツールを作るアイデアの紹介をします。
Frontmatter のパースにはadrg/frontmatter を使います。
go get github.com/adrg/frontmatter
今回は以下のようなメモに YAML 形式で Frontmatter が書かれているとして、それをパースしてみましょう。
(大体普段こんな感じで書いてます。)
---
works:
- start: 09:00
end: 12:00
memo: アイデアを考える
- start: 13:00
end: 15:00
memo: 検証・実装
---
## タスク整理
- [x] アイデアを考える
- [x] 書く
- [ ] 公開する
- [ ] Next Action 整理
## アイデアを考える
普段メモと一緒に稼働時間を埋め込んで集計している。
これを紹介するのはどうだろうか?
## 書く
zenn に頑張って書いた。
PR: https://github.com/...
## Next Action
* Tweetする
パースには frontmatter.Parse
を利用します。
io.Reader
と割り当てたい変数のアドレスを渡せば良いです。
type Matter struct {
Works []Work `yaml:"works"`
}
type Work struct {
Start PeriodTime `yaml:"start"`
End PeriodTime `yaml:"end"`
Memo string `yaml:"memo"`
}
type PeriodTime struct {
Hour int
Minute int
}
func main() {
var matter Matter
frontmatter.Parse(strings.NewReader("<Markdown形式の本文>", &matter))
}
最終的に CSV とかにまとめたいので、時間の表記は 15:04
のようにしたいです。
そのために時間と分を保持する PeriodTime
を定義したので、これを Unmarshal するメソッドを作っておきましょう。
ついでに出力用の String()
メソッドも用意しておきます。
func (t *PeriodTime) UnmarshalYAML(unmarshal func(interface{}) error) error {
var buf string
if err := unmarshal(&buf); err != nil {
return err
}
tt, err := time.Parse("15:04", buf)
if err != nil {
return err
}
t.Hour = tt.Hour()
t.Minute = tt.Minute()
return nil
}
func (t *PeriodTime) String() string {
return fmt.Sprintf("%02d:%02d", t.Hour, t.Minute)
}
あとはデータをまとめて CSV に出力できるようにすれば、それっぽいものが出来上がります。
自分は {ProjectName}/{Year}/{Month}/{Day}
みたいなパスでファイルを作っているので、そこから Project の名前やメモの日付を特定しています。
type Record struct {
ProjectName string
Date time.Time
Link string
Matter Matter
}
func (r *Record) ToCSVLines() [][]string {
var lines [][]string
for _, work := range r.Matter.Works {
lines = append(lines, []string{
r.ProjectName,
r.Date.Format("2006/01/02"),
work.Start.String(),
work.End.String(),
work.Memo,
r.Link,
})
}
return lines
}
func main() {
...
record := Record{
ProjectName: "Advent Calendar",
Date: time.Date(2022, 12, 9, 0, 0, 0, 0, time.FixedZone("Asia/Tokyo", 9*60*60)),
Link: "https://example.com/notes/AdventCalendar/2022/12/09",
Matter: matter,
}
csvLines := record.ToCSVLines()
w := csv.NewWriter(os.Stdout)
header := []string{"ProjectName", "Date", "Start", "End", "Memo", "Link"}
w.WriteAll(append([][]string{header}, csvLines...))
}
出力結果例:
ProjectName | Date | Start | End | Memo | Link |
---|---|---|---|---|---|
Advent Calendar | 2022/12/09 | 09:00 | 12:00 | アイデアを考える | https://example.com/notes/AdventCalendar/2022/12/09 |
Advent Calendar | 2022/12/09 | 13:00 | 15:00 | 検証・実装 | https://example.com/notes/AdventCalendar/2022/12/09 |
割と単純なアイデアですが、メモ魔の自分的には普段のアウトプットからそのまま稼働のサマリなんかを作れてしまうので結構重宝しています。
スプレッドシートに出力してもう少しリッチにすれば稼働の残り時間の確認や、そこからのスケジュールの見直しなんかもできますしね。
あとはこれを GitHub Actions で定期実行したり、集計ロジックを凝ったりしてどんどん盆栽化します。
今回使ったサンプルコードの全体像はこちらから: https://gist.github.com/taxio/3ab5d9b1fb01b2c5503d7afb6d21caea
Discussion