zshで祝日取得

# zshで祝日計算

## 20140405追記

 * 結局、Appleのicsを採用した
  * GoogleCalendarだと銀行休業日なんてものがあっていいかなと思った
  * クリスマスまで祝日になってて、日本では休日じゃないですしおすしってなった

## 概要

* Perl で何回か書いたことあったけど、シェルスクリプトで年末くらいにトライした
* きっかけは第N曜日って cal コマンドから簡単にとれるんじゃないか？
 - cal して、tail して cut するだけだよね？
 - [dateコマンド色々](https://gist.github.com/s-age/9681455)
* よし！作れる(確信)

## 思い出す

 * パースする条件ってなんだっけ？ということで

```
$ curl -s 'http://www.mozilla.org/projects/calendar/caldata/JapanHolidays.ics' | cut -d: -f1 | sort | uniq
 Day
BEGIN
CATEGORIES
CREATED
DTEND;VALUE=DATE;TZID=/mozilla.org/20070129_1/Asia/Tokyo
DTSTAMP
DTSTART
DTSTART;VALUE=DATE;TZID=/mozilla.org/20070129_1/Asia/Tokyo
END
LAST-MODIFIED
PRODID
RRULE
SUMMARY
TZID
TZNAME
TZOFFSETFROM
TZOFFSETTO
UID
VERSION
X-LIC-LOCATION
```

### おkおk、とりあえず、祝日を計算するだけなら次の2つをだけみればいい
 * DTSTART
  * 祝日、もしくは繰り返し条件がある場合の最初の時間
 * RULE
  * 繰り返し条件、後述

## RRULE をパースするのでパターン分け

 * おもむろにRRULEだけ抽出

```
 curl -s 'http://www.mozilla.org/projects/calendar/caldata/JapanHolidays.ics' | grep RRULE
$ curl -s 'http://www.mozilla.org/projects/calendar/caldata/JapanHolidays.ics' | grep RRULE | sort | uniq
RRULE:FREQ=MONTHLY;INTERVAL=12;BYDAY=2MO
RRULE:FREQ=MONTHLY;INTERVAL=12;BYDAY=3MO
RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=4
RRULE:FREQ=YEARLY;COUNT=4;INTERVAL=4
RRULE:FREQ=YEARLY;INTERVAL=1
```

### なし
  - これは楽、無条件で DTSTART が実行日(大体、振替休日)
 
### RRULE:FREQ=MONTHLY;INTERVAL=12;BYDAY=2MO
### RRULE:FREQ=MONTHLY;INTERVAL=12;BYDAY=3MO
  - DTSTARTの月の第N曜日が祝日

### RRULE:FREQ=YEARLY;COUNT=4;INTERVAL=4
### RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=4
  - 4年間隔でCOUNT回繰り返す祝日

### RRULE:FREQ=YEARLY;INTERVAL=1
  - 毎年、同じ月日が祝日

## 抽出条件を整理

### RRULEがないケース
  - 年が一致していればDTSTARTの月日を返す

### ``:`` 以下を ``;`` で切った結果、要素が2つ
  - DTSTARTの月日を返す

### ``:`` 以下を ``;`` で切った結果、要素が3つ
  * ``YEARLY`` のケース
   * ``COUNT`` 回 ``INTERBAL`` を加算し続けて年が一致すれば 
``DTSTART`` の月日を返す
  * ``MONTHLY`` のケース
   * ``DTSTART``の月の第N曜日を返す

## 引数

 * 年は必要
 * 月もあってもいいかもくらい(grepすれば対象月だけとれるし)

## 実装

```
#!/usr/bin/zsh

readonly YEAR=$1
typeset -A DoW
DoW=('SU' 1 'MO' 2 'TU' 3 'WE' 4 'TH' 5 'FR' 6 'SA' 7)

set -ue

function get_Nth_wday () {
    integer Nth="$1"
    local dow=${DoW[$2]}
    integer mm="$3"
    echo "$(printf '%02d' $(ncal "${mm}" "${YEAR}" | tail -n+2 | tail -n+"$dow" | head -n1 | sed 's/  */ /g' | cut -d' ' -f$(expr 1 + ${Nth})))"
}

function parse_ics () {
    integer dtstart=$1
    typeset -a rrule
    rrule=($(echo "$2" | sed 's/;/ /g'))

    if [ "${#rrule}" -eq 2 ]; then
        echo "${dtstart}" | sed 's/^[0-9]\{4\}//'
    elif [[ "${rrule}" =~ 'YEARLY' ]] ; then
        [[ "${rrule}" =~ 'COUNT=([0-9]*)' ]]
        integer count=${match[1]}

        [[ "${rrule}" =~ 'INTERVAL=([0-9]*)' ]]
        integer interval=${match[1]}

        [[ "${dtstart}" =~ '(....)(....)' ]]
        integer yyyy=${match[1]}
        local mmdd="${match[2]}"

        for ((i=0; i <= $count; i += 1))
        do
            yyyy=$(expr "${yyyy}" + "$interval")
            if [ $yyyy -eq $YEAR ] ; then
                echo "${dtstart}" | sed 's/^....//'
            fi
        done
    else
        [[ "${rrule}" =~ 'BYDAY=(.)(..)' ]]
        integer Nth="${match[1]}"
        local dow="${match[2]}"

        [[ "${dtstart}" =~ '....(..)..' ]]
        local mm=$match[1]
        echo "${mm}$(get_Nth_wday ${Nth} ${dow} ${mm})"
    fi
}

() {
    typeset -A holiday
    local holidays

    wget -q http://ical.mac.com/ical/Japanese32Holidays.ics -O - | \
    while read result
    do :
        if [[ "${result}" =~ "^BEGIN:VEVENT" ]] ; then
            :
            holiday=(
                rrule ''
                dtstart ''
                summary ''
            )
        elif [[ "${result}" =~ "^RRULE:(.*)" ]] ; then
            holiday[rrule]="${match[1]}"
        elif [[ "${result}" =~ "^DTSTART;.*:([0-9]*)" ]] ; then
            holiday[dtstart]="${match[1]}"
        elif [[ "${result}" =~ "^SUMMARY" ]] ; then
            holiday[summary]="$(echo ${result} | cut -d: -f2 | cut -d' ' -f1)"
        elif [[ "${result}" =~ "^END:VEVENT" ]] ; then
            if [[ "${holiday[dtstart]}" = '' ]] ; then
                continue
            elif [[ "${holiday[rrule]}" = '' ]] ; then
                if [[ "${holiday[dtstart]}" =~ "^${YEAR}" ]] ; then
                    [[ "${holiday[dtstart]}" =~ '....(....)' ]]
                    holidays="$holidays\n${YEAR}${match[1]}: ${holiday[summary]}"
                fi
            else
                local mmdd="$(parse_ics "${holiday[dtstart]}" "${holiday[rrule]}")"

                if [[ "${mmdd}" != '' ]] ; then
                    holidays="$holidays\n${YEAR}${mmdd}: ${holiday[summary]}"
                fi
            fi
        fi
    done

    echo "${holidays}" | sort
}
```

## 実行結果(Apple)

 * 銀行休業日とりたかったなー

```
$ zsh holidays.sh 2015

20150101: 元日
20150112: 成人の日
20150211: 建国記念の日
20150321: 春分の日
20150429: 昭和の日
20150503: 憲法記念日
20150504: みどりの日
20150505: こどもの日
20150506: 振替休日
20150720: 海の日
20150921: 敬老の日
20150922: 国民の休日
20150923: 秋分の日
20151012: 体育の日
20151103: 文化の日
20151123: 勤労感謝の日
20151223: 天皇誕生日

$ zsh holidays.sh 2014

20140101: 元日
20140113: 成人の日
20140211: 建国記念の日
20140321: 春分の日
20140429: 昭和の日
20140503: 憲法記念日
20140504: みどりの日
20140505: こどもの日
20140506: 振替休日
20140721: 海の日
20140915: 敬老の日
20140923: 秋分の日
20141013: 体育の日
20141103: 文化の日
20141123: 勤労感謝の日
20141124: 振替休日
20141223: 天皇誕生日
```

## 実行結果(GoogleCalendar)

 * 年を食わせてあげる

```
$ zsh holidays.sh 2014

20140101: 元日
20140102: 銀行休業日
20140103: 銀行休業日
20140113: 成人の日
20140211: 建国記念の日
20140321: 春分の日
20140429: 昭和の日
20140503: 憲法記念日
20140504: みどりの日
20140505: こどもの日
20140506: みどりの日
20140721: 海の日
20140915: 敬老の日
20140923: 秋分の日
20141013: 体育の日
20141103: 文化の日
20141123: 勤労感謝の日
20141124: 勤労感謝の日
20141223: 天皇誕生日
20141225: クリスマス
20141231: 大晦日
$ zsh holidays.sh 2015

20150101: 元日
20150102: 銀行休業日
20150103: 銀行休業日
20150112: 成人の日
20150211: 建国記念の日
20150321: 春分の日
20150429: 昭和の日
20150503: 憲法記念日
20150504: みどりの日
20150505: こどもの日
20150506: 憲法記念日
20150720: 海の日
20150921: 敬老の日
20150922: 国民の休日
20150923: 秋分の日
20151012: 体育の日
20151103: 文化の日
20151123: 勤労感謝の日
20151223: 天皇誕生日
20151225: クリスマス
```

## 実行環境
```
$ cat /etc/debian_version
7.1
```