Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

KotlinとSwiftとDartの標準的な日付変換を比べてみる

こんにちは。 最近はヨッシーストーリーを進めています、若宮(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ページで実行可能です。 ぜひ、気になったコードは手元で動かしてみてください。

play.kotlinlang.org

swiftfiddle.com

dartpad.dev

なお、ISO8601形式といいつつISO8601拡張形式を利用していきます。 これはよくあるAPIレスポンスに含まれるISO8601の形式が拡張形式のためです。

Kotlin

Kotlinの日付変換を考えると、Java 8のDate&Time APIなのかkotlinx-datetimeなのかを決める必要があります。

docs.oracle.com

github.com

本ブログの執筆時点では、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

DateFormatterISO8601DateFormatterを駆使していくことになります。 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)をタイムゾーンとして扱うため、意図しないタイムゾーンのずれにご注意ください。

developer.apple.com

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を生成し、ISO8601DateFormatterDateFormatterで取り出すことができます。

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の経験が長いため、どうしてもLocalDateOffsetDateTimeのような、型が日付情報をサポートしてくれる言語に便利さを感じてしまいます。 一方で、Dartのような「標準的なケースでは、難しく考えずに実装すると、問題なく動作させることができる」言語の良さも感じます。

多言語対応やマルチプラットフォーム対応と同様に、日付も適切なローカライズをしながら開発していきましょう!