LoginSignup
27
10

ISO 8601形式のDateTimeの扱いには気をつけよう

Last updated at Posted at 2023-08-01

概要

これは、私の体験談ですがFlutterでクライアントサイドの開発をしている際、APIのレスポンスで来る日付と、アプリ内部で持っている現在日時を比較するというロジックに関する不具合報告が上がってきました。
それについて調査していたところ、ISO 8601形式のDateTimeがAPIから返却されるのに対し、何も考えずただDateTime型にパースしていました。

その結果、よからぬ挙動を起こしていたため備忘録として残したいと思います。

DateTimeの種類(抜粋)

データーベースのシステムや、FirebaseなどのNoSQLサービスなどにも依存すると思いますが、DateTimeはいろいろな形式があります。

この記事でのサンプルは、JST(日本標準時)の補正をかけた時間を用います

ISO 8601

ISO8601が今回の問題の日付フォーマットですが、以下のような種類があります

  • 完全形式: 2023-08-01T00:00:00+09:00
  • 短縮形式: 20230801T000000+0900
  • 時刻情報なし: 2023-08-01
  • etc...

RFC 3339

こちらも意外と使われやすいのではないでしょうか

  • 2023-08-01T00:00:00+09:00

ASP.NET JSON Date

  • /Date(1694056800000+0900)/ (ミリ秒単位のUNIXタイムスタンプ、JST)

実際に不具合の事象

Dartを使っていたので、事例だけDartで記載します。
後半の方で説明する解決法については Java, TypeScript, Dart, PHPで記載します

void main() {
  final DateTime currentTime = DateTime.parse('2023-08-01T16:00:00.000Z');
  final DateTime apiResponseDate = DateTime.parse('2023-08-02T00:00:00.000+09:00');
  
  print(currentTime); // 2023-08-01 16:00:00.000Z
  print(apiResponseDate); // 2023-08-01 15:00:00.000Z
  print(currentTime.isBefore(apiResponseDate)); // false
}

apiResponseDateを何も考えずに変換してしまった結果、当然のとおりJSTの補正が入り9時間ずれてしまいました。
正直、サーバー側で補正かけるかUTCで共通にしておいてくれ...と思いましたが今回のプロジェクトではクライアントで補正するようになりました。

対処案1:力技で時間補正を削除する

クライアントで処理するにあたり、日付と時刻まで必要で補正分だけ無視しなくていけなかったので、補正時刻を排除するメソッドを作成しました。
実際に、言語別で書いてみました

基本的に、相当なことがない限り力技は好ましくありません。対処案2の方で記載した組み込み関数や専用のライブラリで解決する手法の方が適しています。

Dart

void main() {
  final DateTime jstDateTime = DateTime.parse('2023-08-02T00:00:00.000+09:00');
  print(jstDateTime); // 2023-08-01 15:00:00.000Z
  
  // 補正時刻を排除した処理
  final DateTime currentTime = DateTime.parse(removeTimezone('2023-08-01T16:00:00.000Z'));
  final DateTime apiResponseDate = DateTime.parse(removeTimezone('2023-08-02T00:00:00.000+09:00'));
  
  print(currentTime); // 2023-08-01 16:00:00.000
  print(apiResponseDate); // 2023-08-02 00:00:00.000
  print(currentTime.isBefore(apiResponseDate)); // true
}

// 補正時刻の排除をする
String removeTimezone(String date) {
  RegExp regExp = RegExp(r"(\+\d{2}:\d{2}|\-\d{2}:\d{2}|Z)$");
  return date.replaceAll(regExp, '');
}

Java

import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime jstDateTime = ZonedDateTime.parse("2023-08-02T00:00:00.000+09:00");
        System.out.println(jstDateTime); // 2023-08-02T00:00+09:00

        LocalDateTime currentTime = LocalDateTime.parse(removeTimezone("2023-08-01T16:00:00.000Z"), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"));
        LocalDateTime apiResponseDate = LocalDateTime.parse(removeTimezone("2023-08-02T00:00:00.000+09:00"), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"));

        System.out.println(currentTime); // 2023-08-01T16:00
        System.out.println(apiResponseDate); // 2023-08-02T00:00
        System.out.println(currentTime.isBefore(apiResponseDate)); // true
    }

    public static String removeTimezone(String date) {
        return date.replaceAll("(\\+\\d{2}:\\d{2}|\\-\\d{2}:\\d{2}|Z)$", "");
    }
}

PHP

<?php
date_default_timezone_set('UTC');

$jstDateTime = new DateTime('2023-08-02T00:00:00.000+09:00');
echo $jstDateTime->format('c'); // 2023-08-02T00:00:00+09:00
echo "\n";

$currentTime = new DateTime(removeTimezone('2023-08-01T16:00:00.000Z'));
$apiResponseDate = new DateTime(removeTimezone('2023-08-02T00:00:00.000+09:00'));

echo $currentTime->format('c'); // 2023-08-01T16:00:00+00:00
echo "\n";
echo $apiResponseDate->format('c'); // 2023-08-02T00:00:00+00:00
echo "\n";
echo $currentTime < $apiResponseDate ? 'true' : 'false'; // true

function removeTimezone($date) {
    return preg_replace('/(\+\d{2}:\d{2}|\-\d{2}:\d{2}|Z)$/', '', $date);
}
?>

TypeScript

function main() {
    let jstDateTime = new Date('2023-08-02T00:00:00.000+09:00');
    console.log(jstDateTime); // 2023-08-01T15:00:00.000Z

    let currentTime = new Date(removeTimezone('2023-08-01T16:00:00.000Z'));
    let apiResponseDate = new Date(removeTimezone('2023-08-02T00:00:00.000+09:00'));

    console.log(currentTime); // 2023-08-01T16:00:00.000Z
    console.log(apiResponseDate); // 2023-08-02T00:00:00.000Z
    console.log(currentTime < apiResponseDate); // true
}

function removeTimezone(date: string): string {
    let regExp = new RegExp("(\\+\\d{2}:\\d{2}|\\-\\d{2}:\\d{2}|Z)$");
    return date.replace(regExp, '');
}

main();

対処案2:組み込み関数で解決

こういうことって、結構あるあるでは?と思い組み込み関数が言語別にないか調べてみました。

Dart: toLocal()

DartではtoLocalメソッドがありました!これを用いたらJSTの差分を取り除けそうです

void main() {
  final date = "2023-08-02T00:00:00.000+09:00";
  print(DateTime.parse(date).toLocal()); // 2023-08-02 00:00:00.000
}

Java: toLocalDateTime()

Javaも変換をしつつtoLocalDateTimeを使えば同じことができそうです

import java.time.*;

public class Main {
    public static void main(String[] args) {
        String date = "2023-08-02T00:00:00.000+09:00";
        ZonedDateTime zdt = ZonedDateTime.parse(date);
        System.out.println(zdt.toLocalDateTime()); // 2023-08-02T00:00
    }
}

PHP: DateTimeZone + format

PHPは専用関数があるというわけではなさそうです。
DateTimeZoneformatの組み合わせで達成できそうです。

<?php

$dateTime = new DateTime("2023-08-02T00:00:00.000+09:00");
$zoneOffset = new DateTimeZone("Asia/Tokyo");
$dateTime->setTimezone($zoneOffset);

echo $dateTime->format("Y-m-d H:i:s");

?>

TypeScript

TypeScriptでは、Luxonというパッケージを用いればできそうです

npm install --save luxon
import { DateTime, IANAZone } from 'luxon';

let dateTime = DateTime.fromISO("2023-08-02T00:00:00.000+09:00");
const zoneOffset = new IANAZone("Asia/Tokyo").offset(dateTime);
dateTime = dateTime.setZone("Asia/Tokyo");

console.log(dateTime);

最後に

今回、やはり原因はサーバーサイドで予期せぬJST(日本標準時)の補正がかかっていたことと、その補正を考慮していなかったことが根本原因だと思われます。
今回はクライアントサイドで開発していたためサーバーサイドの技術スタックやインフラ要件がわからないので直接「これだ!」という原因はわかりませんが、おそらくTimeZoneをAsia/Tokyoで指定している関係かなと思います。
意図的にというよりは、フレームワークやインフラ側の設定が影響してそうです。

API作る側も使う側も気をつけていきたいですね。

27
10
21

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
10