Mobile Date TZ ⏰⚡️
mobile_date_tz is the Dart/Flutter edition of the original DateTz Swiss‑army knife. It keeps minute-precision timestamps rock-solid across IANA zones, slices through daylight-saving transitions, and ships an expressive formatting DSL—all without dragging in heavyweight dependencies.
Think of it as your time-travel sidekick: whether you’re building scheduling flows, dashboards, automations, or mobile reminders, it keeps every cross-zone hop perfectly aligned.
Feature Highlights
- DST precision without drama – Static offsets plus
timezonepackage integration give you correct answers even when the host OS doesn’t ship TZDB files. - Minute-accurate arithmetic – Internal timestamps are truncated to the minute so scheduled jobs never drift due to stray seconds.
- Expressive format tokens – Familiar patterns (
YYYY,hh,tz,LM, …) and literal escaping make string building painless. - Mutability by choice –
convertToTimezone,cloneToTimezone,add,set, and comparisons mirror the TypeScript API for effortless migration. - Production-ready pipeline – GitHub Actions workflow auto-bumps versions, runs tests, and publishes to pub.flutter-io.cn when you merge to
master. - Zero runtime ballast – Just Dart code. No mirrors, no build_runner, no platform channels.
Getting Started
Install from pub.flutter-io.cn:
dart pub add mobile_date_tz
Or add manually to pubspec.yaml:
dependencies:
mobile_date_tz: ^0.1.0
Run dart pub get and you’re in business.
Tip: Run
DateTz.initializeTimezones()during boot so the timezone database is ready before you render anything. If you skip it, the library lazily initialises the first time it needs dynamic rules.
Quick Start
import 'package:mobile_date_tz/mobile_date_tz.dart';
void main() {
// Optional but recommended.
DateTz.initializeTimezones();
final rome = DateTz.now('Europe/Rome');
final nyc = rome.cloneToTimezone('America/New_York');
print(rome.format()); // 2025-06-15 09:30:00
print(nyc.format('YYYY-MM-DD HH:mm tz')); // 2025-06-15 03:30 America/New_York
final parsed = DateTz.parse(
'2025-06-15 09:30',
'YYYY-MM-DD HH:mm',
'UTC',
);
final handoff = parsed.cloneToTimezone('Asia/Tokyo')
..add(1, 'day')
..set(11, 'hour')
..convertToTimezone('Europe/Rome');
print(handoff.format('DD LM YYYY HH:mm', 'it')); // 17 Giugno 2025 11:00
}
Formatting Cheatsheet
| Token | Meaning | Example |
|---|---|---|
YYYY, yyyy |
Four-digit year | 2025 |
YY, yy |
Two-digit year | 25 |
MM |
Month (01–12) | 06 |
LM |
Locale month name (capitalised, falls back to English) | June |
DD |
Day of month (01–31) | 15 |
HH |
Hour (00–23) | 09 |
hh |
Hour (01–12) | 03 |
mm |
Minute (00–59) | 30 |
ss |
Second (00–59) | 00 |
aa |
Lowercase am/pm | pm |
AA |
Uppercase AM/PM | PM |
tz |
Timezone identifier | Europe/Rome |
Escape literal text by wrapping it in []: YYYY-MM-DD[ @ ]HH:mm → 2025-06-15 @ 09:30.
API Tour
Constructors
final utcNow = DateTz.now(); // defaults to UTC
final timestamp = DateTz(1700000000000, 'Europe/Rome'); // from epoch ms
final clone = DateTz(utcNow); // copy constructor
final mapInput = DateTz({'timestamp': 1700000000000}); // Map-based
Mutation & Cloning
final meeting = DateTz.parse('2025-02-28 09:00', 'YYYY-MM-DD HH:mm', 'America/New_York');
meeting.add(1, 'day'); // 2025-03-01 09:00
meeting.add(3, 'hour'); // 2025-03-01 12:00
meeting.set(15, 'minute'); // 12:15
final utcClone = meeting.cloneToTimezone('UTC'); // new instance
meeting.convertToTimezone('Europe/London'); // mutate in place
Supported units:
add:minute,hour,day,month,yearset:year,month(1–12),day,hour,minute
Comparison Guards
final rome = DateTz.now('Europe/Rome');
final tokyo = rome.cloneToTimezone('Asia/Tokyo');
if (!rome.isComparable(tokyo)) {
// Align before comparing
tokyo.convertToTimezone(rome.timezone);
}
final diffMs = rome.compare(tokyo); // now safe
Comparison throws if timezones differ—your reminder to convert before mixing apples and oranges.
Parsing Recipes
// AM/PM pattern
final breakfast = DateTz.parse('02/14/2025 08:30 AM', 'MM/DD/YYYY hh:mm AA', 'America/Chicago');
// Literals and seconds
final deployment = DateTz.parse('Deploy @ 2025-07-01 22:00:00', '[Deploy @ ]YYYY-MM-DD HH:mm:ss', 'UTC');
// Locale-specific names (fallback to English if locale data is missing)
print(deployment.format('DD LM YYYY HH:mm', 'es')); // "01 Julio 2025 22:00"
Timezone Offsets & DST Insight
final romeNoon = DateTz.parse('2025-08-01 12:00', 'YYYY-MM-DD HH:mm', 'Europe/Rome');
print(romeNoon.timezoneOffset.sdt); // 3600 (standard offset seconds)
print(romeNoon.timezoneOffset.observesDst); // true
print(romeNoon.isDst); // true in summer
Flutter Integration
Provider / Riverpod Example
import 'package:flutter/material.dart';
import 'package:mobile_date_tz/mobile_date_tz.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final clockProvider = StreamProvider.family<DateTz, String>((ref, tz) async* {
DateTz.initializeTimezones();
while (true) {
yield DateTz.now(tz);
await Future<void>.delayed(const Duration(minutes: 1));
}
});
class ClockText extends ConsumerWidget {
const ClockText({required this.tz, super.key});
final String tz;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncClock = ref.watch(clockProvider(tz));
return asyncClock.maybeWhen(
data: (date) => Text(date.format('HH:mm tz')),
orElse: () => const CircularProgressIndicator(),
);
}
}
Material Localization Compatibility
format(pattern, locale) piggybacks on intl. If the requested locale isn’t initialized, the library falls back to English month names automatically—no hard crash during boot.
Server & CLI Recipes
Scheduling Email Digests
final offices = [
{'tz': 'America/New_York', 'hour': 9},
{'tz': 'Europe/Rome', 'hour': 9},
{'tz': 'Asia/Tokyo', 'hour': 9},
];
final baseUtc = DateTz.now();
final sends = offices.map((office) {
final local = DateTz(baseUtc).convertToTimezone(office['tz']!);
local.set(office['hour']! as int, 'hour');
if (local.compare(baseUtc) < 0) {
local.add(1, 'day');
}
return local;
}).toList();
sends.sort((a, b) => a.timestamp.compareTo(b.timestamp));
Express Middleware (Shelf-style)
Handler withContext(Handler inner) {
return (request) {
final headerTz = request.headers['x-user-tz'] ?? 'UTC';
final now = () => DateTz.now(headerTz);
return inner(request.change(context: {'now': now}));
};
}
Package Anatomy
lib/
├─ mobile_date_tz.dart # Barrel export (library entry)
└─ src/
├─ date_tz.dart # Core implementation
├─ idate_tz.dart # Minimal interface for interop
└─ timezones.dart # Generated timezone offsets
tool/
└─ bump_version.dart # CI helper for automated releases
.github/workflows/
└─ release.yml # Auto bump, test, publish to pub.flutter-io.cn
Release Flow
- Merge or push to
master. - GitHub Actions runs
tool/bump_version.dart, updates the changelog, formats/analyzes/tests, publishes to pub.flutter-io.cn (usingPUB_CREDENTIALS_JSONsecret), and tags the release. - Profit.
Want to ship manually? Run:
dart run tool/bump_version.dart # prints new version, updates changelog
dart pub publish --dry-run
dart pub publish
Testing & Quality
dart format .
dart analyze
dart test
The bundled tests mirror the original TypeScript suite, covering formatting, arithmetic, parsing edge cases, and DST conversions. Contributions should include matching test updates.
Migration Guide (TypeScript → Dart)
| TypeScript | Dart |
|---|---|
new DateTz(ts, 'Europe/Rome') |
DateTz(ts, 'Europe/Rome') |
date.toString(pattern, locale) |
date.format(pattern, locale) |
DateTz.defaultFormat = '...' |
Same API |
cloneToTimezone('UTC') |
Identical |
convertToTimezone('UTC') |
Identical |
The biggest differences:
- Optional parameters replace overloads (
format([pattern, locale])). - Parsing errors throw
FormatException. - Month getter returns zero-based month (aligns with the TypeScript port).
FAQ
What if the device doesn’t have timezone data?
DateTz falls back to the bundled offsets, so you still get coherent answers. Call DateTz.initializeTimezones() where possible for maximum accuracy.
Can I store seconds or milliseconds?
Timestamps are truncated to the minute on purpose. Keep sub-minute precision elsewhere if you need it.
How do I support custom tokens?
Wrap the output of format() with your own string replacements or fork the formatter—it’s a straightforward map replace.
Can I ship a subset of timezones?
Yes. Regenerate timezones.dart with your curated list to shrink size.
Community & Support
- Issues & feature requests: https://github.com/lbdsh/mobile-date-tz/issues
- Discussions & roadmap: https://github.com/lbdsh/mobile-date-tz
License
ISC © LBD SRL Timezone data derived from the IANA TZDB.
Libraries
- mobile_date_tz
- Utilities for creating, manipulating, and formatting timezone-aware dates.