【Dart】DateTime,クラスの基本と応用を徹底解説 +TimeOfDay

  • 2024年6月14日
  • 2024年7月11日
  • Dart

対象者

  • Flutterを使用してアプリ開発を行っているソフトウェアエンジニア
  • 日時の操作やフォーマットについて深く理解し、効率的に実装したいと考えている
  • グローバル対応のアプリ開発に興味があり、タイムゾーンやロケール対応について学びたい

はじめに

アプリ開発を行う中で、日付や時刻の操作が必要な場面に直面したことはありませんか?特にFlutterを使用している場合、DateTimeクラスをうまく活用することで、より効率的にそして正確に日時を扱うことができます。しかし、DateTimeクラスの使い方や日時のフォーマット、タイムゾーンの変換など、知識不足で困った経験を持つエンジニアも多いのではないでしょうか。

この記事では、DateTimeクラスの基本的な使い方から応用までを詳しく解説します。特に、現地時間とUTCの変換やタイムゾーンの指定、パフォーマンス最適化のポイントについても触れているため、グローバルなユーザーを対象としたアプリ開発を行っているエンジニアにとって必見の内容です。

DateTimeクラスの操作について深く理解し、アプリ開発のスキルを一段と向上させましょう。

DateTimeの基本

DateTimeクラスとは

DateTimeクラスは、FlutterやDartで日付と時刻を扱うための基本的なクラスです。日付や時刻を操作するために必須のクラスであり、アプリケーションのさまざまなシナリオで利用されます。DateTimeクラスを使うことで、現在の日付や時刻の取得、特定の日時の設定、日付と時刻の演算などが簡単に行えます。
例えば、スケジュール管理アプリやリマインダー機能、ログ機能など、時間に関する機能を持つアプリケーションでは必ずと言って良いほど使われます。

DateTimeクラスを使用することで、日付や時刻に関連する操作が効率的かつ正確に行えるため、開発者にとって非常に便利なツールです。

DateTimeのインスタンス化

DateTimeクラスのインスタンス化は非常に簡単で、いくつかの方法があります。最も一般的な方法は、現在の日時を取得することです。また、特定の日時を設定してインスタンス化することもできます。

現在の日時を取得する場合、DateTime.now()メソッドを使用します。これはシステムの現在の日時を返します。特定の日時を設定する場合、DateTime(year, month, day, hour, minute, second)コンストラクタを使用します。

// 現在の日時を取得
DateTime now = DateTime.now();
print('現在の日時: $now');

// 特定の日時を設定してインスタンス化
DateTime specificDate = DateTime(2023, 6, 12, 14, 30);
print('特定の日時: $specificDate');

このように、DateTimeクラスのインスタンス化は非常に柔軟で、さまざまなシナリオに対応できます。

変な日付

DartのDateTimeクラスは、日付や時間の計算に関して非常に柔軟です。例えば、無効な日付を指定した場合でも、自動的に正しい日付に調整してくれます。以下のテストコードでは、その例を示しています。

  test('変な日付', () {
    expect(DateTime(2024, 1, 0), DateTime(2023, 12, 31));
    expect(DateTime(2024, 1, 32), DateTime(2024, 2, 1));
    expect(DateTime(2024, 0, 1), DateTime(2023, 12, 1));
    expect(DateTime(2024, -1, 1), DateTime(2023, 11, 1));
    expect(DateTime(2024, 13, 1), DateTime(2025, 1, 1));
  });

DateTimeの操作

DateTimeのプロパティ

DateTimeクラスには、日付と時刻を操作するための多くのプロパティとメソッドが用意されています。これらのプロパティとメソッドを活用することで、日付や時刻に関する複雑な操作も簡単に行うことができます。

主要なプロパティには、年(year)、月(month)、日(day)、時(hour)、分(minute)、秒(second)、ミリ秒(millisecond)があります。これらのプロパティを使用することで、DateTimeオブジェクトから各要素を簡単に取得できます。

  test('DateTime properties test', () {
    DateTime datetime = DateTime(2023, 6, 12, 14, 30);
    expect(datetime.year, 2023);
    expect(datetime.month, 6);
    expect(datetime.day, 12);
    expect(datetime.hour, 14);
    expect(datetime.minute, 30);
    expect(datetime.second, 0);
  });

日付と時刻の比較

DateTimeクラスでは、日付と時刻の比較が簡単に行えます。これにより、特定の日付や時刻が他の日付や時刻と比べて早いか遅いかを判断できます。

日付と時刻を比較するためのメソッドには、isBefore(), isAfter(), isAtSameMomentAs()などがあります。

  test('日付と時刻の比較', () {
    DateTime pastDate = DateTime(2024, 1, 1);
    DateTime baseDate = DateTime(2024, 1, 2);
    DateTime sameDate = DateTime(2024, 1, 2);
    DateTime futureDate = DateTime(2024, 1, 3);

    expect(baseDate.isBefore(pastDate), false);
    expect(baseDate.isBefore(sameDate), false);
    expect(baseDate.isBefore(futureDate), true);

    expect(baseDate.isAfter(pastDate), true);
    expect(baseDate.isAfter(sameDate), false);
    expect(baseDate.isAfter(futureDate), false);

    expect(!baseDate.isBefore(pastDate), true);
    expect(!baseDate.isBefore(sameDate), true);
    expect(!baseDate.isBefore(futureDate), false);

    expect(!baseDate.isAfter(pastDate), false);
    expect(!baseDate.isAfter(sameDate), true);
    expect(!baseDate.isAfter(futureDate), true);
    
    expect(!baseDate.isAfter(pastDate), false);
    expect(!baseDate.isAfter(sameDate), true);
    expect(!baseDate.isAfter(futureDate), true);

    expect(baseDate.isAtSameMomentAs(pastDate), false);
    expect(baseDate.isAtSameMomentAs(sameDate), true);
    expect(baseDate.isAtSameMomentAs(futureDate), false);
  });

特定の日時も含めたい場合、反対のメソッドを否定することで対応出来ます。
例えば、isAfter()メソッドを使用してある日時が指定の日時より後かどうかを確認したい場合、指定の日時そのものも含めたいことがあります。その場合は、isBefore()メソッドを使用し、その結果を否定することで対応できます。

このように、DateTimeクラスを使用することで、日付と時刻の比較が容易に行えるため、アプリケーションのロジックをシンプルに保つことができます。

時間の加減算

DateTimeクラスでは、時間の加減算も簡単に行えます。これにより、特定の日付から一定期間を加えたり、減らしたりする操作が可能です。

時間の加減算を行うためには、add()subtract()メソッドを使用します。これらのメソッドは、Durationクラスと組み合わせて使用します。

  test('時間の加減算', () {
    DateTime baseDate = DateTime(2024, 1, 2);

    // 10日後の日時を計算
    expect(
        baseDate.add(const Duration(
            days: 1,
            hours: 2,
            minutes: 3,
            seconds: 4,
            milliseconds: 5,
            microseconds: 6)),
        DateTime(2024, 1, 3, 2, 3, 4, 5, 6));

    // 10日前の日時を計算
    DateTime pastDate = baseDate.subtract(const Duration(days: 1));
    expect(pastDate, DateTime(2024, 1, 1));

    // 13ヶ月後の日時を計算(Durationは月をサポートしていないため、手動で計算)
    expect(DateTime(baseDate.year, baseDate.month + 13, baseDate.day),
        DateTime(2025, 2, 2));

    // 1年後の日時を計算
    expect(DateTime(baseDate.year + 1, baseDate.month, baseDate.day),
        DateTime(2025, 1, 2));
  });

このように、DateTimeクラスを使用することで、時間の加減算が簡単に行えるため、開発者は複雑な日時計算を容易に実装できます。

日時の差分

  test('日時の差分', () {
    DateTime baseDate = DateTime(2024, 1, 2);

    expect(DateTime(2024, 1, 2 + 90, 1).difference(baseDate),
        const Duration(days: 90, hours: 1));

    DateTime threeDays = DateTime(2024, 1, 2 + 3);
    expect(threeDays.difference(baseDate), const Duration(days: 3));
    expect(threeDays.difference(baseDate).inDays, 3);
    expect(threeDays.difference(baseDate).inHours, 24 * 3);
    expect(threeDays.difference(baseDate).inMinutes, 60 * 24 * 3);
  });

DateTimeのフォーマット

DateFormatクラスの使用

DateFormatクラスを使用することで、DateTimeオブジェクトを指定された形式の文字列に変換することができます。これは、ユーザーに見やすい形で日付や時刻を表示するために非常に重要です。

DateFormatクラスを使用する理由は、様々な形式で日付や時刻を表示する必要があるからです。例えば、短い形式、長い形式、月と年のみなど、多様なニーズに応じて柔軟に対応できます。

import 'package:intl/intl.dart';

  test('DateFormatクラスの使用', () {
    DateTime baseDate = DateTime(2024, 1, 2, 3, 4);
    String formattedDate = DateFormat('yyyy-MM-dd – kk:mm').format(baseDate);
    expect(formattedDate, '2024-01-02 – 03:04');

    DateTime midnight = DateTime(2024, 1, 2);
    expect(DateFormat('hh:mm a').format(midnight), '12:00 AM');
    expect(DateFormat('HH:mm a').format(midnight), '00:00 AM');
    expect(DateFormat('kk:mm a').format(midnight), '24:00 AM');
    expect(DateFormat('H時m分').format(midnight), '0時0分');
    expect(DateFormat('HH時mm分').format(midnight), '00時00分');
  });

DateFormatクラスを使うことで、DateTimeオブジェクトを任意のフォーマットで表示することができ、アプリケーションのユーザーインターフェースをより魅力的にすることができます。

日本でよく使いそうな形式

日本で一番なじみがありそうな「yyyy/MM/dd hh:mm:ss = 年/月/日 時:分:秒」で24時間形式のテストをしました。HHは大文字にしないと、12時間形式になるので注意。

  test('yyyy/MM/dd hh:mm:ss', () {
    DateFormat dateFormat = DateFormat('yyyy/MM/dd HH:mm:ss');

    expect(dateFormat.format(DateTime(2023, 1, 1)), '2023/01/01 00:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 0)), '2023/01/01 00:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 12)), '2023/01/01 12:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 13)), '2023/01/01 13:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 24)), '2023/01/02 00:00:00');

    expect(dateFormat.parse('2023/01/01 00:00:00'), DateTime(2023, 1, 1));
  });

さらなるカスタムフォーマット

カスタムフォーマットを作成することで、特定の表示形式を満たすことができます。これは、アプリケーションのブランドやデザインガイドラインに従った日付表示が必要な場合に特に有効です。

カスタムフォーマットを作成する理由は、標準的なフォーマットが要件を満たさない場合があるためです。例えば、特定の文化やビジネスの要件に応じたフォーマットが必要な場合があります。

import 'package:intl/intl.dart';

  test('さらなるカスタム', () {
    DateTime baseDate = DateTime(2024, 1, 2, 3, 4);
    expect(DateFormat('E, M d, y').format(baseDate), 'Tue, 1 2, 2024');
    expect(DateFormat('EE, MM dd, yy').format(baseDate), 'Tue, 01 02, 24');
    expect(
      DateFormat('EEE, MMM ddd, yyy').format(baseDate),
      'Tue, Jan 002, 2024',
    );
    expect(
      DateFormat('EEEE, MMMM dddd, yyyy').format(baseDate),
      'Tuesday, January 0002, 2024',
    );

    expect(DateFormat('MM').format(baseDate), '01');
    expect(DateFormat('LL').format(baseDate), '01');
    expect(DateFormat('dd').format(baseDate), '02');
    expect(DateFormat('cc').format(baseDate), '2');

    expect(DateFormat('G yyyy D').format(baseDate), 'AD 2024 2');
    expect(
        DateFormat('G yyyy D').format(baseDate.add(const Duration(days: 50))),
        'AD 2024 52');
  });

このように、カスタムフォーマットを作成することで、特定の要件に応じた日付表示が可能になります。

DateFormatの記号と意味

記号 意味
G 時代の指定 AD
y 1996
M 月(年の中の月) July & 07
L 独立した月 July & 07
d 日(その月の中の日) 10
c 独立した日 10
h 時間(午前/午後の時間1~12) 12
H 時間(1日の中の時間0~23) 0
m 分(その時間の中の分) 30
s 秒(その分の中の秒) 55
S ミリ秒 978
E 曜日 Tuesday
D 年の日数 189
a 午前/午後のマーカー PM
k 時間(1日の中の時間1~24) 24
K 時間(午前/午後の時間0~11) 0
Q 四半期 Q3
' テキストのエスケープ 'Date='
'' シングルクォート 'o''clock'

MとL, dとcの違いがよく分からない、、、
L は、文脈の中で使われる月を表すらしい、、

ロケール対応のフォーマット

ロケール対応のフォーマットを使用することで、ユーザーの地域に応じた日付表示を行うことができます。これは、国際化対応アプリケーションにおいて非常に重要です。

ロケール対応のフォーマットを使用する理由は、異なる文化圏のユーザーに適切な日付表示を提供するためです。例えば、アメリカでは月/日/年の形式が一般的ですが、日本では年/月/日の形式が一般的です。

import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';

 test('ロケール対応のフォーマット', () async {
    DateTime baseDate = DateTime(2024, 1, 2);
    await initializeDateFormatting();

    // 日本語でのフォーマット
    expect(DateFormat.yMMMMEEEEd('ja_JP').format(baseDate), '2024年1月2日火曜日');
    // 英語(アメリカ)でのフォーマット
    expect(DateFormat.yMMMMEEEEd('en_US').format(baseDate),
        'Tuesday, January 2, 2024');

    // 月と年のみのフォーマット
    expect(DateFormat.yM('ja_JP').format(baseDate), '2024/1');
    expect(DateFormat.yM('en_US').format(baseDate), '1/2024');

    // 年、月、日の日付フォーマット
    expect(DateFormat.yMd('ja_JP').format(baseDate), '2024/1/2');
    expect(DateFormat.yMd('en_US').format(baseDate), '1/2/2024');

    // 年、月、日、曜日の日付フォーマット
    expect(DateFormat.yMEd('ja_JP').format(baseDate), '2024/1/2(火)');
    expect(DateFormat.yMEd('en_US').format(baseDate), 'Tue, 1/2/2024');

    // 月と年、月名のフォーマット
    expect(DateFormat.yMMM('ja_JP').format(baseDate), '2024年1月');
    expect(DateFormat.yMMM('en_US').format(baseDate), 'Jan 2024');

    // 年、月、日、月名の日付フォーマット
    expect(DateFormat.yMMMd('ja_JP').format(baseDate), '2024年1月2日');
    expect(DateFormat.yMMMd('en_US').format(baseDate), 'Jan 2, 2024');

    // 年、月、日、曜日、月名の日付フォーマット
    expect(DateFormat.yMMMEd('ja_JP').format(baseDate), '2024年1月2日(火)');
    expect(DateFormat.yMMMEd('en_US').format(baseDate), 'Tue, Jan 2, 2024');

    // 年、月、日、月名の日付フォーマット
    expect(DateFormat.yMMMM('ja_JP').format(baseDate), '2024年1月');
    expect(DateFormat.yMMMM('en_US').format(baseDate), 'January 2024');

    // 年、月、日、曜日、月名の日付フォーマット
    expect(DateFormat.yMMMMd('ja_JP').format(baseDate), '2024年1月2日');
    expect(DateFormat.yMMMMd('en_US').format(baseDate), 'January 2, 2024');

    // 年、月、日、曜日、月名の日付フォーマット
    expect(DateFormat.yMMMMEEEEd('ja_JP').format(baseDate), '2024年1月2日火曜日');
    expect(DateFormat.yMMMMEEEEd('en_US').format(baseDate),
        'Tuesday, January 2, 2024');

    // 年、四半期のフォーマット
    expect(DateFormat.yQQQ('ja_JP').format(baseDate), '2024/Q1');
    expect(DateFormat.yQQQ('en_US').format(baseDate), 'Q1 2024');

    // 年、四半期のフォーマット
    expect(DateFormat.yQQQQ('ja_JP').format(baseDate), '2024年第1四半期');
    expect(DateFormat.yQQQQ('en_US').format(baseDate), '1st quarter 2024');
  });

ロケール対応のフォーマットを使用することで、グローバルなユーザーに対して直感的で理解しやすい日付表示を提供することができます。

エラー LocaleDataException: Locale data has not been initialized への対応

import 'package:intl/date_symbol_data_local.dart'; await initializeDateFormatting();を追加。import 'package:intl/date_symbol_data_file.dart';の方ではない

エラー全文: LocaleDataException: Locale data has not been initialized, call initializeDateFormatting().

DateTimeのタイムゾーン

現地時間とUTCの変換

DateTimeクラスを使用することで、現地時間とUTC(協定世界時)の変換が簡単に行えます。これは、異なるタイムゾーンに住むユーザー間で時間を同期する場合に非常に重要です。グローバルなアプリケーションでは、タイムゾーンの扱いが不可欠です。

現地時間とUTCの変換は、異なるタイムゾーンにおける正確な時間を管理するために重要です。特に、国際会議のスケジュールや、グローバルなユーザー間のコミュニケーションを円滑にするために必要です。

  test('現地時間とUTCの変換(日本で実施されることが前提。タイムゾーンに依存するため、環境によって異なる可能性があります)', () {
    DateTime baseDate = DateTime(2024, 1, 2, 9, 1);
    expect(baseDate.toUtc(), DateTime.utc(2024, 1, 2, 0, 1));

    expect(DateTime.utc(2024, 1, 2).toLocal(), DateTime(2024, 1, 2, 9));
  });

このように、DateTimeクラスを使うことで、現地時間とUTCの変換が簡単に行え、グローバルな時間管理が容易になります。

タイムゾーンの指定

DateTimeクラスを使用して特定のタイムゾーンを指定することで、異なる地域の時間を正確に管理することができます。これは、例えば、イベントや会議のスケジュールを異なるタイムゾーンのユーザーに提供する場合に役立ちます。

タイムゾーンの指定は、グローバルなユーザーを対象とするアプリケーションで非常に重要です。ユーザーが異なるタイムゾーンに住んでいる場合でも、一貫した時間表示を提供することが求められます。

import 'package:timezone/data/latest.dart';
import 'package:timezone/timezone.dart;

  test('タイムゾーンの指定', () async {
    initializeTimeZones();
    DateTime baseDate = DateTime(2024, 1, 2, 14);

    // タイムゾーンを指定して日時を取得
    final TZDateTime tokyoTime =
        TZDateTime.from(baseDate, getLocation('Asia/Tokyo'));
    final TZDateTime nyTime =
        TZDateTime.from(baseDate, getLocation('America/New_York'));

    expect(tokyoTime.copyWith(), DateTime(2024, 1, 2, 14));
    expect(tokyoTime.toString(), '2024-01-02 14:00:00.000+0900');
    expect(tokyoTime.toIso8601String(), '2024-01-02T14:00:00.000+0900');

    // ニューヨークは東京より14時間遅れ
    expect(nyTime.copyWith(), DateTime(2024, 1, 2, 0));
  });

TZDateTime から DateTimeへの変換はcopyWithでできる、、、いいのか?DateTime のメソッドは使える。
このように、特定のタイムゾーンを指定することで、異なる地域の時間を正確に管理することができます。これにより、ユーザー体験が向上し、グローバルなアプリケーションでの時間管理が一貫性を持つようになります。

エラー Tried to get location before initializing timezone database への対応

import 'package:timezone/data/latest.dart';initializeTimeZones(); を追加

時刻のみのクラス「TimeOfDay」

TimeOfDayクラスは時刻のみを表すクラスであり、以下の特徴があります。

  • hourとminuteで時刻を表す。秒の情報なない
  • hourOfPeriodで午前/午後の時を取得
  • periodOffsetで現在の期間が始まる時間を取得
  • periodで午前/午後を表すDayPeriodを取得
  • replacingで時刻を変更可能
  test('TimeOfDay', () {
    TimeOfDay pmTime = TimeOfDay(hour: 23, minute: 12);
    expect(pmTime.hour, 23);
    expect(pmTime.minute, 12);
    expect(pmTime.hourOfPeriod, 11);
    expect(pmTime.periodOffset, 12); // 現在の期間が始まる時間。
    expect(pmTime.period, DayPeriod.pm);
    expect(pmTime.replacing(hour: 0), TimeOfDay(hour: 0, minute: 12));
    expect(pmTime.replacing(minute: 0), TimeOfDay(hour: 23, minute: 0));

    TimeOfDay amTime = TimeOfDay(hour: 11, minute: 12);
    expect(amTime.hour, 11);
    expect(amTime.hourOfPeriod, 11);
    expect(amTime.periodOffset, 0); // 現在の期間が始まる時間。
    expect(amTime.period, DayPeriod.am);

    TimeOfDay midnight1 = TimeOfDay(hour: 0, minute: 0);
    expect(midnight1.period, DayPeriod.am);

    // 12時以降は常にpm扱い
    TimeOfDay midnight2 = TimeOfDay(hour: 24, minute: 0);
    expect(midnight2.hour, 24);
    expect(midnight2.period, DayPeriod.pm);

    expect(TimeOfDay.hoursPerDay, 24);
    expect(TimeOfDay.hoursPerPeriod, 12);
    expect(TimeOfDay.minutesPerHour, 60);
    expect(
      TimeOfDay.fromDateTime(DateTime(2024, 1, 1, 1, 2)),
      TimeOfDay(hour: 1, minute: 2),
    );
  });

保存や復元のための変換

文字列へ変換: toString

  test('toString', () {
    DateTime baseDate = DateTime(2024, 1, 2, 9);
    expect(baseDate.toString(), '2024-01-02 09:00:00.000');
    expect(baseDate.toIso8601String(), '2024-01-02T09:00:00.000');

    expect(baseDate.toUtc().toString(), '2024-01-02 00:00:00.000Z');
    expect(baseDate.toUtc().toIso8601String(), '2024-01-02T00:00:00.000Z');
  });

文字列の最後にZが

  • 現地時間:つかない
  • UTC:つく

文字列からDateTimeに変換: parse

parseできる形式と、できると思ったけどできなかった形式の一覧。tryParseを思い出して、途中から変わってます(throwAを使ったら長かったので)

  test('parseできる', () {
    DateTime baseDate = DateTime(2024, 1, 2, 9);
    expect(DateTime.parse('2024-01-02 09:00:00.0'), baseDate);
    expect(DateTime.parse('2024-01-02 09:00:00.000'), baseDate);
    expect(DateTime.parse('2024-01-02 09:00:00.000000'), baseDate);

    expect(DateTime.parse('2024-01-02 09:00:00'), baseDate);
    expect(DateTime.parse('2024-01-02 09:00'), baseDate);
    expect(DateTime.parse('2024-01-02T09'), baseDate);
    expect(DateTime.parse('2024-01-02T09:00'), baseDate);
    expect(DateTime.parse('2024-01-02T09:00:00'), baseDate);
    expect(DateTime.parse('2024-01-02'), DateTime(2024, 1, 2));

    expect(DateTime.parse('2024-01-02T00:00Z').toLocal(), baseDate);
  });

  test('parseできない', () {
    expect(() => DateTime.parse('2024/01/02'), throwsA(isA<FormatException>()));
    expect(() => DateTime.parse('2024-1-2'), throwsA(isA<FormatException>()));
    expect(DateTime.tryParse('2024-01-02Z'), isNull);
    expect(DateTime.tryParse('2024-01-02TZ'), isNull);
    expect(DateTime.tryParse('2024-01-02T'), isNull);
   expect(DateTime.tryParse('09:00:00'), isNull);
  });

数値化された日付

年月日を表す形として数値にする場合がある。例えば2022年1月2日を「20220102」と表すケースです。それにDateTimeに変換するためのメソッドを作成します。「~/」は除算(小数切り捨て)

  test('日付を数値にしたものを変換', () {
    DateTime convertDateTime(int datetime) {
      int year = datetime ~/ 10000;
      int month = (datetime ~/ 100) % 100;
      int day = datetime % 100;

      return DateTime(year, month, day);
    }

    expect(convertDateTime(20220102), DateTime(2022, 1, 2));
  });

応用

年始年末月初月末を取得する

変な日付の応用例になってます。

  test('年始年末月初月末を取得する', () {
    DateTime baseDate = DateTime(2024, 3, 5);

    // 年始を取得
    DateTime startOfYear = DateTime(baseDate.year, 1, 1);
    expect(startOfYear, DateTime(2024, 1, 1));

    // 年末を取得
    DateTime endOfYear = DateTime(baseDate.year + 1, 1, 0);
    expect(endOfYear, DateTime(2024, 12, 31));

    // 今月初めを取得
    DateTime startOfMonth = DateTime(baseDate.year, baseDate.month, 1);
    expect(startOfMonth, DateTime(2024, 3, 1));

    // 今月末を取得
    DateTime endOfMonth = DateTime(baseDate.year, baseDate.month + 1, 0);
    expect(endOfMonth, DateTime(2024, 3, 31));

    // 先月末を取得
    DateTime endOfLastMonth = DateTime(baseDate.year, baseDate.month, 0);
    expect(endOfLastMonth, DateTime(2024, 2, 29));
  });

有効期限の日付

データベースに有効期限や有効期限の最終日を保存するのはよくあるケースです。例えば、「月末まで有効」とする場合、月末の日付をデータベースに保存し、それを取得してプログラム上で実行します。しかし、この方法には注意が必要です。直接月末の日付を保存すると、その日付の0時ちょうどを指すことになります。このため、実際にはまだ有効である月末の17時であっても、無効と判断されてしまう可能性があります。

正確に有効期限を扱うためには、月末の日付ではなく、月末の次の日の0時より前の時間であることを確認する必要があります。例えば、6月30日まで有効とする場合、実際の判定では7月1日の0時より前であることを確認します。

個人的には「無効になる日付」をデータベースに保存すれば良いと思いますが、分かりづらいと提案は却下されました。そのため、「最終日の次の日より前」であることを確認する方法で実装しました。この一手間を加えることで、正しい会員の有効情報が取得できます。

  test('今月末まで有効', () {
    DateTime endDay = DateTime(2024, 6, 30);

    // 間違った判定 最終日の17時はまだ有効のはず
    expect(DateTime(2024, 6, 29, 17).isBefore(endDay), isTrue);
    expect(
      DateTime(2024, 6, 30, 17).isBefore(endDay),
      isFalse, //有効であるので上記と同じisTrueにしたい
    );
    expect(DateTime(2024, 7, 1, 0).isBefore(endDay), isFalse);

    // 正しい判定
    DateTime notAvailableDay = endDay.add(const Duration(days: 1));
    expect(DateTime(2024, 6, 29, 17).isBefore(notAvailableDay), isTrue);
    expect(DateTime(2024, 6, 30, 17).isBefore(notAvailableDay), isTrue);
    expect(DateTime(2024, 7, 1, 0).isBefore(notAvailableDay), isFalse);
  });

DateTimeのパフォーマンス

DateTimeと他の時刻関連クラスの比較

DateTimeクラスは、DartおよびFlutterにおける日付と時刻の基本クラスですが、他にも時刻関連のクラスがあります。例えば、DurationクラスやStopwatchクラスがあり、それぞれ用途に応じて使い分けることが重要です。

DateTimeクラスは、具体的な日時の表現に適しています。一方、Durationクラスは時間の長さを表現するのに使用され、Stopwatchクラスは経過時間の計測に適しています。これらのクラスを適切に選択することで、パフォーマンスとコードの可読性が向上します。

このように、各クラスには特定の用途があり、それぞれの特性を理解して使い分けることで、アプリケーションのパフォーマンスを最大化できます。

パフォーマンス最適化のポイント

DateTimeの操作において、パフォーマンス最適化は重要なポイントです。特に、大量の日付データを扱う場合や、頻繁に日付計算を行う場合には注意が必要です。

パフォーマンス最適化のためのいくつかのポイントを以下に示します:

  • インスタンスの再利用: DateTimeオブジェクトの再利用を検討しましょう。頻繁に新しいインスタンスを作成するよりも、既存のインスタンスを再利用する方が効率的です。
  • 適切なクラスの選択: 日付計算にはDateTimeクラス、時間の長さにはDurationクラス、経過時間の計測にはStopwatchクラスを使うなど、用途に応じたクラスを選択します。
  • ミリ秒精度の注意: DateTimeクラスはミリ秒単位での精度を持っていますが、すべてのプラットフォームで同じ精度を持つわけではありません。必要に応じて、他の精度の高いライブラリを使用することを検討します。

これらのポイントを踏まえて実装することで、アプリケーションのパフォーマンスを向上させることができます。

日付と時間の切り出し

日付や時刻のTrimとか便利メソッドありそうですが、Dart標準ではないです。以下の新しいDateTimeを作成します。

  test('日付と時間の切り出し', () {
    DateTime baseDateTime = DateTime(2024, 3, 5, 6, 7, 8);
    DateTime onlyDate =
        DateTime(baseDateTime.year, baseDateTime.month, baseDateTime.day);
    expect(onlyDate, DateTime(2024, 3, 5));

    TimeOfDay onlyTime =
        TimeOfDay(hour: baseDateTime.hour, minute: baseDateTime.minute);
    expect(onlyTime.hour, 6);
    expect(onlyTime.minute, 7);
  });

Q&A

Q1: DateTimeクラスとは何ですか?

A1: DateTimeクラスは、DartおよびFlutterで日付と時刻を扱うための基本的なクラスです。これを使用することで、現在の日時の取得、特定の日時の設定、日付と時刻の演算が簡単に行えます。アプリケーションの時間管理に不可欠です。

Q2: 現地時間とUTCの変換はどのように行いますか?

A2: 現地時間とUTCの変換は、DateTimeクラスのtoUtc()toLocal()メソッドを使用して行います。例えば、DateTime.now().toUtc()で現在の日時をUTCに変換し、toLocal()で現地時間に戻すことができます。

Q3: DateFormatクラスはどのように使用しますか?

A3: DateFormatクラスを使用すると、DateTimeオブジェクトを指定された形式の文字列に変換できます。例えば、DateFormat('yyyy-MM-dd – kk:mm').format(DateTime.now())で、現在の日時を「年-月-日 – 時:分」の形式で表示できます。

Q4: DateTimeから日付や時刻だけ切り出す方法はありますか

標準では専用のメソッドはありません。ここを参照してに自作しましょう。(パッケージにあるかな)

まとめ

この記事を通じて、DateTimeの基本、操作、フォーマット、タイムゾーン、パフォーマンスについて学びました。DateTimeクラスの基本的な使い方やインスタンス化、プロパティとメソッドを理解しました。日付と時刻の取得、設定、比較、加減算の操作方法も学びました。さらに、DateFormatクラスを使ったフォーマット方法、カスタムフォーマットの作成、ロケール対応の方法を勉強しました。タイムゾーンの変換や指定についても理解し、DateTimeと他の時刻関連クラスとの比較やパフォーマンス最適化のポイントについても学びました。これらの知識を活用して、効率的な時間管理を行えるようになりました。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart';
import 'package:timezone/data/latest.dart';

void main() {
  DateTime now = DateTime.now();
  print('現在の日時: $now');

  DateTime specificDate = DateTime(2023, 6, 12, 14, 30);
  print('特定の日時: $specificDate');

  test('DateTimeのプロパティ', () {
    DateTime datetime = DateTime(2023, 6, 12, 14, 30);
    expect(datetime.year, 2023);
    expect(datetime.month, 6);
    expect(datetime.day, 12);
    expect(datetime.hour, 14);
    expect(datetime.minute, 30);
    expect(datetime.second, 0);
    expect(datetime.millisecond, 0);
    expect(datetime.microsecond, 0);
  });

  test('変な日付', () {
    expect(DateTime(2024, 1, 0), DateTime(2023, 12, 31));
    expect(DateTime(2024, 1, 32), DateTime(2024, 2, 1));
    expect(DateTime(2024, 0, 1), DateTime(2023, 12, 1));
    expect(DateTime(2024, -1, 1), DateTime(2023, 11, 1));
    expect(DateTime(2024, 13, 1), DateTime(2025, 1, 1));
  });

  test('日付と時刻の比較', () {
    DateTime pastDate = DateTime(2024, 1, 1);
    DateTime baseDate = DateTime(2024, 1, 2);
    DateTime sameDate = DateTime(2024, 1, 2);
    DateTime futureDate = DateTime(2024, 1, 3);

    expect(baseDate.isBefore(pastDate), false);
    expect(baseDate.isBefore(sameDate), false);
    expect(baseDate.isBefore(futureDate), true);

    expect(baseDate.isAfter(pastDate), true);
    expect(baseDate.isAfter(sameDate), false);
    expect(baseDate.isAfter(futureDate), false);

    expect(!baseDate.isBefore(pastDate), true);
    expect(!baseDate.isBefore(sameDate), true);
    expect(!baseDate.isBefore(futureDate), false);

    expect(!baseDate.isAfter(pastDate), false);
    expect(!baseDate.isAfter(sameDate), true);
    expect(!baseDate.isAfter(futureDate), true);

    expect(baseDate.isAtSameMomentAs(pastDate), false);
    expect(baseDate.isAtSameMomentAs(sameDate), true);
    expect(baseDate.isAtSameMomentAs(futureDate), false);
  });

  test('時間の加減算', () {
    DateTime baseDate = DateTime(2024, 1, 2);

    expect(
        baseDate.add(const Duration(
            days: 1,
            hours: 2,
            minutes: 3,
            seconds: 4,
            milliseconds: 5,
            microseconds: 6)),
        DateTime(2024, 1, 3, 2, 3, 4, 5, 6));

    expect(
      baseDate.add(const Duration(days: 50)),
      DateTime(2024, 2, 21),
    );

    // 10日前の日時を計算
    DateTime pastDate = baseDate.subtract(const Duration(days: 1));
    expect(pastDate, DateTime(2024, 1, 1));

    // 13ヶ月後の日時を計算(Durationは月をサポートしていないため、手動で計算)
    expect(DateTime(baseDate.year, baseDate.month + 13, baseDate.day),
        DateTime(2025, 2, 2));

    // 1年後の日時を計算
    expect(DateTime(baseDate.year + 1, baseDate.month, baseDate.day),
        DateTime(2025, 1, 2));
  });

  test('日時の差分', () {
    DateTime baseDate = DateTime(2024, 1, 2);

    expect(DateTime(2024, 1, 2 + 90, 1).difference(baseDate),
        const Duration(days: 90, hours: 1));

    DateTime threeDays = DateTime(2024, 1, 2 + 3);
    expect(threeDays.difference(baseDate), const Duration(days: 3));
    expect(threeDays.difference(baseDate).inDays, 3);
    expect(threeDays.difference(baseDate).inHours, 24 * 3);
    expect(threeDays.difference(baseDate).inMinutes, 60 * 24 * 3);
  });

  test('DateFormatクラスの使用', () {
    DateTime baseDate = DateTime(2024, 1, 2, 3, 4);
    String formattedDate = DateFormat('yyyy-MM-dd – kk:mm').format(baseDate);
    expect(formattedDate, '2024-01-02 – 03:04');

    DateTime midnight = DateTime(2024, 1, 2);
    expect(DateFormat('hh:mm a').format(midnight), '12:00 AM');
    expect(DateFormat('HH:mm a').format(midnight), '00:00 AM');
    expect(DateFormat('kk:mm a').format(midnight), '24:00 AM');
    expect(DateFormat('H時m分').format(midnight), '0時0分');
    expect(DateFormat('HH時mm分').format(midnight), '00時00分');
  });

  test('yyyy/MM/dd hh:mm:ss', () {
    DateFormat dateFormat = DateFormat('yyyy/MM/dd HH:mm:ss');

    expect(dateFormat.format(DateTime(2023, 1, 1)), '2023/01/01 00:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 0)), '2023/01/01 00:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 12)), '2023/01/01 12:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 13)), '2023/01/01 13:00:00');
    expect(dateFormat.format(DateTime(2023, 1, 1, 24)), '2023/01/02 00:00:00');

    expect(dateFormat.parse('2023/01/01 00:00:00'), DateTime(2023, 1, 1));
  });

  test('さらなるカスタム', () {
    DateTime baseDate = DateTime(2024, 1, 2, 3, 4);
    expect(DateFormat('E, M d, y').format(baseDate), 'Tue, 1 2, 2024');
    expect(DateFormat('EE, MM dd, yy').format(baseDate), 'Tue, 01 02, 24');
    expect(
      DateFormat('EEE, MMM ddd, yyy').format(baseDate),
      'Tue, Jan 002, 2024',
    );
    expect(
      DateFormat('EEEE, MMMM dddd, yyyy').format(baseDate),
      'Tuesday, January 0002, 2024',
    );

    expect(DateFormat('M').format(baseDate), '1');
    expect(DateFormat('L').format(baseDate), '1');
    expect(DateFormat('d').format(baseDate), '2');
    expect(DateFormat('c').format(baseDate), '2');

    expect(DateFormat('G yyyy D').format(baseDate), 'AD 2024 2');
    expect(
        DateFormat('G yyyy D').format(baseDate.add(const Duration(days: 50))),
        'AD 2024 52');
  });

  test('ロケール対応のフォーマット', () async {
    DateTime baseDate = DateTime(2024, 1, 2);
    await initializeDateFormatting();

    // 日本語でのフォーマット
    expect(DateFormat.yMMMMEEEEd('ja_JP').format(baseDate), '2024年1月2日火曜日');
    // 英語(アメリカ)でのフォーマット
    expect(DateFormat.yMMMMEEEEd('en_US').format(baseDate),
        'Tuesday, January 2, 2024');

    // 月と年のみのフォーマット
    expect(DateFormat.yM('ja_JP').format(baseDate), '2024/1');
    expect(DateFormat.yM('en_US').format(baseDate), '1/2024');

    // 年、月、日の日付フォーマット
    expect(DateFormat.yMd('ja_JP').format(baseDate), '2024/1/2');
    expect(DateFormat.yMd('en_US').format(baseDate), '1/2/2024');

    // 年、月、日、曜日の日付フォーマット
    expect(DateFormat.yMEd('ja_JP').format(baseDate), '2024/1/2(火)');
    expect(DateFormat.yMEd('en_US').format(baseDate), 'Tue, 1/2/2024');

    // 月と年、月名のフォーマット
    expect(DateFormat.yMMM('ja_JP').format(baseDate), '2024年1月');
    expect(DateFormat.yMMM('en_US').format(baseDate), 'Jan 2024');

    // 年、月、日、月名の日付フォーマット
    expect(DateFormat.yMMMd('ja_JP').format(baseDate), '2024年1月2日');
    expect(DateFormat.yMMMd('en_US').format(baseDate), 'Jan 2, 2024');

    // 年、月、日、曜日、月名の日付フォーマット
    expect(DateFormat.yMMMEd('ja_JP').format(baseDate), '2024年1月2日(火)');
    expect(DateFormat.yMMMEd('en_US').format(baseDate), 'Tue, Jan 2, 2024');

    // 年、月、日、月名の日付フォーマット
    expect(DateFormat.yMMMM('ja_JP').format(baseDate), '2024年1月');
    expect(DateFormat.yMMMM('en_US').format(baseDate), 'January 2024');

    // 年、月、日、曜日、月名の日付フォーマット
    expect(DateFormat.yMMMMd('ja_JP').format(baseDate), '2024年1月2日');
    expect(DateFormat.yMMMMd('en_US').format(baseDate), 'January 2, 2024');

    // 年、月、日、曜日、月名の日付フォーマット
    expect(DateFormat.yMMMMEEEEd('ja_JP').format(baseDate), '2024年1月2日火曜日');
    expect(DateFormat.yMMMMEEEEd('en_US').format(baseDate),
        'Tuesday, January 2, 2024');

    // 年、四半期のフォーマット
    expect(DateFormat.yQQQ('ja_JP').format(baseDate), '2024/Q1');
    expect(DateFormat.yQQQ('en_US').format(baseDate), 'Q1 2024');

    // 年、四半期のフォーマット
    expect(DateFormat.yQQQQ('ja_JP').format(baseDate), '2024年第1四半期');
    expect(DateFormat.yQQQQ('en_US').format(baseDate), '1st quarter 2024');
  });

  test('現地時間とUTCの変換(日本で実施されることが前提。タイムゾーンに依存するため、環境によって異なる可能性があります)', () {
    DateTime baseDate = DateTime(2024, 1, 2, 9, 1);
    expect(baseDate.toUtc(), DateTime.utc(2024, 1, 2, 0, 1));

    expect(DateTime.utc(2024, 1, 2).toLocal(), DateTime(2024, 1, 2, 9));
  });

  test('toString', () {
    DateTime baseDate = DateTime(2024, 1, 2, 9);
    expect(baseDate.toString(), '2024-01-02 09:00:00.000');
    expect(baseDate.toIso8601String(), '2024-01-02T09:00:00.000');

    expect(baseDate.toUtc().toString(), '2024-01-02 00:00:00.000Z');
    expect(baseDate.toUtc().toIso8601String(), '2024-01-02T00:00:00.000Z');
  });

  test('parseできる', () {
    DateTime baseDate = DateTime(2024, 1, 2, 9);
    expect(DateTime.parse('2024-01-02 09:00:00.0'), baseDate);
    expect(DateTime.parse('2024-01-02 09:00:00.000'), baseDate);
    expect(DateTime.parse('2024-01-02 09:00:00.000000'), baseDate);

    expect(DateTime.parse('2024-01-02 09:00:00'), baseDate);
    expect(DateTime.parse('2024-01-02 09:00'), baseDate);
    expect(DateTime.parse('2024-01-02T09'), baseDate);
    expect(DateTime.parse('2024-01-02T09:00'), baseDate);
    expect(DateTime.parse('2024-01-02T09:00:00'), baseDate);
    expect(DateTime.parse('2024-01-02'), DateTime(2024, 1, 2));

    expect(DateTime.parse('20240102'), DateTime(2024, 1, 2));
    expect(DateTime.parse('20240102 09:03'), DateTime(2024, 1, 2, 9, 3));
    expect(DateTime.parse('20240102 09:03:04'), DateTime(2024, 1, 2, 9, 3, 4));

    expect(DateTime.parse('2024-01-02T00:00Z').toLocal(), baseDate);
  });

  test('parseできない', () {
    expect(() => DateTime.parse('2024/01/02'), throwsA(isA<FormatException>()));
    expect(() => DateTime.parse('2024-1-2'), throwsA(isA<FormatException>()));
    expect(DateTime.tryParse('2024-01-02Z'), isNull);
    expect(DateTime.tryParse('2024-01-02TZ'), isNull);
    expect(DateTime.tryParse('2024-01-02T'), isNull);
   expect(DateTime.tryParse('09:00:00'), isNull);
  });

  test('日付を数値にしたものを変換', () {
    DateTime convertDateTime(int datetime) {
      int year = datetime ~/ 10000;
      int month = (datetime ~/ 100) % 100;
      int day = datetime % 100;

      return DateTime(year, month, day);
    }

    expect(convertDateTime(20220102), DateTime(2022, 1, 2));
  });

  test('タイムゾーンの指定', () async {
    initializeTimeZones();

    DateTime baseDate = DateTime(2024, 1, 2, 14);

    // タイムゾーンを指定して日時を取得
    final TZDateTime tokyoTime =
        TZDateTime.from(baseDate, getLocation('Asia/Tokyo'));
    final TZDateTime nyTime =
        TZDateTime.from(baseDate, getLocation('America/New_York'));

    expect(tokyoTime.copyWith(), DateTime(2024, 1, 2, 14));
    expect(tokyoTime.isAtSameMomentAs(DateTime(2024, 1, 2, 14)), true);
    expect(tokyoTime.toString(), '2024-01-02 14:00:00.000+0900');
    expect(tokyoTime.toIso8601String(), '2024-01-02T14:00:00.000+0900');

    // ニューヨークは東京より14時間遅れ
    expect(nyTime.copyWith(), DateTime(2024, 1, 2, 0));
    expect(nyTime.isAtSameMomentAs(DateTime(2024, 1, 2, 14)), true);
  });

  test('年始年末月初月末を取得する', () {
    DateTime baseDate = DateTime(2024, 3, 5);

    // 年始を取得
    DateTime startOfYear = DateTime(baseDate.year, 1, 1);
    expect(startOfYear, DateTime(2024, 1, 1));

    // 年末を取得
    DateTime endOfYear = DateTime(baseDate.year + 1, 1, 0);
    expect(endOfYear, DateTime(2024, 12, 31));

    // 今月初めを取得
    DateTime startOfMonth = DateTime(baseDate.year, baseDate.month, 1);
    expect(startOfMonth, DateTime(2024, 3, 1));

    // 今月末を取得
    DateTime endOfMonth = DateTime(baseDate.year, baseDate.month + 1, 0);
    expect(endOfMonth, DateTime(2024, 3, 31));

    // 先月末を取得
    DateTime endOfLastMonth = DateTime(baseDate.year, baseDate.month, 0);
    expect(endOfLastMonth, DateTime(2024, 2, 29));
  });

  test('今月末まで有効', () {
    DateTime endDay = DateTime(2024, 6, 30);

    // 間違った判定
    // 当日の17時はまだ有効のはず
    expect(DateTime(2024, 6, 29, 17).isBefore(endDay), isTrue);
    expect(
      DateTime(2024, 6, 30, 17).isBefore(endDay),
      isFalse, //有効であるので上記と同じにしたい
    );
    expect(DateTime(2024, 7, 1, 0).isBefore(endDay), isFalse);

    // 正しい判定
    DateTime notAvailableDay = endDay.add(const Duration(days: 1));
    expect(DateTime(2024, 6, 29, 17).isBefore(notAvailableDay), isTrue);
    expect(DateTime(2024, 6, 30, 17).isBefore(notAvailableDay), isTrue);
    expect(DateTime(2024, 7, 1, 0).isBefore(notAvailableDay), isFalse);
  });

  test('日付と時間の切り出し', () {
    DateTime baseDateTime = DateTime(2024, 3, 5, 6, 7, 8);
    DateTime onlyDate =
        DateTime(baseDateTime.year, baseDateTime.month, baseDateTime.day);
    expect(onlyDate, DateTime(2024, 3, 5));

    TimeOfDay onlyTime =
        TimeOfDay(hour: baseDateTime.hour, minute: baseDateTime.minute);
    expect(onlyTime.hour, 6);
    expect(onlyTime.minute, 7);
  });
}