asken テックブログ

askenエンジニアが日々どんなことに取り組み、どんな「学び」を得ているか、よもやま話も織り交ぜつつ綴っていきます。 皆さまにも一緒に学びを楽しんでいただけたら幸いです!

iOSDC 2021 「DateComponentsと仲良くなる」

初めに

askenの @sato-shin です。 iOSDC 2021 で「DateComponentsと仲良くなる」という題目で登壇させていただきましたので、記事としても残しておきます。

スライド

speakerdeck.com

YouTube


www.youtube.com

この発表をしようとしたきっかけ

弊社askenでは「あすけん」というサービスを開発しています。食事や運動などの記録を行い、生活習慣改善をサポートするサービスです。 あすけんでは朝食、昼食、夕食、間食の区分に分けて食事記録を行えるのですが、このとき1日という表現がとても大事になってきます。 この1日の表現にDate型を使っていたのですが、タイムゾーンの変更に弱く、取り扱いが難しい課題がありました。 これを解決するのにDateComponentsを利用しました。 この改善を通して、「DateComponentsは便利。もっと話題になっても良いのでは?」と思い、CfPを出すことを決めました。

DateComponentsと仲良くなる

時間の基礎

私たちはよく時間を "2021年9月19日 13時30分" というように年月日時分(+秒)を使って時間を認識することが多いと思います。 しかし、この表現方法のルールはとても複雑でコンピュータ上では扱いが簡単ではありません。 コンピュータにとって最も簡単な時間を定義する方法は、「基準点を決め、そこからの長さ」によって、時間を定義することです。

f:id:techaskeninc:20210917182709p:plain

Date型

この時間の定義をそのまま実装したクラスとして、Swift では Date型 が使われます。Date型のイニシャライザを見ると、それがよく分かります。

  • init(timeIntervalSinceNow: TimeInterval)
  • init(timeIntervalSince1970: TimeInterval)
  • init(timeIntervalSinceReferenceDate: TimeInterval)
  • init(timeInterval: TimeInterval, since: Date)
  • init() = (timeIntervalSinceNow: 0) と同義

Sinceは基準点で、TimeIntervalは基準点からの長さを表し単位は秒です。 Sinceに使える特徴的なものとして、以下のものがあります。特にUnixEpochはUnixTimeにおける基準点で、とてもよく使われます。

なお、TimeInterval は Double の alias です。

英語でDateといった場合に想像するものと違うので注意してください。Date型の正体は時間軸上の一点です。

日時計算の難しさ

Date型の表現はコンピュータにとっては扱いやすいのですが、人間にとっては扱いづらいです。 やはり私たちが日付を認識するときには、2021年9月19日 9時30分といった表現を使います。 しかしこの表現のルールは思った以上に複雑です。

  • 1年
  • 1ヶ月
    • 閏年ではない2月 = 28日間
    • 閏年の2月 = 29日間
    • 4, 6, 9, 11月 = 30日間
    • 1, 3, 5, 7, 8, 10, 12月 = 31日間
  • 1日
  • 1分
    • 閏秒が挿入されない = 60秒間
    • 閏秒が挿入される = 61秒間

年や月だけでなく、日や分の長さも固定長ではないことが分かります。さらにサマータイムタイムゾーンに依存するので、タイムゾーン特有の処理も必要になります。 これらを自前で全て網羅しつつ日時計算を行うのはあまりにも非効率です。そこで、多くのプログラミング言語では日時計算を行うものが提供されています。 SwiftではCalendar型が日時計算を担当する型として定義されています。

Date型では表現できないもの

あなたの生年月日をDate型で表現するとしたらどうなるでしょうか? 実はDate型では正確に表現することはできません。 生年月日が持っている情報は、「タイムゾーンに依存しない特定の1日」と言えるでしょう。 このタイムゾーンに依存しないというのが厄介です。 たとえば、日本の1日とニューヨークの1日というのはタイムゾーンによって、「ずれ」があります。 Date型は時間の点なので、この「ずれ」を表現することはできず、生年月日を正確に表現できません。

DateComponents型

そこで、もっと私たちが日常的に使っている日付の形式で表現できる型があると便利です。 この日付の形式で表現できる方というのがDateComponents型です。 DateComponents型は日時の構成要素の集合です。 この構成要素の数が多いため、DateComponentsのイニシャライザの引数は16個と多くなっています。

init(calendar: Calendar?, timeZone: TimeZone?, era: Int?, year: Int?, month: Int?, day: Int?, hour: Int?, minute: Int?, second: Int?, nanosecond: Int?, weekday: Int?, weekdayOrdinal: Int?, quarter: Int?, weekOfMonth: Int?, weekOfYear: Int?, yearForWeekOfYear: Int?)

全てoptionalとなっているため一度に全て利用しなければいけないわけではありません。表現したい時間に必要なものだけを指定します。 また、要素一つ一つは簡単です。

  • calendar: Calendar? = カレンダー(暦)
  • timeZone: TimeZone? = タイムゾーン
  • era: Int? = 時代、年号
    • グレゴリオ暦では 0 が紀元前を、1 が西暦を表す
    • 和暦では、0 が大化を、249 が平成を、250 が令和を表す
  • year: Int? = 年
  • month: Int? = 月
  • day: Int? = 日
  • hour: Int? = 時
  • minute: Int? = 分
  • second: Int? = 秒
  • nanosecond: Int? = ナノ秒
  • weekday: Int? = 曜日
  • weekdayOrdinal: Int? = その曜日が何番目か
    • たとえば、2021年9月19日は第日曜日
  • quarter: Int? = 四半期
  • weekOfMonth: Int? = その月の何週目か
    • たとえば、2021年9月19日は第4週目
  • weekOfYear: Int? = その年の何週目か
  • yearForWeekOfYear: Int? = weekOfYear計算用の年

DateComponents型の3つの表現

DateComoponents型は日時の構成要素の集合であり、実際にどのような意味を持つかはプログラムによって異なります。 意味のパターンとしては、大きく分けて3つあります。

  • 日時を表す
  • 量を表す
  • パターンマッチングを表す

日時を表す

この表現は、私たちがよく利用する "2021年9月19日 13時30分" などといった日時を表現することを目的とし、Date型へと変換可能な表現になります。

let today = DateComponents(calendar: gregorianCalendar, year: 2021, month: 9, day: 19, hour: 13, minute: 30)

DateComponents型では、意識しないものについては指定しなくて良いので、「Date型では表現できないもの」のところで挙げた生年月日を正しく表現することができます。 たとえば、私の誕生日は西暦1990年10月18日なのですが、これをDateComponents型で表現すると以下のようになります。

let birthday = DateComponents(calendar: gregorianCalendar, year: 1990, month: 10, day: 18)

ここで着目して欲しいのが、TimeZoneを指定していないということです。 生年月日が表したい「タイムゾーンに依存しない特定の1日」というものを正確に表現できています。

量を表す

この表現では時間の長さを表します。時間の量を表す型としてTimeInterval型がありますが、これは単位が秒しか扱えませんでした。 DateComponents型を使うことで、年、月、日、時、分といった長さも正しく表現できるようになります。 この量の表現と、Calendar型の日時計算関数を利用して日時計算を行うことができます。 たとえば、今から一ヶ月後を計算する場合には以下のように書けます。

let oneMonth = DateComponents(month: 1)
let oneMonthLater = calendar.date(byAdding: oneMonth, to: Date())

パターンマッチを表す

毎朝、決まった時間に目覚まし時計を設定している人は多いと思います。 この表現はそんなときに利用できます。 たとえば、朝の8時30分に通知したい場合のUserNotificationTriggerを作りたい場合には以下のように書けます。

let wakeUpAlertTime = DateComponents(hour: 8, minute: 30)
let trigger = UNCalendarNotificationTrigger(dateMatching: wakeUpAlertTime, repeats: true)

Tips

DateComponents型がここまで説明したどの意味を持つかはプログラムによって異なります。 たとえば、コードを以下のような切れ端で見たときに、

DateComponents(hour: 12)
  • 12時間という「量」の表現
  • 12時という「パターンマッチ」の表現

このどちらの表現なのかは察することはできません。プログラムの前後の文脈から判断するしかありません。 ですので、作っているアプリケーションでどのような表現が欲しいのか?を分析し、それに合うようなラッパークラスを作るのをお勧めします。 たとえば、時間の長さが必要であるといった場合には以下のようなラッパークラスを作るだけでも、どんな表現をしたいのか?が分かりやすくなります。

struct DateLength {
    var value: DateComponents
}

最後に

Date型は原始的な時間の表現であり、単純なケースではとても扱いやすいです。 しかし、Date型では表現しきれなかったり、複雑になるケースも多くあります。 DateComponents型がその問題を解決してくれる可能性があります。 実際に私は開発内で、DateComponents型を使うことでコードをスッキリさせることができました。

「これまでDate型しか使ったことなかったよ」という人は、これを期にDateComponents型を使った方が良いケースがないかどうか考えてみてください。

We are hiring!

askenでは、iOSエンジニアを募集しています! www.wantedly.com