Dart Advanced Features

Date, Time & Duration

45 min Lesson 12 of 16

The DateTime Class

Dart’s DateTime class is the foundation for working with dates and times. It represents a point in time, either in UTC or the local time zone. Understanding DateTime is essential for scheduling, logging, data processing, and any time-sensitive application.

Creating DateTime Objects

void main() {
  // Current date and time
  final now = DateTime.now();
  print(now);  // 2024-03-15 14:30:00.000

  // Specific date and time
  final specific = DateTime(2024, 3, 15, 14, 30, 0);
  print(specific);  // 2024-03-15 14:30:00.000

  // UTC time
  final utc = DateTime.utc(2024, 3, 15, 14, 30);
  print(utc);       // 2024-03-15 14:30:00.000Z
  print(utc.isUtc); // true

  // Only date (time defaults to 00:00:00)
  final dateOnly = DateTime(2024, 3, 15);
  print(dateOnly);  // 2024-03-15 00:00:00.000

  // Accessing components
  print('Year: ${now.year}');
  print('Month: ${now.month}');     // 1-12
  print('Day: ${now.day}');          // 1-31
  print('Hour: ${now.hour}');        // 0-23
  print('Minute: ${now.minute}');    // 0-59
  print('Second: ${now.second}');    // 0-59
  print('Millisecond: ${now.millisecond}');
  print('Microsecond: ${now.microsecond}');
  print('Weekday: ${now.weekday}');  // 1=Monday ... 7=Sunday
}
Note: Dart’s DateTime months are 1-based (January = 1, December = 12) and weekdays follow ISO 8601 (Monday = 1, Sunday = 7). This differs from some other languages where months are 0-based or Sunday = 0.

Parsing Dates with DateTime.parse

The DateTime.parse and DateTime.tryParse methods convert strings into DateTime objects. They accept a subset of ISO 8601 formats.

Parsing Date Strings

void main() {
  // Standard ISO 8601 formats
  final d1 = DateTime.parse('2024-03-15');
  print(d1);  // 2024-03-15 00:00:00.000

  final d2 = DateTime.parse('2024-03-15 14:30:00');
  print(d2);  // 2024-03-15 14:30:00.000

  final d3 = DateTime.parse('2024-03-15T14:30:00Z');  // UTC
  print(d3);       // 2024-03-15 14:30:00.000Z
  print(d3.isUtc); // true

  final d4 = DateTime.parse('2024-03-15T14:30:00+05:00');  // With offset
  print(d4.toUtc());  // 2024-03-15 09:30:00.000Z

  // Safe parsing with tryParse
  final valid = DateTime.tryParse('2024-03-15');
  final invalid = DateTime.tryParse('not-a-date');
  print(valid);    // 2024-03-15 00:00:00.000
  print(invalid);  // null

  // IMPORTANT: DateTime.parse does NOT handle formats like '03/15/2024'
  // For custom formats, use the intl package (shown later)
}

UTC vs Local Time

Understanding the difference between UTC and local time is crucial for applications that deal with users in different time zones, server logs, or API communications.

UTC and Local Time Conversion

void main() {
  // Local time
  final local = DateTime.now();
  print('Local: $local');
  print('Is UTC: ${local.isUtc}');         // false
  print('Timezone: ${local.timeZoneName}');  // e.g., AST, EST, PST
  print('Offset: ${local.timeZoneOffset}');  // e.g., 3:00:00.000000

  // Convert local to UTC
  final utc = local.toUtc();
  print('UTC: $utc');
  print('Is UTC: ${utc.isUtc}');  // true

  // Convert UTC back to local
  final backToLocal = utc.toLocal();
  print('Back to local: $backToLocal');

  // Create UTC directly
  final utcDirect = DateTime.utc(2024, 3, 15, 12, 0);
  print(utcDirect);         // 2024-03-15 12:00:00.000Z
  print(utcDirect.toLocal()); // Depends on local timezone

  // millisecondsSinceEpoch — universal timestamp
  final epoch = local.millisecondsSinceEpoch;
  print('Epoch ms: $epoch');

  // Reconstruct from epoch
  final fromEpoch = DateTime.fromMillisecondsSinceEpoch(epoch);
  print('From epoch: $fromEpoch');
  final fromEpochUtc = DateTime.fromMillisecondsSinceEpoch(epoch, isUtc: true);
  print('From epoch (UTC): $fromEpochUtc');
}
Tip: Always store and transmit dates in UTC format. Convert to local time only for display purposes. This prevents bugs when users are in different time zones, when servers use a different timezone than clients, or during daylight saving time transitions.

The Duration Class

The Duration class represents a span of time. It is used for date arithmetic, timeouts, delays, and measuring elapsed time.

Working with Duration

void main() {
  // Creating Duration objects
  final fiveMinutes = Duration(minutes: 5);
  final oneHour = Duration(hours: 1);
  final complex = Duration(hours: 2, minutes: 30, seconds: 15);

  print(fiveMinutes);         // 0:05:00.000000
  print(oneHour);             // 1:00:00.000000
  print(complex);             // 2:30:15.000000

  // Accessing components
  print('In minutes: ${complex.inMinutes}');       // 150
  print('In seconds: ${complex.inSeconds}');       // 9015
  print('In milliseconds: ${complex.inMilliseconds}');  // 9015000

  // Duration arithmetic
  final total = fiveMinutes + oneHour;
  print('Total: $total');  // 1:05:00.000000

  final difference = oneHour - fiveMinutes;
  print('Difference: $difference');  // 0:55:00.000000

  final doubled = fiveMinutes * 2;
  print('Doubled: $doubled');  // 0:10:00.000000

  // Negative durations
  final negative = Duration(hours: -2);
  print(negative);                // -2:00:00.000000
  print(negative.isNegative);     // true
  print(negative.abs());          // 2:00:00.000000

  // Comparison
  print(fiveMinutes < oneHour);   // true
  print(fiveMinutes > oneHour);   // false
  print(fiveMinutes == Duration(seconds: 300));  // true
}

Date Arithmetic

You can add or subtract Duration from DateTime to calculate future or past dates. For calendar-aware operations (like adding months), you need manual calculations.

Adding and Subtracting Time

void main() {
  final now = DateTime(2024, 3, 15, 10, 0);

  // Add duration
  final inTwoHours = now.add(Duration(hours: 2));
  print(inTwoHours);  // 2024-03-15 12:00:00.000

  final tomorrow = now.add(Duration(days: 1));
  print(tomorrow);  // 2024-03-16 10:00:00.000

  final nextWeek = now.add(Duration(days: 7));
  print(nextWeek);  // 2024-03-22 10:00:00.000

  // Subtract duration
  final yesterday = now.subtract(Duration(days: 1));
  print(yesterday);  // 2024-03-14 10:00:00.000

  // Difference between two dates
  final start = DateTime(2024, 1, 1);
  final end = DateTime(2024, 12, 31);
  final diff = end.difference(start);
  print('Days between: ${diff.inDays}');  // 365

  // Adding months (calendar-aware — must be done manually)
  DateTime addMonths(DateTime date, int months) {
    var year = date.year;
    var month = date.month + months;
    while (month > 12) {
      month -= 12;
      year++;
    }
    while (month < 1) {
      month += 12;
      year--;
    }
    // Clamp day to valid range for the target month
    final maxDay = DateTime(year, month + 1, 0).day;
    final day = date.day > maxDay ? maxDay : date.day;
    return DateTime(year, month, day, date.hour, date.minute, date.second);
  }

  final jan31 = DateTime(2024, 1, 31);
  print(addMonths(jan31, 1));  // 2024-02-29 (leap year, clamped!)
  print(addMonths(jan31, 2));  // 2024-03-31
}
Warning: Adding Duration(days: 30) does not correctly add “one month” because months have different lengths (28-31 days). Always use calendar-aware logic when working with months or years. Similarly, adding Duration(days: 365) is not always “one year” due to leap years.

Comparing Dates

Dart provides several ways to compare DateTime objects. Understanding comparison semantics is important for sorting, filtering, and scheduling.

Date Comparison Methods

void main() {
  final date1 = DateTime(2024, 3, 15);
  final date2 = DateTime(2024, 6, 20);
  final date3 = DateTime(2024, 3, 15);

  // Comparison operators
  print(date1.isBefore(date2));  // true
  print(date1.isAfter(date2));   // false
  print(date1.isAtSameMomentAs(date3));  // true

  // compareTo for sorting
  print(date1.compareTo(date2));  // negative (date1 is before date2)
  print(date2.compareTo(date1));  // positive
  print(date1.compareTo(date3));  // 0 (equal)

  // Sorting a list of dates
  final dates = [
    DateTime(2024, 12, 25),
    DateTime(2024, 1, 1),
    DateTime(2024, 7, 4),
    DateTime(2024, 3, 15),
  ];
  dates.sort((a, b) => a.compareTo(b));
  print(dates.map((d) => '${d.month}/${d.day}').toList());
  // [1/1, 3/15, 7/4, 12/25]

  // Check if a date is within a range
  bool isInRange(DateTime date, DateTime start, DateTime end) {
    return !date.isBefore(start) && !date.isAfter(end);
  }

  final checkDate = DateTime(2024, 5, 10);
  print(isInRange(checkDate, date1, date2));  // true
}

Formatting Dates with the intl Package

Dart’s built-in DateTime.toString() produces ISO format, which is not user-friendly. The intl package provides DateFormat for locale-aware, customizable formatting.

Date Formatting with DateFormat

import 'package:intl/intl.dart';

void main() {
  final now = DateTime(2024, 3, 15, 14, 30, 0);

  // Predefined formats
  print(DateFormat.yMMMd().format(now));       // Mar 15, 2024
  print(DateFormat.yMMMMEEEEd().format(now));  // Friday, March 15, 2024
  print(DateFormat.Hm().format(now));          // 14:30
  print(DateFormat.jm().format(now));          // 2:30 PM

  // Custom patterns
  print(DateFormat('yyyy-MM-dd').format(now));           // 2024-03-15
  print(DateFormat('dd/MM/yyyy').format(now));           // 15/03/2024
  print(DateFormat('EEEE, MMMM d, y').format(now));     // Friday, March 15, 2024
  print(DateFormat('h:mm a').format(now));                // 2:30 PM
  print(DateFormat('MMM d, y ''at'' h:mm a').format(now)); // Mar 15, 2024 at 2:30 PM

  // Locale-specific formatting
  print(DateFormat.yMMMd('ar').format(now));  // ١٥ مارس ٢٠٢٤
  print(DateFormat.yMMMd('fr').format(now));  // 15 mars 2024
  print(DateFormat.yMMMd('ja').format(now));  // 2024/03/15

  // Parsing with DateFormat
  final parsed = DateFormat('MM/dd/yyyy').parse('03/15/2024');
  print(parsed);  // 2024-03-15 00:00:00.000
}
Note: To use the intl package, add it to your pubspec.yaml: dependencies: intl: ^0.19.0. The package also provides number formatting, plural rules, and message translation capabilities beyond just dates.

Stopwatch for Measuring Elapsed Time

The Stopwatch class provides precise time measurement for performance profiling and benchmarking.

Using Stopwatch

void main() {
  final stopwatch = Stopwatch();

  // Start measuring
  stopwatch.start();

  // Simulate work
  int sum = 0;
  for (var i = 0; i < 1000000; i++) {
    sum += i;
  }

  // Stop and read
  stopwatch.stop();
  print('Elapsed: ${stopwatch.elapsed}');
  print('Milliseconds: ${stopwatch.elapsedMilliseconds}');
  print('Microseconds: ${stopwatch.elapsedMicroseconds}');

  // Reset and reuse
  stopwatch.reset();
  print('After reset: ${stopwatch.elapsedMilliseconds}');  // 0

  // Lap timing helper
  stopwatch.start();
  // ... phase 1 ...
  final lap1 = stopwatch.elapsedMilliseconds;
  // ... phase 2 ...
  final lap2 = stopwatch.elapsedMilliseconds;
  stopwatch.stop();

  print('Phase 1: ${lap1}ms');
  print('Phase 2: ${lap2 - lap1}ms');
  print('Total: ${stopwatch.elapsedMilliseconds}ms');
}

Practical Example: Age Calculator

Calculating a person’s age requires careful handling of months and days, not just subtracting years.

Precise Age Calculator

class Age {
  final int years;
  final int months;
  final int days;

  Age(this.years, this.months, this.days);

  @override
  String toString() => '$years years, $months months, $days days';
}

Age calculateAge(DateTime birthDate, [DateTime? referenceDate]) {
  final now = referenceDate ?? DateTime.now();

  int years = now.year - birthDate.year;
  int months = now.month - birthDate.month;
  int days = now.day - birthDate.day;

  if (days < 0) {
    months--;
    // Days in the previous month
    final prevMonth = DateTime(now.year, now.month, 0);
    days += prevMonth.day;
  }

  if (months < 0) {
    years--;
    months += 12;
  }

  return Age(years, months, days);
}

void main() {
  final birthday = DateTime(1995, 8, 15);
  final today = DateTime(2024, 3, 15);

  final age = calculateAge(birthday, today);
  print('Age: $age');  // Age: 28 years, 7 months, 0 days

  // Days until next birthday
  var nextBirthday = DateTime(today.year, birthday.month, birthday.day);
  if (nextBirthday.isBefore(today) || nextBirthday.isAtSameMomentAs(today)) {
    nextBirthday = DateTime(today.year + 1, birthday.month, birthday.day);
  }
  final daysUntil = nextBirthday.difference(today).inDays;
  print('Days until next birthday: $daysUntil');  // 153
}

Practical Example: Countdown Timer

A countdown timer calculates the remaining time until a target date and displays it in a human-readable format.

Countdown Timer

class Countdown {
  final DateTime target;

  Countdown(this.target);

  Duration get remaining {
    final now = DateTime.now();
    if (now.isAfter(target)) return Duration.zero;
    return target.difference(now);
  }

  bool get isExpired => DateTime.now().isAfter(target);

  String get formatted {
    final r = remaining;
    if (r == Duration.zero) return 'Expired!';

    final days = r.inDays;
    final hours = r.inHours % 24;
    final minutes = r.inMinutes % 60;
    final seconds = r.inSeconds % 60;

    final parts = <String>[];
    if (days > 0) parts.add('$days day${days == 1 ? '' : 's'}');
    if (hours > 0) parts.add('$hours hour${hours == 1 ? '' : 's'}');
    if (minutes > 0) parts.add('$minutes minute${minutes == 1 ? '' : 's'}');
    if (seconds > 0) parts.add('$seconds second${seconds == 1 ? '' : 's'}');

    return parts.join(', ');
  }
}

void main() {
  // Countdown to New Year 2025
  final newYear = DateTime(2025, 1, 1);
  final countdown = Countdown(newYear);

  print('Time until New Year: ${countdown.formatted}');
  print('Is expired: ${countdown.isExpired}');
}

Practical Example: Date Range Utilities

Date range operations are common in scheduling, reporting, and calendar applications.

Date Range Class

class DateRange {
  final DateTime start;
  final DateTime end;

  DateRange(this.start, this.end) : assert(!end.isBefore(start));

  Duration get duration => end.difference(start);
  int get dayCount => duration.inDays + 1;  // Inclusive

  bool contains(DateTime date) {
    return !date.isBefore(start) && !date.isAfter(end);
  }

  bool overlaps(DateRange other) {
    return !end.isBefore(other.start) && !start.isAfter(other.end);
  }

  DateRange? intersection(DateRange other) {
    if (!overlaps(other)) return null;
    final s = start.isAfter(other.start) ? start : other.start;
    final e = end.isBefore(other.end) ? end : other.end;
    return DateRange(s, e);
  }

  /// Generates all dates in the range (inclusive).
  Iterable<DateTime> get days sync* {
    var current = DateTime(start.year, start.month, start.day);
    final endDate = DateTime(end.year, end.month, end.day);
    while (!current.isAfter(endDate)) {
      yield current;
      current = current.add(Duration(days: 1));
    }
  }

  /// Returns only weekdays (Monday-Friday).
  Iterable<DateTime> get weekdays =>
      days.where((d) => d.weekday <= 5);

  @override
  String toString() => '$start - $end ($dayCount days)';
}

void main() {
  final q1 = DateRange(DateTime(2024, 1, 1), DateTime(2024, 3, 31));
  final march = DateRange(DateTime(2024, 3, 1), DateTime(2024, 3, 31));

  print('Q1: ${q1.dayCount} days');  // Q1: 91 days
  print('Q1 contains Mar 15: ${q1.contains(DateTime(2024, 3, 15))}');  // true
  print('Q1 overlaps March: ${q1.overlaps(march)}');  // true

  final overlap = q1.intersection(march);
  print('Overlap: $overlap');  // March 1-31

  // Business days in March 2024
  final businessDays = march.weekdays.length;
  print('Business days in March: $businessDays');  // 21
}

Practical Example: Simple Scheduling System

Combining DateTime, Duration, and date ranges to build a basic scheduling system.

Event Scheduler

class ScheduleEvent {
  final String title;
  final DateTime start;
  final Duration duration;

  ScheduleEvent(this.title, this.start, this.duration);

  DateTime get end => start.add(duration);

  bool conflictsWith(ScheduleEvent other) {
    return start.isBefore(other.end) && end.isAfter(other.start);
  }

  @override
  String toString() {
    final endTime = end;
    return '$title: ${_formatTime(start)} - ${_formatTime(endTime)}';
  }

  String _formatTime(DateTime dt) =>
      '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}

class Scheduler {
  final List<ScheduleEvent> _events = [];

  List<ScheduleEvent> get events => List.unmodifiable(_events);

  bool addEvent(ScheduleEvent event) {
    // Check for conflicts
    for (final existing in _events) {
      if (existing.conflictsWith(event)) {
        print('Conflict: "${event.title}" overlaps with "${existing.title}"');
        return false;
      }
    }
    _events.add(event);
    _events.sort((a, b) => a.start.compareTo(b.start));
    return true;
  }

  List<ScheduleEvent> eventsOn(DateTime date) {
    return _events.where((e) =>
        e.start.year == date.year &&
        e.start.month == date.month &&
        e.start.day == date.day).toList();
  }
}

void main() {
  final scheduler = Scheduler();
  final today = DateTime(2024, 3, 15);

  scheduler.addEvent(ScheduleEvent(
    'Team Meeting',
    DateTime(2024, 3, 15, 9, 0),
    Duration(hours: 1),
  ));

  scheduler.addEvent(ScheduleEvent(
    'Lunch',
    DateTime(2024, 3, 15, 12, 0),
    Duration(minutes: 45),
  ));

  // This will show a conflict
  scheduler.addEvent(ScheduleEvent(
    'Conflicting Call',
    DateTime(2024, 3, 15, 9, 30),
    Duration(minutes: 30),
  ));

  print('\nSchedule for today:');
  for (final event in scheduler.eventsOn(today)) {
    print('  $event');
  }
  // Team Meeting: 09:00 - 10:00
  // Lunch: 12:00 - 12:45
}
Tip: For production scheduling systems, consider using the timezone package for proper IANA timezone support. Dart’s built-in DateTime only supports UTC and the system’s local timezone, which is insufficient for applications that schedule events across multiple time zones.

Summary

Dart provides solid tools for date, time, and duration operations. Key takeaways:

  • DateTime represents a point in time; create with constructors, .now(), or .parse().
  • Always store/transmit in UTC; convert to local only for display.
  • Duration represents a span of time; use add() and subtract() for date arithmetic.
  • Adding months/years requires calendar-aware logic — Duration alone is not sufficient.
  • Use the intl package (DateFormat) for locale-aware formatting and parsing of custom date strings.
  • Stopwatch is ideal for measuring elapsed time in performance-critical code.
  • Build utilities like DateRange for overlap detection, iteration, and business day counting.