📝

Goで日報をパースして稼働時間の集計をする

2022/12/08に公開

これは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

脚注
  1. https://qiita.com/umi_mori/items/4a9cc9e291d92101fe4a ↩︎

GitHubで編集を提案

Discussion