🐡

Flutter WebではDateTimeの扱いに少し気をつけましょうというお話

2023/11/28に公開

DateTimeの罠

DateTime.parse('2023-11-30 23:59:59:999999')がどう扱われるかご存知でしょうか?
まずは、このDateTime型のインスタンスをISO8601の文字列としてiOS/AndroidとWebでそれぞれのプラットフォームで表示してみます。

iOS/Android Web

お分かりいただけますでしょうか?

Webの方は日時が繰り上がってしまってますね。

なぜこのようなことが起こるのかparseの仕様を少し調べてみました。

DateTime.parseの仕様を掘り下げる

以下に一部省略したparseの実装を転載しています。これを確認すると、精度に関してはマイクロ秒までちゃんとあるようです。

static DateTime parse(String formattedString) {
  var re = _parseFormat;
  Match? match = re.firstMatch(formattedString);
  if (match != null) {
    int parseIntOrZero(String? matched) {
      if (matched == null) return 0;
      return int.parse(matched);
    }

    // Parses fractional second digits of '.(\d+)' into the combined
    // microseconds. We only use the first 6 digits because of DateTime
    // precision of 999 milliseconds and 999 microseconds.
    int parseMilliAndMicroseconds(String? matched) {
      if (matched == null) return 0;
      int length = matched.length;
      assert(length >= 1);
      int result = 0;
      for (int i = 0; i < 6; i++) {
        result *= 10;
        if (i < matched.length) {
          result += matched.codeUnitAt(i) ^ 0x30;
        }
      }
      return result;
    }

    int? value = _brokenDownDateToValue(years, month, day, hour, minute,
        second, millisecond, microsecond, isUtc);

    return DateTime._withValue(value, isUtc: isUtc);
  } else {
    throw FormatException("Invalid date format", formattedString);
  }
}

ただどこかで丸めていると思うのですが、このコードでは特に見当たりませんでしたが、_brokenDownDateToValue()のが怪しそうな雰囲気を醸し出していますね。

この実装はDartのエンジン側にありそうなので、そちらを確認してみます。

Flutter/Dartのエンジンを確認

このページを参考に、Flutter/Dartエンジンのコードを生成します。

生成されたコードから該当のコードを検索すると、VM側とJSランタイム側の実装があるのがわかります。[1]JS側の実装を見てみると、以下の実装がありました。

  /// Rounds the given [microsecond] to the nearest milliseconds value.
  ///
  /// For example, invoked with argument `2600` returns `3`.
  static int _microsecondInRoundedMilliseconds(int microsecond) {
    return (microsecond / 1000).round();
  }

  static int? _brokenDownDateToValue(int year, int month, int day, int hour,
      int minute, int second, int millisecond, int microsecond, bool isUtc) {
    return Primitives.valueFromDecomposedDate(
        year,
        month,
        day,
        hour,
        minute,
        second,
        millisecond + _microsecondInRoundedMilliseconds(microsecond),
        isUtc);
  }

ご覧の通り、Web側のエンジンではミリ秒を扱う際にマイクロ秒を四捨五入していることで繰り上がりが発生します。iOS/AndroidのVMではそのような実装は見つけられませんでした。
なぜこのようにプラットフォームで実装を変えているのか理由は推察できませんでしたが、このことが原因で冒頭で示した日時の挙動に差異が出てしまっていました。

もし、Flutter WebでDateTimeの境界値を扱うような場合は、この実装仕様は覚えておいた方が良いかもしれません。

参考

脚注
  1. Dartのプラットフォームごとの違い: https://dart.dev/overview#platform ↩︎

Discussion