カレンダー作成問題の解答
前回に引き続き、こちらの記事を見てみて、面白そうな問題がいくつかあったので、プログラミング初心者ではないですが解きました。元記事では、解答を見ないで解くこととなっていましたが、ある程度自分で考えた後に他の人の解答を見ました。それでもオリジナルなコードになったと思います。
カレンダー作成問題
「たのしい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のテーブル形式で出力するとよさそうです。以下のテーブルを出力するイメージで実装を進めます。
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つは(当月の日付でなければ)空白で出力しますが、ここに該当する前月と次月の日付を求めます。
カレンダーの左上は、上で求めたstart_of_month.wday
ぶんstart_of_month
から過去の日付です。
カレンダーの右下は、end_of_month.wday
から土曜までの日数を足せば良いので、6 - end_of_month.wda
ぶん未来の日付がカレンダーの右下の日付になります。ここまでをコードにします。日付を足したり引いたりするにはDate#prev_day
とDate#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で実装する時に、
- 前後の月の日付を表示してほしい(薄い文字で)
- "今日"をハイライトしてほしい
といった追加の要望にも簡単に応えられる実装にしました。
出題者の伊藤淳一さんの書籍はこちら↓