このページについて
このページは、paiza ラーニング内に開設されているコンテンツ「レベルアップ問題集」で取り扱われているプログラミング課題について、独自の見解を述べたものです。
見解については、paizaラーニングの規約に基づき、許可されている範囲でのみ公開していますが、その内容については paiza とは一切関係なく、また paiza の立場を反映したものではありませんのでご注意ください。
挑戦する課題
レベルアップ問題集の日付セットから「曜日 (paizaランク B 相当)」を取り上げます。
以下は、問題公開 Web ページからの引用です。
問題
西暦y年m月d日が何曜日か表示してください。
ただし、各月の日数は以下のように決まることに注意してください。
- 4, 6, 9, 11月は30日
- 2月は閏年ならば29日、そうでなければ28日
- それ以外の月は31日
ただし、閏年は次のような年のことをいいます。
- 西暦が4で割り切れる年は閏年
- ただし、100で割り切れる年は平年
- ただし、400で割り切れる年は閏年
ただし、1800年1月1日は水曜日です。
入力される値
整数yとmとdが次のように、スペース区切りで1行で入力されます。
1 |
y m d |
期待する出力
以下のような形式で、答えを出力してください。
1 |
a曜日 |
- aは”月”, “火”, “水”, “木”, “金”, “土”, “日”のいずれかの文字です。
条件
すべてのテストケースにおいて、以下の条件をみたします。
- 1800≦y≦3000
- 1≦m≦12
- 1≦d≦31
- y年m月d日は実際に存在する日付です。(2019年2月31日のような存在しない日付は与えられません)
考え方
考え方は2つある。一つは、経過日数をチマチマ計算し、最後に 7 で割った剰余を求め、曜日を判別する方法。もう一つは公式を使う方法。
経過日数の計算
今回の場合、 y の範囲が「1800≦y≦3000」と狭い範囲に限定されているため、1800年を開始年として目的年まで日付を足していく方法でも計算は時間内に終了する。
計算するためには、あらかじめ閏年を計算する関数を作っておき、毎年閏年か否かを判断しながら日数を加算してゆくのが基本的な方針となる。
しかし、 y の範囲が莫大となると、この方法は適用できなくなる(時間が足りない)ので、素直に公式を適用する方法を取るしかなくなる。
公式の適用
Zeller の公式、もしくは Fairfield の公式を知っているか否かにかかっている。
ここでは簡単に理解できる上、覚えやすいフェアフィールドの公式を元にして解説する。
まず、前提として1月と2月は、前年の13月と14月とみなして計算する。これは、閏年に伴う計算を簡単にするための処理である。
すなわち、
y 年
m 月
d 日を計算する場合には、下準備として次のような処理を行う。
1 2 3 4 5 |
// Fairfield の公式に適用させるためには、1年を 3月から14月に変形する必要があある。 if m == 1 || m == 2 { y -= 1 m += 12 } |
はじめに、ある年 y までに経過した日数は、次の式で計算できる。
1 |
365 * (y - 1) + y / 4 - y / 100 + y / 400 //(1) |
1年を365日とするとき、ある年 y までの経過日数はおおよそ 365 * (y - 1) となる。
しかし、実際には閏年の影響を考慮する必要がある。それは
- 4 年に 1 回、経過日数は 1 日増える。
- しかし、100 年に 1 回は、上の増加分をキャンセルする必要がある( y を4で割り切れても、100 で割り切れる年は閏年ではないため)。
- しかし、直前の条件が成り立ったとしても、400 年に 1 回は閏年となる。
という条件である。それを考慮した増加分が y / 4 - y / 100 + y / 400 である。実際には、 / の計算は商であり、小数分は無視しなければならない( floor関数と同じ)。
一方、今年(3月はじまりの3月)に入ってからの日数は次の式で求まる。
1 |
31 + 28 + 306 * (m + 1) / 10 - 122 + d //(2) |
306 * (m + 1) / 10 だが、これは 3月から12月までの合計日数(306日)から m 月までの経過日数の計算となる。
これに、13月(通常の1月、つまり31日)と14月(2月、つまり28日)を加算し、さらに d を加えることで最終的な経過日数を求める。
なお、この式において、閏年の影響は考慮する必要はない。なぜなら、前年度までの経過日数の計算で閏年の影響は考慮ずみであり、また閏年が連続して発生することはないためである。
上記 (1) 式および (2) 式を加算し、式を変形すると次のような式となり、この値から経過日数を求めることが可能となる。
1 |
365 * y + (y / 4) - ( y / 100) + (y / 400) + 306 * (m + 1) / 10 - 428 + d |
さらに、この式に対して 7 の余剰を求めることで、曜日を日付として求めることができる。このとき、月曜日が 1 に対応する。
番号で得られた曜日を対応する曜日名で出力する一番簡単な方法は、あらかじめ配列にした曜日名を使うことである。そのとき、月曜が要素 の場合、剰余を求める前に 1 を減算する必要がある点に注意する必要がある。
解答例
経過日数で計算する場合(おそらく想定されている回答)
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
let ia = readLine()!.split(separator: " ").map { Int($0)! } // 計算しやすいように、変数に入れ直す var y = ia[0] var m = ia[1] var d = ia[2] // 曜日番号 -> 曜日名に変換するための配列 var date = ["月", "火", "水", "木", "金", "土", "日"] // 経過日数 var totalDay = 0 // ひと月が 30日のテーブル let d30 = [4:1, 6:1, 9:1, 11:1] // 閏年の計算 func isLeap(_ y: Int) -> Bool { if y % 400 == 0 || ( y % 100 != 0 && y % 4 == 0) { return true } return false } // y = 1800 年以降の場合だけ計算 if (y - 1800) > 0 { for i in 1800...(y - 1) { // 閏年によって1年の日数が変わる totalDay += isLeap(i) ? 366 : 365 } } // 2月以降のみ計算 if m > 1 { for i in 1...(m - 1){ if let _ = d30[i] { // 4,6, 9, 11 月の場合 totalDay += 30 } else if i == 2 { // 2月は閏年によって違う totalDay += isLeap(y) ? 29 : 28 } else { // それ以外は31日 totalDay += 31 } } } totalDay += d // 結果出力 // 1800年1月1日を水曜日にするために + 1 する。 print(date[(totalDay + 1) % 7] + "曜日") |
公式を適用する場合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 文字列を入力し、空白で区切り、Int 型にする let ia = readLine()!.split(separator: " ").map { Int($0)! } // 計算しやすいように、変数に入れ直す var y = ia[0] var m = ia[1] var d = ia[2] // 曜日番号 -> 曜日名に変換するための配列 var date = ["月", "火", "水", "木", "金", "土", "日"] // Fairfield の公式 func totalDate(_ y: Int, _ m: Int, _ d: Int) -> Int { return 365 * y + (y / 4) - ( y / 100) + (y / 400) + 306 * (m + 1) / 10 - 428 + d } // Fairfield の公式に適用させるためには、1年を 3月から14月に変形する必要があある。 if m == 1 || m == 2 { y -= 1 m += 12 } // 結果出力 print(date[(totalDate(y, m, d) - 1) % 7] + "曜日") |