■ Ruby でカレンダー

コマンドライン上で実行できるカレンダーを作成しました。 休日として祝日もサポートしていますので、日常的なカレンダーとして使用できると思います。 土曜日、日曜日、祝日を強調表示します。 祝日名も表示します。 UNIX 上の cal コマンドに不満がある方はどうぞ。 まあ、いまは便利なカレンダーコマンドが多数ありますけど、そのうちの一つとして...

私は Z shell 上で次のように alias してしまっています。 便利ですから。

alias cal=cal.rb

■ 使い方

引数がない場合は、当年当月です。 引数として、「1970 〜 2037」までは年、「1 〜 12」までは月として扱います。 年と月の指定の順番はどちらでも OK です。

$ cal.rb
     April 1999     
Su Mo Tu We Th Fr Sa
             1  2  3 
 4  5  6  7  8  9 10 
11 12 13 14 15 16 17 
18 19 20 21 22 23 24 
25 26 27 28 29 30    29:緑の日

$ cal.rb 2000 1
    January 2000    
Su Mo Tu We Th Fr Sa
                   1 1:元旦
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15 10:成人の日
16 17 18 19 20 21 22 
23 24 25 26 27 28 29 
30 31

■ ソースコード

Web ブラウザでの表示上は Ruby のスクリプトですが、HTML の特殊文字をエスケープしています。 テキストとしてセーブしてご利用ください。

#! /usr/local/bin/ruby -Ke
# -*- mode:ruby; coding:euc-jp -*-
# /home/tetsu/src/ruby/time/cal.rb
# Created: May 07,1998 Thursday 21:34:45
# Author: tetsu(WATANABE Tetsuya)
# $Id: cal.rb,v 1.21 2007/12/22 14:16:47 tetsu Exp tetsu $
# usage: 月 年 または 年 月 どちらも省略可

class TTerminfo
  def initialize
    if ENV['TERM'] == 'dumb' || ENV['TERM'] == 'emacs'
      @sgr0 = ''
      @smso = ''
      @bold = ''
      @home = ''
      @clear = ''
      return
    end
    pipe = 
      begin
        IO.popen('infocmp', 'r')
      rescue
      end or
      begin
        IO.popen('untic', 'r')
      rescue
      end

    @sgr0 = "\C-[[m"
    @smso = "\C-[[0;7m"
    @bold = "\C-[[1m"
    @home = "\C-[[H"
    @clear = "\C-[[H\C-[[2J"

    foo = pipe.gets             # drop header
    foo = pipe.read
    pipe.close

    foo.split(/[\s,]+/).each do |bar|
      bar.sub!(/\$<\d+>$/, '')
      case bar
      when /^sgr0/
        @sgr0 = bar.split('=')[1].sub(/\\\E/, "\C-[").sub(/(\\[0-7]{3})/) do $1[1,3].oct.chr end
      when /^smso/
        @smso = bar.split('=')[1].sub(/\\\E/, "\C-[")
      when /^home/
        @home = bar.split('=')[1].sub(/\\\E/, "\C-[")
      when /^bold/
        @bold = bar.split('=')[1].sub(/\\\E/, "\C-[")
      when /^clear/
        @clear = bar.split('=')[1].gsub(/\\\E/, "\C-[")
      end
    end
  end
  attr :sgr0
  attr :smso
  attr :bold
  attr :home
  attr :clear
end

require 'tcal'

# mon_arr 0..11

year = 0
month = 0

ARGV.each do |arg|
  foo = arg.to_i
  if foo >= 1 and foo <= 12
    month = foo
  elsif foo >= 1903 and foo <= 2037
    year = foo 
  end
end

# 週の文字列
# 月の配列
# 12 か月分の配列

term = TTerminfo.new
cal = TCalendar.new(year, month)
print cal.header

holiday_name = []
last_wday = 0

1.upto(31) do |d|
  wday = cal.wday(d)
  break unless wday
  last_wday = wday

  status = cal.status(d)

  if (d == 1)
    print '   ' * wday
  elsif wday == 0
    puts holiday_name.compact.join(', ')
    holiday_name = []
  end

  if cal.today?(d)
    print term.bold
  end

  case status
  when 'weekend'
    printf('%s%2d%s', term.smso, d, term.sgr0)
  when 'holiday'
    printf('%s%2d%s', term.smso, d, term.sgr0)
    holiday_name.push(d.to_s + ':' + (cal.holiday_name[d] || '振替休日'))
  when 'workday'
    printf('%2d', d)
  end

  if cal.today?(d)
    print term.sgr0
  end

  print ' '
end

if holiday_name.size > 0
  print '   ' * (6 - last_wday)
  puts holiday_name.compact.join(', ')
else
  puts
end

クラスライブラリです。 環境変数 RUBYLIB の通っているところなどに入れて置いてください。 漢字コードは EUC-JP を想定しています。

#! /usr/local/bin/ruby -Ke
# -*- mode:ruby; coding:euc-jp -*-
# /home/tetsu/src/ruby/class/tcal.rb
# created: September 24,2005 Saturday 13:38:32
# author: tetsu(WATANABE Tetsuya)
# $Id: tcal.rb,v 1.9 2008/10/22 14:25:13 tetsu Exp $
# usage:
# 振替休日 http://ja.wikipedia.org/wiki/%E6%8C%AF%E6%9B%BF%E4%BC%91%E6%97%A5

class TCalendar
  def initialize(year = 0, month = 0)
    @now = Time.now
    @year = if year == 0 then @now.year else year end
    @month = if month == 0 then @now.month else month end
    @holiday = []
    @holiday_name = []
    @mday_arr = []
    1.upto(31) do |d|
      begin
        @mday_arr[d] = Time.local(@year, @month, d, 0, 0, 0)
        if d > 28 and @mday_arr[d].month != @month
          @mday_arr[d] = nil
        end
      rescue ArgumentError
        @mday_arr[d] = nil
      end
      @holiday[d] = false
    end
    month_name = @mday_arr[1].strftime('%b')

# このデータ形式は次のようになっています。
# 月名<space>日付<space>有効年<space>コメント
# 行の先頭の「#」以降はコメント
# 木村さん感謝!
# HM2 Happy Monday(2nd monday)
# HM3 Happy Monday(3rd monday)

    holiday_str = '
Jan 1       0         元旦
Jan 15      -1999     成人の日
Jan HM2     2000-     成人の日
Feb 11      0         建国記念の日
Mar SHUNBUN 0         春分の日
Apr 29      -1988     天皇誕生日
Apr 29      1989-2006 みどりの日
Apr 29      2007-     昭和の日
May 3       0         憲法記念日
# May 4       1986-2006 国民の休日
May 4       2007-     みどりの日
May 5       0         こどもの日
Jul 20      1996-2002 海の日
Jul HM3     2003-     海の日
Sep 15      -2002     敬老の日
Sep HM3     2003-     敬老の日
Sep SYUBUN  0         秋分の日
Oct 10      -1999     体育の日
Oct HM2     2000-     体育の日
Nov 3       0         文化の日
Nov 12      2009      天皇即位20年
Nov 23      0         勤労感謝の日
Dec 23      1989-     天皇誕生日
'

    holiday_str.split(/\n/).each do |l|
      next if l == '' or l =~ /^\#/
      l.sub!(/\#.*$/, '')
      m, d, y, c = l.split(/\s+/, 4)

      if y != '0'
        if y[0,1] == '-'
          next if @year > y[1,4].to_i
        elsif y[-1,1] == '-'
          next if @year < y[0,4].to_i
        elsif y[4,1] == '-'
          next if @year < y[0,4].to_i || @year > y[5,4].to_i
        else
          next if @year != y.to_i
        end
      end

      if month_name == m
        case d
        when 'SHUNBUN'
          d = syunbun(@year).to_s
        when 'SYUBUN'
          d = syubun(@year).to_s
        when 'HM2'
          d = nMonday(2).to_s
        when 'HM3'
          d = nMonday(3).to_s
        end
        @holiday[d.to_i] = true
        @holiday_name[d.to_i] = c
      end
    end

    if @year >= 1986
      i = 0
      while i < 31 - 2
        # 「国民の休日」判定
        # 当日が祝日       次の日が祝日でない           日曜日でない                   次の次の日が祝日
        if @holiday[i] and @holiday[i + 1] == false and @mday_arr[i + 1].wday != 0 and @holiday[i + 2] 
          @holiday[i + 1] = true
          @holiday_name[i + 1] = '国民の休日'
          i += 1                # skip
        end
        i += 1
      end
    end
  end
  attr_reader :holiday_name, :year, :month

  def nMonday(n)
    count = 0
    @mday_arr.each_index do |d|
      next if d < 1
      count += 1 if @mday_arr[d].wday == 1
      return d if count == n
    end
  end

  def today?(mday)
    @now.year == @year and @now.month == @month and @now.mday == mday
  end

  def wday(mday)
    return nil unless @mday_arr[mday]
    @mday_arr[mday].wday
  end

  # 2005 からの振替休日 連続する祝日が日曜日にかかると祝日の終りの次の平日を振替休日に
  def furikae2005(mday, wday)
    year = @mday_arr[mday].year
    if year < 2005
      return 'workday'
    end
    if mday <= wday
      return 'workday'
    end
    (1..wday).each do |i|
      if @holiday[mday - i] == false
        return 'workday'
      end
    end
    'holiday'
  end

  def status(mday)
    return nil unless @mday_arr[mday]
    return 'holiday' if @holiday[mday]
    wday = @mday_arr[mday].wday

    case wday
    when 1
      if @mday_arr[mday].year >= 1973 and mday > 1 and @holiday[mday - 1]
        'holiday'
      else
        'workday'
      end
    when 2..5
      furikae2005(mday, wday)
    when 0, 6
      'weekend'
    end
  end

  def header
    msg = @mday_arr[1].strftime('%B %Y').center(20)
    msg + "\n" + 'Su Mo Tu We Th Fr Sa' + "\n"
  end

#| From: hajima atmark crimson.gen.u-tokyo.ac.jp (Ryoichi Hajima)
#| Newsgroups: fj.questions.misc
#| Subject: Re: vernal/autumnal equinox
#| Message-ID: <HAJIMA.94Jul13161542@tanelorn.gen.u-tokyo.ac.jp>
#| Date: 13 Jul 94 07:15:42 GMT
#|
#| 春分日 (31y+2213)/128-y/4+y/100    (1851年-1999年通用)
#|     (31y+2089)/128-y/4+y/100    (2000年-2150年通用)
#|
#| 秋分日 (31y+2525)/128-y/4+y/100    (1851年-1999年通用)
#|     (31y+2395)/128-y/4+y/100    (2000年-2150年通用)

  def syunbun(year)
    if year > 2150
      STDERR.print "over year's: #{year}\n"  #'
      exit 1
    end
    v = if year < 2000 then 2213 else 2089 end
    (31 * year + v)/128 - year/4 + year/100
  end

  def syubun(year)
    if year > 2150
      STDERR.print "over year's: #{year}\n" #'
      exit 1
    end
    v = if year < 2000 then 2525 else 2395 end
    (31 * year + v)/128 - year/4 + year/100
  end
end

if $0 == __FILE__
  puts 'テスト'
  
  cal = TCalendar.new(2007, 9)
  print cal.header
  p cal
end

■ 解説

「年」として有効なものは、UNIX としての一般的年の 1970 〜 2037 までです。

ターミナルタイプについては、まだちょっと不十分です。 Linux での infocmp(1) というコマンドか、HP-UX での untic(1) というコマンドに依存してます。 コマンドがない場合には、vt100 系のターミナルの制御コードを使います(たぶん Win 系でも動くと思います)。 terminfo(5) 形式の出力からターミナル情報を取りだしています。

祝日はとても簡単な形で実現しています。 定義中「XX」の場合には、不定のため計算します。 この計算については、同じ月に不定の祝日が 2 日もないと決めつけていますので、もし対応できないような場合になれば修正が必要です。

休日の形式は単純なので、個人の休日などを加えるのは容易と思います。 ただ、「年」の指定が、当年だけというフォーマットは、現状ではありません。

ちょっと無駄なことをしています。 出力する「月」について、毎日の Time クラスのオブジェクトを生成しています。 曜日の計算を自分で行えば、こういうことはしなくてすむとは思いますが、便利なのでついつい。

引数の「年」と「月」には、指定のための順番がありません。 好きな順番で指定してください。 これは、UNIX での cal コマンドの指定がどうも私には合わないためです。

■ 履歴

tcal.rb 1.9 2008/10/22

2009/11/12 天皇即位20年の祝日対応

1.21 2007/12/22

coding:euc-jp を追加しました。

1.20 2007/12/3

2005 年から振替休日の扱いが変更されています。 連続する祝日が日曜日にかかると次の平日が振替休日になるそうです。 日曜日と月曜日が祝日の場合火曜日が振替休日になります。

1.19 2005/09/24

クラスライブラリを別ファイルにしました。

1.17 2005/09/23

「みどりの日」を「緑の日」にしていたので修正です。

1.16 2005/09/23

2007 年からの改訂対応です。 4/29 が「昭和の日」。 5/4 が「みどりの日」。

「国民の休日」の扱いを、プログラム的に追加。 祝日と祝日にはさまれた平日は、「国民の休日」です。

1.15 2004/12/03

1/1 が日曜日のときに「振り替え休日」扱いになっていなかったのを修正。

1.14 2004/08/13

確認用に co してしまったのでした。

1.13 2003/11/01

Tera Term 対応。 vt100 で $<2> が入ってきたので外す。

1.12 2003/02/03

EUC を前提に -Ke を指定する。 処理系が SJIS の場合には -Ks にしてください。

1.11 2002/07/29

2002-07-29 対応

1.10 2002/4/14

祝日名の表示をサポート。 最近、HAPPY MONDAY が増えて、何の日かわからなくなってきたから。

1.9 2001/9/5

bug fix です。 秋分の日のつづりを間違っていました。

1.8 2001/8/14

ruby 1.7.1 2001-08-06 対応です。

1.7 2001/6/15

新しい happy monday は 2003 年からでした。

1.6 2001/5/22

海の日と敬老の日が happy monday になるそうです。 第三月曜日とのことでした。

■ 参考 URL


渡辺哲也(WATANABE Tetsuya): Tetsuya.WATANABE atmark nifty.com