Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

日本の祝祭日を計算してカレンダ-に表示するアプリサンプル

1,026 views

Published on

集まれSwift好き!Swift愛好会 #7での発表資料になります。

日本の祝祭日を判定するライブラリを作成しました。そのロジックを実装する過程で得た知見や気を付けたポイント等をまとめました。

(詳細解説)日本の祝祭日を計算してカレンダ-に表示するアプリサンプル
http://qiita.com/fumiyasac@github/items/33bfc07ad36dfffcdf8f

(サンプルリポジトリ)
https://github.com/fumiyasac/handMadeCalendarAdvance

Published in: Technology
  • Be the first to comment

  • Be the first to like this

日本の祝祭日を計算してカレンダ-に表示するアプリサンプル

  1. 1. 日本の祝祭日を計算して カレンダ−に表示するアプリサンプル 集まれSwift好き!Swift愛好会 #7 2016/05/30 Fumiya Sakai
  2. 2. 自己紹介と簡単な経歴など ✦ 今までの仕事履歴(本業) 石川県金沢市生まれ 本業はサーバーサイドのプログラマ ※Rails&PHP使い 26歳〜31歳: Webプログラマ(PHP & Rubyがキャリア長い) 23歳〜25歳: Webデザイナー兼ディレクター チャンスがあればiOSアプリ開発も絶賛やってみたい! 趣味:シルバーアクセサリー集め・スイーツ作り・アプリ開発 女子向け・グルメ・エンタメ関連のお仕事が多い Qiita : http://qiita.com/fumiyasac@github Github : https://github.com/fumiyasac ✦ 酒井文也(さかい ふみや) 東京(大塚)住まいの31歳 こんな格好を普段からしているので 遊び人に見られますがエンジニアです。 文系卒に思われますが 実は数学科で理系卒です。 めっちゃお酒好きそうに見えますが ビール苦手でお酒も超弱いです。 今でもたまにUIまわりとか触りたく なることがあったりなかったり 今年の4月からフリーランスです。 (今は渋谷のとある会社にいます) 最近のはまっている食べ物は カボチャと担々麺です。 最近はSwift以外ではRailsやLaravel・CakePHP・Node.jsなんかも
  3. 3. これまでに作ったもの(ネイティブアプリ) ① 簡易家計簿アプリ「Coffre」 ② ゲームアプリ「10秒虫食い算」 ・カレンダーを自作しています ・シンプルなお小遣い帳感覚で支出管理できます ・全問正解者ほとんどいません… ・不定期ですがコラムも書いています ・サーバーサイドはRuby on Railsを使用 http://www.coffre.me/ ・デザインにもこだわってみました(特にグラフ) ・実はちょっとバグがあります。 ・問題は今後追加予定(現在110問収録) 個人的にはなりますが、他にもアプリ・Webサービスなど開発中です(2016年も宜しくお願いします) ・サイト等は次回のアップデートで公開予定 http://blog.just1factory.net/services/284 ・若干の中毒性を含みます
  4. 4. ✦ 家計簿アプリにカレンダーを入れたくって試行錯誤をした iPhoneアプリ開発を始めたての頃 など予め事前データとして持っておくのは実運用ではちょっと面倒かもと思ったりした。 もう自分でつくるしかねぇ!と半ば自暴自棄の中で自作することを決心する。 日本の祝祭日という点を考慮してなおかつ既存のカレンダーでも簡単に導入することができれば! ★日本の祝祭日をカレンダーに実装するためのいくつかのアイデア ・Googleカレンダーからデータを取得してそのデータを永続化する? ・自分で調べてその都度アップデートして更新する? ★自分なりにライブラリを探してみるけど… などライブラリとして自分の気にいったもの&導入しやすいものがなかったりした。 ・カレンダー自体のライブラリはあるんだけど祝祭日判定込みのものはほとんど無い ・当時のSwift力では到底コードが理解できないので後で死にかねない 【結論】
  5. 5. ✦ Objective-Cでそれっぽいロジックのものを作成 1年前にカレンダーのプロトタイプを作成 これがきっかけで2015年3月に「potatotips #15」に登壇:カレンダーを自作する 初めてpull requestを頂いたり、使ってもいいですか?という反響を頂いたのは本当に嬉しかった。 ほんの少しだけオープンソースに貢献できたような気がしていちデベロッパーとしても嬉しかった思い出。 ★リリースしている家計簿アプリ(Coffre)にも試験的に実装 ・別言語で作られたカレンダーサンプルを基にシコシコと作成 ・一応日本の祝祭日にもなんとか考慮(ただしソース…汚いけれども) ★あれから1年が経ちSwiftでも対応しなければと思いつく 1年越しに今腰を据えて取り組んでみようかと思い立った次第。 ・Objective-C版があるのにSwift版はカレンダーだけで祝祭日ロジックは未実装 ・祝祭日の判定ロジックのみをライブラリにして欲しい等の要望があった 【嬉しかったこと】
  6. 6. ✦ 実際のカレンダーに日本の祝祭日を導入してみました 今回のサンプルの概要 おかげさまでSwiftだけでなく日本の祝祭日にも詳しくなることができて取り組みがいがありました(笑) ★祝祭日の部分は赤色で表示されるようになっています ・振替え日やハッピーマンデー法の施行前・施行後の祝祭日にもできるだけ対応 ゴールデンウィーク・シルバーウィーク 対応も考慮済み ・日本の祝祭日全てのロジックを計算式で考慮しています 春分・秋分・ハッピーマンデー法 対応も考慮済み
  7. 7. ✦ 今回もサンプルと詳細な解説をご用意しました サンプルはこちら リファクタリングや実装に関しては、@akio0911さんにご協力を頂きました。本当にありがとうございます! ★サンプルはこちら ・祝祭日ロジックのみを分離するようにしました ・既存のカレンダーアプリにも導入しやすいように年・月・日から祝祭日判定ができるようにしました ・Github: https://github.com/fumiyasac/handMadeCalendarAdvance ★詳細な解説はこちら 日本の祝祭日を計算してカレンダ−に表示するアプリサンプル ・Qiita: http://qiita.com/fumiyasac@github/items/33bfc07ad36dfffcdf8f ★(参考)以前に作成したカレンダーにも導入してみました Carthageまたは手動で計算ロジックファイルをコピーして導入できます ※導入に関しては今回のライブラリのREADMEを参照してみて下さい ・Github: https://github.com/fumiyasac/handMadeCalendarOfSwift
  8. 8. ✦ 日本の祝祭日の計算に当たっての参考資料やサンプルソース一式 日本の祝祭日計算の参考資料 しかしなんだかんだで困った時に一番参考になったのはWikipediaの情報でした:国民の祝日に関する法律 ★祝祭日の判定ロジックの参考資料 ・phpな休日 http://p-ho.net/index.php?page=22 ・祝日を自動判定するDateTime拡張クラス http://qiita.com/chiyoyo/items/539dc2840a1b70a8e2c3 上記のPHPプログラムを参考にした上でSwiftの実装に落としこんで開発 ★その他計算の参考資料 ・春分の日・秋分の日の計算 http://koyomi8.com/reki_doc/doc_0330.htm ・今年のゴールデンウィークは「火曜日」が振替休日・・・なんでこうなるの? https://www.bengo4.com/other/1146/1288/n_1412/ ・(豆知識)シルバーウィークになる時の条件 敬老の日の2日後が秋分の日ならば間に挟まれた期間は国民の休日とする
  9. 9. ✦ 条件分岐にタプルを使用して複数の引数をひとまとめにする 判定ロジックの見通しを良くする工夫 switch文での判定対象にタプルを設定できるのでwhere句を組み合わせてロジックの見通しを良くしている。 ★case (year[年], month[月], day[日], weekday[曜日]) where 条件式: ・引数が多くなってしまう場合はタプルを用いて一旦ひとまとめにしてから条件判定を行うと便利 ・条件判定はwhere句を用いることで条件の見通しを良くする(ちょっとSQLっぽいかも) switch (year, month, day, weekday) { //1月1日: 元旦 case (_, 1, 1, _): return true //1月2日: 振替休日 case (_, 1, 2, .Mon): return true //(1).1月15日(1999年まで)、(2).1月の第2月曜日(2000年から): 成人の日 case (year, 1, 15, _) where year <= 1999: return true case (year, 1, 8...14, .Mon) where year >= 2000: return true //2月11日: 成人の日 case (_, 2, 11, _): return true //2月12日: 振替休日 case (_, 2, 12, .Mon): return true タプルとは?:http://blog.kitoko552.com/entry/2015/06/17/213553 ・型の異なるものをひとまとめにしてくれる ・switch文の条件判定の条件にタプルを設定することが可能 ・必要な部分だけを抜き出してwhere句で条件判定をする 場合: monthが1 かつ dayが8から14の間 かつ weekdayが月曜日(enumで設定) 条件: yearが2000以上 ※2000年以降の1月の第2月曜日である[成人の日の判定]
  10. 10. ✦ 予め判定しておきたい曜日に関してはenumに定義してまとめておく Swiftでより柔軟になったenumの活用 enumの中にメソッドやコンストラクタの設定を行うことで関連する値を扱いやすく整理しておくと良いです。 ★Swiftではenumを柔軟に扱うことができる(メソッドやコンストラクタの設定) ・カレンダーの曜日を判定する部分についてはiOS8から使える dateWithEra()メソッドを使用している iOS8からの日付(NSDate)操作・比較:http://qiita.com/_mpon/items/c5b9573b4250f9a1d567 public enum Weekday: Int { case Sun, Mon, Tue, Wed, Thu, Fri, Sat init?(year: Int, month: Int, day: Int) { let cal = NSCalendar.currentCalendar() guard let date = cal.dateWithEra(AD, year: year, month: month, day: day, hour: 0, minute: 0, second: 0, nanosecond: 0) else { return nil } let weekdayNum = cal.component(.Weekday, fromDate: date) // 1:日曜日 〜 7:土曜日 self.init(rawValue: weekdayNum - 1) } var shortName: String { switch self { case .Sun: return "日" case .Mon: return "月" case .Tue: return "火" case .Wed: return "水" case .Thu: return "木" case .Fri: return "金" case .Sat: return "土" } } ・・・(一部省略)・・・ enumの中にコンストラクタを設定可能: ここでは現在の日付もとにしてカレンダー情報から曜日だけを抜き出すようにしている。 最後にself.init(rawValue: weekdayNum - 1)としてenumのrawValueの初期値を設定する。 ※ enumで設定した値と合わせるために -1 をしている。 enumの設定を利用するものによって変わりうる値の一覧などは、 このように設定しておくと後で色々と活用ができます。
  11. 11. ✦ 春分の日と秋分の日を算出する計算ロジック enumのメソッドを活用している例 このように結構すっきりとまとめてしまえば、条件分岐で利用する場合でも見通しがよくなると思います。 ★ここもenumを駆使して他のメソッドで扱いやすいようにする 春分)case (year, 3, day, _) where PublicHolidaysLawYear < year && day == SpringAutumn.Spring.calcDay(year: year): 秋分)case (year, 9, day, _) where PublicHolidaysLawYear <= year && day == SpringAutumn.Autumn.calcDay(year: year): private enum SpringAutumn { /// 春分の日 case Spring /// 秋分の日 case Autumn var constant: Double { switch self { case .Spring: return 20.69115 case .Autumn: return 23.09000 } } /// 春分の日・秋分の日を計算する /// 参考:http://koyomi8.com/reki_doc/doc_0330.htm func calcDay(year year: Int) -> Int { let x1: Double = Double(year - 2000) * 0.242194 let x2: Int = Int(Double(year - 2000) / 4) return Int(constant + x1 - Double(x2)) } } 使い方はこんな感じで使います。 春分の日と秋分の計算の方法は実はきちんと天文学的に決まっているみたいです。 なのでこの部分についてはキャストをしています。 ※詳しいことについては参考URLをご一読くださいませ。
  12. 12. ✦ 独立した条件判定をするメソッドを定義して利用しやすくする ゴールデンウィーク・シルバーウィーク判定 この部分は実装をする上でもテストケースを作成する部分でもかなり試行錯誤をした部分でした…(キツイ) ★ゴールデンウィークで5/6が振替休日になる場合とシルバーウィークになる場合 /** * * ゴールデンウィークの振替休日を判定する * 2007年以降で5/6が月・火・水(5/3 or 5/4 or 5/5が日曜日)なら5/6を祝日とする * See also: https://www.bengo4.com/other/1146/1288/n_1412/ * */ private func getGoldenWeekAlterHoliday(year: Int, weekday: Weekday) -> Bool { switch weekday { case .Mon, .Tue, .Wed where 2007 <= year: return true default: return false } } /** * * シルバーウィークの振替休日を判定する * 敬老の日の2日後が秋分の日ならば間に挟まれた期間は国民の休日とする * */ private func getAlterHolidaySliverWeek(year year: Int) -> Bool { return oldPeopleDay(year: year) + 2 == SpringAutumn.Autumn.calcDay(year: year) } 実際のswitch文での条件判定: case (_, 5, 6, _) where getGoldenWeekAlterHoliday(year, weekday: weekday): 実際のswitch文での条件判定:※敬老の日は別途oldPeopleDayメソッドで計算を行っている case (_, 9, _, _) where oldPeopleDay(year: year) < day && day < SpringAutumn.Autumn.calcDay(year: year) && getAlterHolidaySliverWeek(year: year):
  13. 13. ✦ 各テストケースもタプルに入れて判定をする XCTestでの祝祭日判定の検証 ちなみにこちらは随時追記していく予定&日本の法律が変わったらアップデートをしていく所存です。 ★該当祝祭日の年月日と期待される値をタプルに持たせてまとめて判定する ・テストがあるとやはり開発する側も使う側もお互いに安心をして使えるのは非常に心強く思う ・特に計算で検知する部分(ゴールデンウィーク・シルバーウィーク・春分の日・秋分の日)では大助かり func testCurrentYear() { let test = CalculateCalendarLogic() //2016年の場合のテストケース let testCases: [(Int,Int,Int,Bool)] = [  // 敬老の日: 2016年9月19日(月曜日) (2016, 9, 19, true), // 秋分の日: 2016年9月22日(木曜日) (2016, 9, 22, true), ] testCases.forEach { year, month, day, expected in let result = test.judgeJapaneseHoliday(year, month: month, day: day) guard let weekday = Weekday(year: year, month: month, day: day) else { XCTFail() ; return } let message = "(year)年(month)月(day)日((weekday.longName)):(result)" if expected { XCTAssertTrue (result, message) }else{ XCTAssertFalse(result, message) } } } 実装のポイント: XCTAssertTrueでテスト結果がtrueになることを、 XCTAssertFalseでテスト結果がfalseになることをテストする。 あら〜残念! 今年はシルバーウィークではないですね
  14. 14. ✦ 今後もSwift力を上げていきたいなと思う良いきっかけになった 今回のまとめ ご清聴ありがとうございました!またこのような機会があった際には是非ともよろしくお願い致します! ★ライブラリ開発に取り組む中でSwiftそのものに取り組む楽しみ ・ライブラリの作成それ自体に加えて「Swiftらしい実装」を考える上でもかなり勉強になった ・アプリ開発と同じようにライブラリやサンプル開発にも注力をしていきたい Swiftの便利なenumやタプルの使い方やその便利さを改めて知ることができた ★pull requestを実際に頂いたので本当に勉強になった よりシンプルで見通しの良い実装の書き方を実際に学ぶ機会は本当に貴重でした ★動作保証の難しさとテストの重要性 春分の日・秋分の日・ゴールデンウィーク・シルバーウィークの判定の際には本当に重宝しました ★サンプルやアプリ開発に加えて他にもライブラリの作成も今後してみたい まだまだ機能追加やリファクタリング・テストケースの充実化などはしていく予定です

×