こんにちは。 最近はヨッシーストーリーを進めています、若宮(id:D_R_1009)です。
Studyplusでは、いくつかの画面で日付を扱っています。 私はAndroidとiOSをKotlinとSwift、そしてDartで書く毎日を送っています。 そうなると、特に日付の操作で「この言語、この日付処理できたっけ?」となることが多くあります。
そんなわけで、今回はKotlinとSwift、Dartの日付処理についてまとめます。
やること
KotlinとSwift、Dartのそれぞれで同じ日付の操作をしてみます。 行う操作のシナリオは下記の4つです。
- ISO8601形式の文字列パース
yyyy-MM-dd
形式の文字列パース- UTCの日時をJSTとして表示
- 日付を生成し
yyyy-MM-dd
に変換
それぞれ、コードは次のWebページで実行可能です。 ぜひ、気になったコードは手元で動かしてみてください。
なお、ISO8601形式といいつつISO8601拡張形式を利用していきます。 これはよくあるAPIレスポンスに含まれるISO8601の形式が拡張形式のためです。
Kotlin
Kotlinの日付変換を考えると、Java 8のDate&Time APIなのかkotlinx-datetimeなのかを決める必要があります。
本ブログの執筆時点では、kotlinx-datetimeのバージョンが0.3.1となります。0.3.0でだいぶ便利になる機能が入っているのは見えているのですが、まだAPIが安定していないということで、Java 8のDate&Time APIを利用してきます。
JavaのDate&Time APIでは、下記のように日付の表現します。
対応クラス | 対応表現 | 用途 |
---|---|---|
LocalDate |
yyyy-MM-dd |
日付の操作 |
LocalDateTime |
yyyy-MM-ddThh:mm:ss |
日時(UTC)の操作 |
OffsetDateTime |
yyyy-MM-ddThh:mm:ssZ |
オフセット付き日時の操作 |
ZonedDateTime |
*1 | タイムゾーン付きの日時の操作 |
ISO8601形式の文字列パース
2021-12-06T10:30:15+09:00
(日本標準時2021年12月6日10時30分15秒)をパースしてみます。
import java.time.OffsetDateTime fun main() { val text = "2021-12-06T10:30:15+09:00" val datetime = OffsetDateTime.parse(text) println(datetime) // 2021-12-06T10:30:15+09:00 }
ちなみに、ミリ秒にも対応しています。
import java.time.OffsetDateTime fun main() { val text = "2021-12-06T10:30:15.5+09:00" val datetime = OffsetDateTime.parse(text) println(datetime) // 2021-12-06T10:30:15.500+09:00 }
Androidアプリであれば、基本的にサーバーからオフセット付きの日付が送られてくるので、このOffsetDateTime
を利用して「ある瞬間」を扱っていきます。
サーバーに送信するときには、OffsetDateTime.toString()
かDateTimeFormatter.ISO_OFFSET_DATE_TIME
あたりを利用して文字列に変換します。
yyyy-MM-dd
形式の文字列パース
日付はLocalDate
を利用します。
import java.time.LocalDate fun main() { val time = "2021-12-06" val date = LocalDate.parse(time) println(date) // 2021-12-06 }
LocalDate
は日付のみを扱うクラスのため、日付の加算や減算に対応しています。
import java.time.LocalDate fun main() { val text = "2021-12-06" val date = LocalDate.parse(text) val newDate = date.plusDays(1) println(newDate) // 2021-12-07 }
UTCの日時をJSTとして表示
2021-12-06T10:30:15Z
(協定世界時2021年12月6日10時30分15秒)をOffsetDateTime
でパースし、日本標準時に変換します。
import java.time.OffsetDateTime import java.time.ZoneOffset fun main() { val text = "2021-12-06T10:30:15Z" val datetime = OffsetDateTime.parse(text) val jstDatetime = datetime.withOffsetSameInstant(ZoneOffset.ofHours(9)) println(datetime) // 2021-12-06T10:30:15Z println(jstDatetime) // 2021-12-06T19:30:15+09:00 }
ここではJSTが年間を通じて+9時間であることを前提に、ZoneOffset.ofHlurs(9)
としています。
もしもJSTが季節ごとにオフセットが変わるようなことになると、この処理は正しく動作しないのでご注意ください。
ZoneId
を考慮する場合は、下記のように書くことができます。
import java.time.OffsetDateTime import java.time.ZoneId fun main() { val text = "2021-12-06T10:30:15Z" val datetime = OffsetDateTime.parse(text) val jstZoneId = ZoneId.of("Asia/Tokyo") val jstDatetime = datetime.atZoneSameInstant(jstZoneId).toOffsetDateTime() println(datetime) // 2021-12-06T10:30:15Z println(jstDatetime) // 2021-12-06T19:30:15+09:00 }
日付を生成しyyyy-MM-dd
に変換
OffsetDateTime
からLocalDate
に変換します。
import java.time.OffsetDateTime fun main() { val text = "2021-12-06T10:30:15Z" val datetime = OffsetDateTime.parse(text) val date = datetime.toLocalDate() println(datetime) // 2021-12-06T10:30:15Z println(date) // 2021-12-06 }
日付部を取り出すので、非常に簡単です。 なお、この処理は単純に日付を取り出すだけとなります。このため、オフセットは特に考慮されません。
import java.time.OffsetDateTime import java.time.ZoneOffset fun main() { val text = "2021-12-06T00:30:15+09:00" val datetime = OffsetDateTime.parse(text) val date = datetime.toLocalDate() println(datetime) // 2021-12-06T00:30:15+09:00 println(date) // 2021-12-06 val utcDatetime = datetime.withOffsetSameInstant(ZoneOffset.UTC) val utcDate = utcDatetime.toLocalDate() println(utcDatetime) // 2021-12-05T15:30:15Z println(utcDate) // 2021-12-05 }
Swift
DateFormatter
とISO8601DateFormatter
を駆使していくことになります。
ISO8601DateFormatter
はiOS 10以降でのみ利用できるので、活用していきましょう。
ISO8601形式の文字列パース
2021-12-06T10:30:15+09:00
(日本標準時2021年12月6日10時30分15秒)をパースしてみます。
import Foundation let text = "2021-12-06T10:30:15+09:00" let formatter = ISO8601DateFormatter() if let date = formatter.date(from: text) { print(formatter.string(from: date)) // 2021-12-06T01:30:15Z }
ミリ秒にも対応する場合は.withFractionalSeconds
を追加します。
import Foundation let text = "2021-12-06T10:30:15.5+09:00" let formatter = ISO8601DateFormatter() formatter.formatOptions.insert(.withFractionalSeconds) if let date = formatter.date(from: text) { print(formatter.string(from: date)) // 2021-12-06T01:30:15.500Z }
なお、ミリ秒有りとミリ秒無しを両方扱えるformatterは作成できません。 それらを同時に扱うためにはformatterを2つ用意しておく必要があります。
import Foundation let text = "2021-12-06T10:30:15.5+09:00" let formatter = ISO8601DateFormatter() let formatterFractional = ISO8601DateFormatter() formatterFractional.formatOptions.insert(.withFractionalSeconds) if let date = formatter.date(from: text) { print(formatter.string(from: date)) print(formatterFractional.string(from: date)) } else { print("first nil") // first nil } if let date = formatterFractional.date(from: text) { print(formatter.string(from: date)) // 2021-12-06T01:30:15Z print(formatterFractional.string(from: date)) // 2021-12-06T01:30:15.500Z } else { print("second nil") }
yyyy-MM-dd
形式の文字列パース
日付は.withFullDate
を利用します。
import Foundation let text = "2021-12-06" let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate] if let date = formatter.date(from: text) { print(formatter.string(from: date)) // 2021-12-06 print(ISO8601DateFormatter().string(from: date)) // 2021-12-06T00:00:00Z }
Swiftの場合、日付のみを扱う型は存在しません。
このため、日付を操作する場合はDate
として日付の増減をします。
import Foundation let text = "2021-12-06" let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate] if let date = formatter.date(from: text) { let nextDate = Calendar.current.date(byAdding: .day, value: 1, to: date)! print(formatter.string(from: nextDate)) // 2021-12-07 print(ISO8601DateFormatter().string(from: nextDate)) // 2021-12-07T00:00:00Z }
なおCalendar.current
はユーザーが利用しているタイムゾーンが反映されます。
ISO8601DateFormatter
はデフォルトでGMT(UTC)をタイムゾーンとして扱うため、意図しないタイムゾーンのずれにご注意ください。
The time zone used to create and parse date representations. When unspecified, GMT is used.
UTCの日時をJSTとして表示
2021-12-06T10:30:15Z
(協定世界時2021年12月6日10時30分15秒)をISO8601DateFormatter
でパースし、日本標準時に変換します。
import Foundation let text = "2021-12-06T10:30:15Z" let formatter = ISO8601DateFormatter() if let date = formatter.date(from: text) { formatter.timeZone = TimeZone(abbreviation: "JST") print(formatter.string(from: date)) // 2021-12-06T19:30:15+09:00 }
formatterのタイムゾーンはoffsetがついていれば影響を及ぼしません。 このため、下記のように書いても意図通りの時間出力を得ることができます。
import Foundation let text = "2021-12-06T10:30:15Z" let formatter = ISO8601DateFormatter() formatter.timeZone = TimeZone(abbreviation: "JST") if let date = formatter.date(from: text) { print(formatter.string(from: date)) // 2021-12-06T19:30:15+09:00 }
下記のようにoffsetがない情報をパースする場合は、注意が必要です。
import Foundation let text = "2021-12-06" let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate] formatter.timeZone = TimeZone(abbreviation: "JST") if let date = formatter.date(from: text) { print(formatter.string(from: date)) // 2021-12-06 print(ISO8601DateFormatter().string(from: date)) // 2021-12-05T15:00:00Z }
このケースでは、JSTが指定されたFormatterでDate
が作られるため、JSTの0時として生成されます。
ISO8601DateFormatter
を新規に生成すると、UTCとして表現されるため、2021-12-05
の15時になります。
日付を生成しyyyy-MM-dd
に変換
Date
を生成し、ISO8601DateFormatter
やDateFormatter
で取り出すことができます。
import Foundation var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(abbreviation: "GMT")! let components = DateComponents(year: 2021, month: 12, day: 6, hour: 20, minute: 30, second: 15) if let date = calendar.date(from: components) { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate] print(formatter.string(from: date)) // 2021-12-06 }
この時、formatterに設定したTimeZone
が考慮されて、日付が出力されます。
import Foundation var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(abbreviation: "GMT")! let components = DateComponents(year: 2021, month: 12, day: 6, hour: 20, minute: 30, second: 15) if let date = calendar.date(from: components) { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate] formatter.timeZone = TimeZone(abbreviation: "JST") print(formatter.string(from: date)) // 2021-12-07 }
作成した時のTimeZone
と、出力する時のTimeZone
を揃えておきましょう。
特に、Calendar.current
を利用している時には、.current
を設定する必要があります。
Dart
DartではDateTime
を利用します。
ISO8601形式の文字列パース
2021-12-06T10:30:15+09:00
(日本標準時2021年12月6日10時30分15秒)をパースしてみます。
void main() { const text = '2021-12-06T10:30:15+09:00'; final date = DateTime.parse(text); print(date); // 2021-12-06 01:30:15.000Z }
ミリ秒もサポートされています。
void main() { const text = '2021-12-06T10:30:15.5+09:00'; final date = DateTime.parse(text); print(date); // 2021-12-06 01:30:15.500Z }
JavaのDate&Time APIっぽさもあるのですが、内部的にはUTC時間で扱われるなど、細かな違いがあります。
yyyy-MM-dd
形式の文字列パース
yyyy-MM-dd
の文字列を、そのまま追加できます。
void main() { const text = '2021-12-06'; final date = DateTime.parse(text); print(date); // 2021-12-06 00:00:00.000 }
日付の増減させるには、Duration
を利用します。
void main() { const text = '2021-12-06'; final date = DateTime.parse(text); final nextDate = date.add(const Duration(days: 3)); print(date); // 2021-12-06 00:00:00.000 print(nextDate); // 2021-12-09 00:00:00.000 }
UTCの日時をJSTとして表示
2021-12-06T10:30:15Z
(協定世界時2021年12月6日10時30分15秒)をDateTime
でパースし、日本標準時に変換します。
void main() { const text = '2021-12-06T10:30:15Z'; final date = DateTime.parse(text); final result = date.toLocal(); final zone = result.timeZoneName; print(result); // 2021-12-06 19:30:15.000 print(zone); // 日本標準時 }
toLocal()
は実行されている環境のLocalを反映させます。
このため、日本時間で動作させている環境では、上記の処理でJSTとして表示が可能です。
日付を生成しyyyy-MM-dd
に変換
DateTime
のコンストラクタから日付を生成できます。
import 'package:intl/intl.dart'; void main() { final date = DateTime(2021, 12, 6, 20, 30, 15); final formatter = DateFormat.yMd(); print(date); // 2021-12-06 20:30:15.000 print(date.timeZoneName); // 日本標準時 print(formatter.format(date)); // 12/6/2021 }
なお、上記のケースでは実行環境のLocalが反映されています。
UTCとして作成する場合には、DateTime.utc
コンストラクタを利用します。
import 'package:intl/intl.dart'; void main() { final date = DateTime.utc(2021, 12, 6, 20, 30, 15); final formatter = DateFormat.yMd(); print(date); // 2021-12-06 20:30:15.000Z print(date.timeZoneName); // UTC print(formatter.format(date)); // 12/6/2021 }
まとめ
日付はアプリケーションの中でも、よく取り扱うデータではないでしょうか。 ただ、今回紹介したように、その扱い方は言語によって異なっています。
私はJavaやKotlinの経験が長いため、どうしてもLocalDate
やOffsetDateTime
のような、型が日付情報をサポートしてくれる言語に便利さを感じてしまいます。
一方で、Dartのような「標準的なケースでは、難しく考えずに実装すると、問題なく動作させることができる」言語の良さも感じます。
多言語対応やマルチプラットフォーム対応と同様に、日付も適切なローカライズをしながら開発していきましょう!