yucatio@システムエンジニア

趣味で作ったものいろいろ

カレンダー作成問題の解答

前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。元記事では、解答を見ないで解くこととなっていましたが、ある程度自分で考えた後に他の人の解答を見ました。それでもオリジナルなコードになったと思います。

blog.jnito.com

カレンダー作成問題

「たのしいRuby」に載っている、オーソドックスなカレンダー作成問題です。
DateクラスのAPIさえわかれば、あとは基礎的なプログラミング知識だけでコードが書けると思います。 Date クラスを使って、今月の1日と月末の日付と曜日を求め、次のような形式でカレンダーを表示させてください

      April 2013
 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

解答

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|
        if day.month == target_date.month
          print ' %2d' % day.day
        else
          print '   '
        end
      }
      puts
    }
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

解説

ブログに解答を載せるに際して、以下のような方針を立てました。

  • Railsで実装することを前提とする
    • HTMLのテーブルを使用する前提で実装する
    • フォーマットを(あまり)がんばらない

Railsで実装することを前提とする、とは、コンソール出力する用にコードを書くのではなく、Railsでhtmlを(erbなどで)出力する場合にこのように書く、という意味です。 特に今回はHTMLのテーブル形式で出力するとよさそうです。以下のテーブルを出力するイメージで実装を進めます。

f:id:yucatio:20190531085309p:plain

HTMLで出力を考えるとき、フォーマットはCSSで行うため、フォーマットについては最低限にします。

まずはクラスとメソッドを作成します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    puts target_date
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

問題では今月のカレンダーを表示する、となっていましたが、メソッド内部に現在時刻が入ってしまうとバグに気付きにくくデバッグもしづらいので、対象とする日にちは引数で渡すことにします。デフォルトで引数を現在日時にすることにより問題の意図通りに動くようにします。

実行します。本日の日付と引数で渡した日付が表示されました。 this monthの方は現在の日付なので実行ごとに結果が変わります。

--- this month ---
2019-05-29
--- 2013 04 ---
2013-04-24

実装を進めます。まずは月の初めの日付と月の終わりの日付のオブジェクトを作成します。 月初めは、現在の年月の1日なので、Date(target_date.year, target_date.month, 1)で作成します。これをstart_of_monthという変数に格納します。 月終わりは、次の月の1日の、1日前なので、これをそのままプログラムに直します。start_of_monthを使用して、start_of_month.next_month.prev_dayでよさそうです。ここまでの実装は以下になります。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)  # 追加
    end_of_month = start_of_month.next_month.prev_day  # 追加
    puts start_of_month
    puts end_of_month
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。月の初めと終わりの日付が取得できました。

--- this month ---
2019-05-01
2019-05-31
--- 2013 04 ---
2013-04-01
2013-04-30

次に、曜日を求めます。曜日のぶん出力をずらすためです。Date#wdayが使用できます。一旦出力を確認しておきましょう。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    puts start_of_month.wday
    puts end_of_month.wday
  end
end

# 使い方
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。曜日の対応は以下なので、曜日に対応する数値が取れました。

0 1 2 3 4 5 6
--- this month ---
3
5
--- 2013 04 ---
1
2

次に曜日のぶんだけ日付を右にずらします。今回は、"ずらす"というという発想より、カレンダー(テーブル)の最初の日付と最後の日付を求める、という発想で解決します。

カレンダーの左上と右下、この2つは(当月の日付でなければ)空白で出力しますが、ここに該当する前月と次月の日付を求めます。

f:id:yucatio:20190531091237p:plain

カレンダーの左上は、上で求めたstart_of_month.wdayぶんstart_of_monthから過去の日付です。 カレンダーの右下は、end_of_month.wdayから土曜までの日数を足せば良いので、6 - end_of_month.wdaぶん未来の日付がカレンダーの右下の日付になります。ここまでをコードにします。日付を足したり引いたりするにはDate#prev_dayDate#next_dayを使用します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)  # 追加
    cal_end = end_of_month.next_day(6 - end_of_month.wday)  # 追加
    puts cal_start
    puts cal_end
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。カレンダーの左上と右下の日付が出力されました。

--- this month ---
2019-04-28
2019-06-01
--- 2013 04 ---
2013-03-31
2013-05-04

ここまでで事前準備は終了なので、本番の出力部分を実装していきます。まずは April 2013 の部分を出力します。時間を文字列にフォーマットするには、Date#strftime( class Date (Ruby 2.6.0) ) を使用します。Aprilのような英語の月名表示は%B、年表示は%Yで表します。センタリングはString#center( class String (Ruby 2.6.0) ) で行います。ここまでのコードです。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)  # 追加
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。月名と年がセンタリングして表示されました。

--- this month ---
      May 2019       
--- 2013 04 ---
     April 2013      

次に曜日のヘッダーSu Mo Tu We Th Fr Saを表示します。これはそのまま表示すれば良いでしょう。コードです。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'  # 追加
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。曜日名が表示されました。

--- this month ---
      May 2019       
 Su Mo Tu We Th Fr Sa
--- 2013 04 ---
     April 2013      
 Su Mo Tu We Th Fr Sa

日にちを表示していきます。まず、カレンダーの左上の日付からカレンダーの右下の日付までの範囲オブジェクトを作成します。(cal_start..cal_end)です。これを7日ごとに分割します。Enumerable#each_slice( each_slice (Enumerable) - Rubyリファレンス )を使用します。ここまでの実装です。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|  # 追加
      days  # 仮実装
    }
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

ここから各日付を出力します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|  # 追加
        print day.day  # 仮実装
      }
      puts
    }
  end
end
puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

日付をフォーマットしていきます。フォーマットを、数値2桁にし、先頭に空白を入れます。 フォーマットには、String#%を使用します。 https://docs.ruby-lang.org/ja/latest/class/String.html#I_--25

先頭に空白1つ、数値2桁に揃えてフォーマットは、%2dで表します。

ここまでのコードです。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|
         print ' %2d' % day.day  # 変更
      }
      puts
    }
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。列が揃っています。

--- this month ---
      May 2019       
 Su Mo Tu We Th Fr Sa
 28 29 30  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 31  1
--- 2013 04 ---
     April 2013      
 Su Mo Tu We Th Fr Sa
 31  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  1  2  3  4

最後に、当月でない部分の日付は表示しないようにします。当月でない場合は空白3つを表示します。 当月かどうかの判定は、出力しようとしている日付と、引数で与えられた日付の月を比較します。

class Calendar
  def self.print_calendar(target_date = Date.today)
    start_of_month = Date.new(target_date.year, target_date.month, 1)
    end_of_month = start_of_month.next_month.prev_day
    cal_start = start_of_month.prev_day(start_of_month.wday)
    cal_end = end_of_month.next_day(6 - end_of_month.wday)

    # month year
    puts target_date.strftime('%B %Y').center(21)
    # week days header
    puts ' Su Mo Tu We Th Fr Sa'
    # days
    (cal_start..cal_end).each_slice(7) {|days|
      days.each {|day|
        if day.month == target_date.month  # if-else 追加
          print ' %2d' % day.day
        else
          print '   '
        end
      }
      puts
    }
  end
end

puts '--- this month ---'
Calendar.print_calendar
puts '--- 2013 04 ---'
Calendar.print_calendar(Date.new(2013, 4, 24))

実行結果です。問題で求められた形式のカレンダーを作成することができました。

--- this month ---
      May 2019       
 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 31   
--- 2013 04 ---
     April 2013      
 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            

あとがき

最初の日付をずらして空白を入れる実装に迷いましたが、 わりと分かりやすく書けたかと思います。

Railsで実装する時に、

  • 前後の月の日付を表示してほしい(薄い文字で)
  • "今日"をハイライトしてほしい

といった追加の要望にも簡単に応えられる実装にしました。

出題者の伊藤淳一さんの書籍はこちら↓