Timezone-aware DateTime for Dart
A drop-in replacement for DateTime with full IANA timezone support, intuitive arithmetic, and flexible formatting. Immutable, accurate, and developer-friendly.
Dart's built-in DateTime and existing libraries often face limitations when handling complex timezone scenarios:
| Solution | Behavior | This Library |
|---|---|---|
| DateTime | Implicitly converts parsed offset to UTC | Preserves original time values |
| timezone | Requires manual getLocation() calls |
Provides constants like TimeZones.tokyo |
| intl | Focused on formatting output | Compatible for combined use |
| jiffy | Mutable object design | Immutable, implements DateTime interface |
Comparison:
// ❌ Native DateTime: Implicitly converts to UTC/Local, losing the original offset context.
DateTime.parse('2025-12-07T10:30:00+08:00').hour // → 2 (Changed to UTC hour)
// ✅ EasyDateTime: Preserves the exact parsed hour and offset.
EasyDateTime.parse('2025-12-07T10:30:00+08:00').hour // → 10 (Preserved)Use standard IANA constants or custom strings.
final tokyo = EasyDateTime.now(location: TimeZones.tokyo);No implicit UTC conversion. Retains the exact parsed values.
EasyDateTime.parse('2025-12-07T10:00+08:00').hour // -> 10Natural syntax for date calculations.
final later = now + 2.hours + 30.minutes;Handles month overflow intelligently.
jan31.copyWithClamped(month: 2); // -> Feb 28Performance-optimized formatting.
dt.format('yyyy-MM-dd'); // -> 2025-12-07Add the following to your pubspec.yaml:
dependencies:
easy_date_time: ^0.4.2Note: You must initialize the timezone database before using the library.
void main() {
EasyDateTime.initializeTimeZone(); // Required
// Optional: Set a global default location
EasyDateTime.setDefaultLocation(TimeZones.shanghai);
runApp(MyApp());
}final now = EasyDateTime.now(); // Uses default or local timezone
final tokyo = EasyDateTime.now(location: TimeZones.tokyo);
final parsed = EasyDateTime.parse('2025-12-07T10:30:00+08:00');
print(parsed.hour); // 10Use pre-defined constants for common timezones:
final tokyo = EasyDateTime.now(location: TimeZones.tokyo);
final shanghai = EasyDateTime.now(location: TimeZones.shanghai);You can also use standard IANA strings:
final nairobi = EasyDateTime.now(location: getLocation('Africa/Nairobi'));Setting a default location allows EasyDateTime.now() to use that timezone globally.
EasyDateTime.setDefaultLocation(TimeZones.shanghai);
final now = EasyDateTime.now(); // Returns time in Asia/ShanghaiManaging the default timezone:
// Get the current default timezone
final current = EasyDateTime.getDefaultLocation();
// Clear the default (reverts to system local timezone)
EasyDateTime.clearDefaultLocation();
// Get the effective default location (user-set or system local)
final effective = EasyDateTime.effectiveDefaultLocation; // or effectiveDefaultLocationEasyDateTime preserves both the literal time and the timezone location, even when parsing strings with offsets:
final dt = EasyDateTime.parse('2025-12-07T10:30:00+08:00');
print(dt.hour); // 10
print(dt.locationName); // Asia/ShanghaiUse explicit methods to convert timezones:
final ny = dt.inLocation(TimeZones.newYork);
final utc = dt.toUtc();Comparing the same instant across different timezones:
final tokyo = EasyDateTime.now(location: TimeZones.tokyo);
final newYork = tokyo.inLocation(TimeZones.newYork);
print(tokyo.isAtSameMomentAs(newYork)); // true: Represents the same absolute instantfinal now = EasyDateTime.now();
final tomorrow = now + 1.days;
final later = now + 2.hours + 30.minutes;EasyDateTime provides safe handling for month overflows:
final jan31 = EasyDateTime.utc(2025, 1, 31);
jan31.copyWith(month: 2); // ⚠️ Mar 3rd (Standard overflow)
jan31.copyWithClamped(month: 2); // ✅ Feb 28 (Clamped to last valid day)Truncate or extend a datetime to the boundary of a time unit:
final dt = EasyDateTime(2025, 6, 18, 14, 30, 45); // Wednesday
dt.startOf(DateTimeUnit.day); // 2025-06-18 00:00:00
dt.startOf(DateTimeUnit.week); // 2025-06-16 00:00:00 (Monday)
dt.startOf(DateTimeUnit.month); // 2025-06-01 00:00:00
dt.endOf(DateTimeUnit.day); // 2025-06-18 23:59:59.999999
dt.endOf(DateTimeUnit.week); // 2025-06-22 23:59:59.999999 (Sunday)
dt.endOf(DateTimeUnit.month); // 2025-06-30 23:59:59.999999Week boundaries follow ISO 8601 (Monday = first day of week).
Use the format() method with pattern tokens for flexible date/time formatting:
final dt = EasyDateTime(2025, 12, 1, 14, 30, 45);
dt.format('yyyy-MM-dd'); // '2025-12-01'
dt.format('yyyy/MM/dd HH:mm:ss'); // '2025/12/01 14:30:45'
dt.format('MM/dd/yyyy'); // '12/01/2025'
dt.format('hh:mm a'); // '02:30 PM'Tip
Performance Optimization: For hot paths (e.g., loops), use EasyDateTimeFormatter to pre-compile patterns.
// Compiled once, reused multiple times
static final formatter = EasyDateTimeFormatter('yyyy-MM-dd HH:mm');
String result = formatter.format(date);Use DateTimeFormats for common patterns:
dt.format(DateTimeFormats.isoDate); // '2025-12-01'
dt.format(DateTimeFormats.isoTime); // '14:30:45'
dt.format(DateTimeFormats.isoDateTime); // '2025-12-01T14:30:45'
dt.format(DateTimeFormats.time12Hour); // '02:30 PM'
dt.format(DateTimeFormats.time24Hour); // '14:30'
dt.format(DateTimeFormats.rfc2822); // 'Mon, 01 Dec 2025 14:30:45 +0800'| Token | Description | Example |
|---|---|---|
yyyy |
4-digit year | 2025 |
MM/M |
Month (padded/unpadded) | 01, 1 |
MMM |
Month abbreviation | Jan, Dec |
dd/d |
Day (padded/unpadded) | 01, 1 |
EEE |
Day-of-week abbreviation | Mon, Sun |
HH/H |
24-hour (padded/unpadded) | 09, 9 |
hh/h |
12-hour (padded/unpadded) | 02, 2 |
mm/m |
Minutes (padded/unpadded) | 05, 5 |
ss/s |
Seconds (padded/unpadded) | 05, 5 |
SSS |
Milliseconds | 123 |
a |
AM/PM marker | AM, PM |
xxxxx |
Timezone offset with colon | +08:00, -05:00 |
xxxx |
Timezone offset | +0800, -0500 |
xx |
Short timezone offset | +08, -05 |
X |
ISO timezone (Z or +0800) | Z, +0800 |
This package adds extensions on int (e.g., 1.days). If this conflicts with other packages, hide the extension via specialized imports:
import 'package:easy_date_time/easy_date_time.dart' hide DurationExtension;For locale-aware formatting (e.g., "January" → "一月"), use EasyDateTime with the intl package:
import 'package:intl/intl.dart';
import 'package:easy_date_time/easy_date_time.dart';
final dt = EasyDateTime.now(location: TimeZones.tokyo);
// Locale-aware formatting via intl
DateFormat.yMMMMd('ja').format(dt); // '2025年12月20日'
DateFormat.yMMMMd('en').format(dt); // 'December 20, 2025'Note:
EasyDateTimeimplementsDateTime, so it works directly withDateFormat.format().
Compatible with json_serializable and freezed via a custom converter:
class EasyDateTimeConverter implements JsonConverter<EasyDateTime, String> {
const EasyDateTimeConverter();
@override
EasyDateTime fromJson(String json) => EasyDateTime.fromIso8601String(json);
@override
String toJson(EasyDateTime object) => object.toIso8601String();
}EasyDateTime follows Dart's DateTime semantics for equality:
final utc = EasyDateTime.utc(2025, 1, 1, 0, 0);
final local = EasyDateTime.parse('2025-01-01T08:00:00+08:00');
// Same moment, different timezone type (UTC vs non-UTC)
utc == local; // false
utc.isAtSameMomentAs(local); // true| Method | Compares | Use Case |
|---|---|---|
== |
Moment + timezone type (UTC/non-UTC) | Exact equality |
isAtSameMomentAs() |
Absolute instant only | Cross-timezone comparison |
isBefore() / isAfter() |
Chronological order | Sorting, range checks |
- Only valid IANA timezone offsets are supported; non-standard offsets will throw an error.
EasyDateTime.initializeTimeZone()must be called before use.
Use tryParse for handling potentially invalid user input safely:
final dt = EasyDateTime.tryParse(userInput);
if (dt == null) {
print('Invalid date format');
}Issues and Pull Requests are welcome. Please refer to CONTRIBUTING.md for guidelines.
BSD 2-Clause