GNU date が難しかった(小学生並みの感想)

2年ぶりのブログ。
タイトルや経緯等はアレな感じがするものの、至極全うな(はずの)記事になるはず。

まとめ(結論とは異なる)

個人のスケジュール・タスク管理用にRedmineをお持ちで、かつ、Linuxを利用できる環境の皆様、
以下のツールを使ってみてやってください。

redmine_daily_ticket_generator

本ブログの5年前の予告が放置されっぱなしであるが、「余り使わないので忘れてしまってどうしようもない」状況は防げるかもしれない。
その状況になってしまったら立て直すのは大変だが、他タスクをもう少し整頓したら多分やる気は出るでしょう。

経緯

おとボクのファン活動やってたころからお世話になっている いつき さんに、
個人で起こされているRedmineのプロジェクトを一つお借りして、プライベート関係のタスク管理を実施している(※)。
しかしながら、プライベート関係のタスクはデイリー・ウィークリー・マンスリー等、周期的に発生するタスクが多く、毎回登録していたのではきりがない。
また、それとは別途思いついたネタなどを書き留めるネタ帳としてのタスク管理と項目が被る。
そんなわけで、周期発生するタスクを自動登録できれば、と考えた。

※本当はラズパイを使って自分でRedmine立てたかったんですが、設定が複雑であったため諦めた経緯あり。

一方で、以下の通り、Linux環境は余っている。

  • Netgear ReadyNAS 316
    Debian系の組み込みLinuxが搭載されたファイルサーバ。
    2015年末に購入したはいいものの、ちょっとした不具合をサポートに問い合わせたが三ヶ月かかっても解決できなかった(上記いつきさんと二人で調査し自己解決した)曰く付きの品。
    今はファイルサーバ兼ちょっとしたcron系仕事をしてもらっている。
  • Raspberry Pi
    先日の記事に出てきた超小型PC……の2代目。購入した用事を別の方法で実装したため、放置状態。
  • 自作Linuxサーバ
    知り合いから譲り受けた、消費電力を下げるチューニングの自作PC。動かす用事がなく放置状態。

そこで、いつきさんのRedmineにREST APIでアクセスして、周期発生するタスクのチケットを自動的に発行しようと試みたもの。

というわけで、本項の前提は以下の通り。

  • プライベートのタスクをRedmineで管理していること。このRedmineはREST APIでアクセス可能なこと。
  • Rubyの動くLinux環境を持っていること。デイリーないしはそれに準ずる頻度で起動すること。

前提

redmine_daily_ticket_generator の設定は、以下のような形式で書かれる。

<?xml version=”1.0″ encoding=”UTF-8″?>
<issues>
<issue>
<project id=”13″/>
<author id=”11″/>
<assigned_to id=”11″/>
<subject>ごみ捨て:資源ゴミ(毎週月曜日)</subject>
<description></description>
<start_date>monday</start_date>
<due_date>monday</due_date>
</issue>
<issue>
<project id=”13″/>
<author id=”11″/>
<assigned_to id=”11″/>
<subject>グラブル:星晶獣討伐戦HARD(毎日)</subject>
<description></description>
<start_date>tomorrow</start_date>
<due_date>tomorrow</due_date>
</issue>
<issue>
<project id=”13″/>
<author id=”11″/>
<assigned_to id=”11″/>
<subject>グラブル:武勲の輝きムーン交換(毎月)</subject>
<description></description>
<start_date>`date ‘+%Y-%m-01’` 1 months</start_date>
<due_date>`date ‘+%Y-%m-01’` 2 months 1 days ago</due_date>
</issue>
<issue>
<project id=”13″/>
<author id=”11″/>
<assigned_to id=”11″/>
<subject>散髪(切り終わってから8週間後の土曜~9or10週間後の日曜)</subject>
<description></description>
<start_date>saturday 8 week</start_date>
<due_date>sunday 10 week</due_date>
</issue>
</issues>

グラブルについて語りたいのは省略し、本設定のポイントは以下2カ所。

`date '+%Y-%m-01'` 1 months
`date '+%Y-%m-01'` 2 months 1 days ago

新しいチケットを切る場合、開始日と終了日は空欄またはYYYY-MM-DDの形式である必要がある。固定の日付を入れればいい場合は、単にその文字列を入れてあげればよいだけだ。
しかし、周期タスクであるからには、適切な開始日と終了日があるはずだ。これを全く入力しないのでは、ToDoリストとしての優先度がつかない。
一方で、毎回毎回カレンダーを眺めながら考えるのも負担である。
そこで、私のプログラムでは上記箇所を、 GNU date のサポートする形式で記載するようにした。つまり、プログラムの中でGNU dateコマンドそのものを、下記の通り実行する。

# date +%F -d "[設定された文字列]"

上記コマンドの戻り値は YYYY-MM-DD 形式になっているので、正しい設定を書いておけば、適切な開始日と終了日を設定できる。

・・・だが、しかし。

man date などを見てみても、 GNU date の形式でどう設定したら思い通りの日付が出てくるのか、よく分からないというのが本音であった。単純なものであればググれば分かるが、複雑なものであるとさっぱりである。

考察

上記、date -d の設定を、以下datestrと呼ぶ。(datestrは時間指定の用語もあるが、今回は考えない)
datestrは、おそらく以下の意味だと思われる。

用語 推測した意味
YYYY-MM-DD 固定日:指定した日
sunday 固定日:今日から見て直近(未来)の日曜日。今日~6日後のいずれか
today 相対日:0日前または0日後
yesterday 相対日:1日前
N day(s) 相対日:N日後
N day(s) ago 相対日:N日前

曜日の指定は、monday,tuesday,…,saturdayもある。
相対日の指定は、week(s),month(s),yearなどもある。

用語の詳細は仕様書 https://www.gnu.org/software/coreutils/manual/html_node/Date-input-formats.html#Date-input-formats に譲るが、曜日の挙動がどうにもうまく行かず、ひねり出したのが上記「推測した意味」の「固定日/相対日」の考え方である。

なお、他に、”xxxx-03-31 1 months”と”xxxx-04-01 1 months”がどちらも”xxxx-05-01″となる問題がよく知られている。
monthsの仕様で、月末の考え方に難しいところがあるらしい。
参考: http://moruho.cocolog-nifty.com/blog/2015/02/gnu-date-c2b0.html

例示と解説

ちょっとバグがあるかもしれないが、解説の穴を見つけていただければバグフィックスできるはずなのでご容赦。

  • 今日
    today
  • ○○日から6日後
    YYYY-MM-DD 6 days
  • 昨日から1週間前
    yesterday 1 weeks ago
  • 次の木曜日
    thursday
  • 今月頭
    `date +%Y-%m-01`
  • 今月末
    `date +%Y-%m-01 1 months` 1 days ago
  • こないだの日曜日
    `date +%u` days ago
  • (月曜始まりで考えて)来週の木曜日
    `date +%u` days ago 1 week 4 days
  • (日曜始まりで考えて)来週の木曜日
    $(expr $(date +%u) % 7) days ago 1 week 4 days
  • 来月の第三月曜日
    $(date +%Y-%m-01 -d ‘1 months’) $(date +%u -d $(date +%Y-%m-01 -d ‘1 months’)) days ago 3 week 1 days

今日、6日後、一週間前は説明略。

今月頭。省略された today の、年月だけ持ってきて、日を1日に書き換える。

今月末。月末が何日か、ということで、前章の参考サイトのとおり「来月頭の1日前」とするのが正しい。

こないだの日曜日。
手順1:月曜日=1、、、土曜日=6、日曜日=7のいずれかを値として取得する。
手順2:(手順1)日前、を指定する。

(月曜始まりで考えて)来週の木曜日
手順1:こないだの日曜日を指定する。月曜日始まりのため、「先週の日曜日」と同義。
手順2:(手順1)の1週間後の4日後を指定する。月曜始まりにとって、「今週の日曜日」の4日後は必ず「来週の木曜日」である。

(日曜始まりで考えて)来週の木曜日
手順1:「こないだの日曜日」を7で剰余をとる(つまり、7だけ0に置き換える)と「今週の日曜日」になる。
手順2:(手順1)の1週間後の4日後を指定する。日曜始まりにとって、「今週の日曜日」の1週間後の4日後は必ず「来週の木曜日」である。
このとき、バッククォート演算子“は入れ子にできないため、$()でコマンドを置換することに注意する。

来月の第三月曜日
手順1:来月頭をYYYY-MM-DD形式で取得する。
手順2:来月頭を、「こないだの日曜日、手順1」と同様に曜日の数字で取得する。
手順3:(手順1)の(手順2)日前、として、来月の第〇日曜日を取得する。
手順4:3週と1日を足して、第三月曜日のできあがり。
参考:http://stackoverflow.com/questions/5655026/date-calculation-using-gnu-date

結論

dateコマンドのパズル感が異常なので、テンプレートが欲しい次第、と思って上記を準備した。
上記を丁寧に追っていただければ、だいたいのパターンには対応できる。。。はず。。。