A powerful, immutable, and type-safe date-time library for Dart, inspired by Day.js.
Hora (from Latin, meaning "hour/time") provides a modern API for date-time manipulation with first-class Dart support, including sealed classes, pattern matching, and comprehensive type safety.
- 🔒 Immutable by Design: All operations return new instances
- 🎯 Type-Safe: Leverages Dart's sealed classes and pattern matching
- 🌍 i18n Ready: Built-in locale support with extensible architecture
- ⚡ Lightweight: No external dependencies (only
metafor annotations) - 📅 Calendar-Aware Durations: Proper handling of months and years
- 🔌 Plugin System: Extend functionality without modifying core
- 🧪 Well Tested: Comprehensive test coverage
Add hora to your pubspec.yaml:
dependencies:
hora: anyThen run:
dart pub getimport 'package:hora/hora.dart';
void main() {
// Create a Hora instance
final now = Hora.now();
final date = Hora.of(year: 2024, month: 6, day: 15);
// Parse from string
final parsed = Hora.parse('2024-06-15T10:30:00');
// Manipulate dates
final nextWeek = now.add(1, TemporalUnit.week);
final lastMonth = now.subtract(1, TemporalUnit.month);
// Format dates
print(now.format('YYYY-MM-DD')); // 2024-06-15
print(now.format('MMMM D, YYYY')); // June 15, 2024
// Relative time
print(date.fromNow()); // "2 months ago"
}// Current time
Hora.now()
Hora.nowUtc()
// From components
Hora.of(year: 2024, month: 6, day: 15)
Hora.of(year: 2024, month: 6, day: 15, hour: 10, minute: 30, second: 45)
// From DateTime
Hora.fromDateTime(DateTime.now())
// From timestamp
Hora.unix(1718409600)
Hora.unixMillis(1718409600000)
// Parse string
Hora.parse('2024-06-15')
Hora.parse('2024-06-15T10:30:00')
Hora.tryParse('invalid') // returns null instead of invalid Horafinal h = Hora.of(year: 2024, month: 6, day: 15, hour: 14, minute: 30);
h.year // 2024
h.month // 6
h.day // 15
h.weekday // 6 (Saturday, ISO 8601)
h.hour // 14
h.minute // 30
h.second // 0
h.millisecond // 0
h.microsecond // 0
// Derived properties
h.quarter // 2
h.dayOfYear // 167
h.isoWeek // 24
h.isLeapYear // true
h.daysInMonth // 30All manipulation methods return new instances:
// Add/subtract time
h.add(1, TemporalUnit.day)
h.subtract(2, TemporalUnit.week)
h.addDuration(Duration(hours: 5))
// Start/end of units
h.startOf(TemporalUnit.month) // First day of month, 00:00:00
h.endOf(TemporalUnit.day) // 23:59:59.999999
// Copy with modifications
h.copyWith(hour: 10, minute: 0)Hora uses a sealed TemporalUnit class for type-safe unit specifications:
// Fixed-duration units
TemporalUnit.microsecond
TemporalUnit.millisecond
TemporalUnit.second
TemporalUnit.minute
TemporalUnit.hour
TemporalUnit.day
TemporalUnit.week
// Calendar-based units (variable duration)
TemporalUnit.month
TemporalUnit.quarter
TemporalUnit.yearh1.isBefore(h2)
h1.isAfter(h2)
h1.isSame(h2, TemporalUnit.day) // Same day?
h1.isSameOrBefore(h2)
h1.isSameOrAfter(h2)
h1.isBetween(start, end)
h1.difference(h2) // Returns Duration
h1.diff(h2, TemporalUnit.day) // Returns numh.isToday
h.isYesterday
h.isTomorrow
h.isThisWeek
h.isThisMonth
h.isThisYear
h.isPast
h.isFuture
h.isWeekend
h.isWeekdayh.format('YYYY-MM-DD') // 2024-06-15
h.format('MMMM D, YYYY') // June 15, 2024
h.format('dddd, MMM D') // Saturday, Jun 15
h.format('h:mm A') // 2:30 PM
h.toIso8601() // 2024-06-15T14:30:00.000| Token | Output | Description |
|---|---|---|
YYYY |
2024 | 4-digit year |
YY |
24 | 2-digit year |
MMMM |
June | Full month name |
MMM |
Jun | Short month name |
MM |
06 | Month (2-digit) |
M |
6 | Month |
DD |
15 | Day (2-digit) |
D |
15 | Day |
Do |
15th | Day with ordinal |
dddd |
Saturday | Full weekday |
ddd |
Sat | Short weekday |
dd |
Sa | Min weekday |
d |
6 | Weekday (0-6) |
HH |
14 | Hour 24h (2-digit) |
H |
14 | Hour 24h |
hh |
02 | Hour 12h (2-digit) |
h |
2 | Hour 12h |
mm |
30 | Minute (2-digit) |
m |
30 | Minute |
ss |
00 | Second (2-digit) |
s |
0 | Second |
A |
PM | AM/PM |
a |
pm | am/pm |
Q |
2 | Quarter |
W |
24 | ISO week |
Z |
+00:00 | Timezone offset |
X |
1718409600 | Unix timestamp |
x |
1718409600000 | Unix millis |
// From now (via extensions)
past.fromNow() // "3 days ago"
future.fromNow() // "in 2 hours"
// From/to another date
h1.from(h2) // "5 days ago"
h1.to(h2) // "in 5 days"
// Without suffix
past.fromNow(withoutSuffix: true) // "3 days"
// Advanced relative time (via relative_time plugin)
import 'package:hora/src/plugins/relative_time.dart';
past.relativeFromNow() // "3 days ago" (with more options)h.calendar()
// Today at 2:30 PM
// Yesterday at 3:00 PM
// Tomorrow at 10:00 AM
// Last Monday at 9:00 AM
// 12/25/2023HoraDuration provides calendar-aware durations that properly handle months and years:
// Create durations
final d1 = HoraDuration(years: 1, months: 6);
final d2 = HoraDuration(days: 30, hours: 12);
final d3 = HoraDuration.between(h1, h2);
// Parse ISO 8601
final d4 = HoraDuration.parse('P1Y6M');
final d5 = HoraDuration.parse('P2DT12H30M');
// Arithmetic
final sum = d1 + d2;
final diff = d1 - d2;
final scaled = d1 * 2;
// Format
d1.toIso8601() // "P1Y6M"
d1.humanize() // "a year"
// Normalize
HoraDuration(minutes: 90).normalize() // 1h 30mHora includes 143 built-in locales covering major world languages, all designed for tree-shaking.
import 'package:hora/hora.dart';
// English and Chinese are included by default
final now = Hora.now(); // Uses HoraLocaleEn
final chinese = Hora.now(locale: const HoraLocaleZhCn());
print(now.format('MMMM D, YYYY')); // December 6, 2024
print(chinese.format('YYYY年M月D日')); // 2024年12月6日Import only the locales you need for optimal tree-shaking:
import 'package:hora/hora.dart';
import 'package:hora/src/locales/ja.dart';
import 'package:hora/src/locales/ko.dart';
import 'package:hora/src/locales/de.dart';
void main() {
final japanese = Hora.now(locale: const HoraLocaleJa());
final korean = Hora.now(locale: const HoraLocaleKo());
final german = Hora.now(locale: const HoraLocaleDe());
print(japanese.format('YYYY年M月D日')); // 2024年12月6日
print(korean.format('YYYY년 M월 D일')); // 2024년 12월 6일
print(german.format('D. MMMM YYYY')); // 6. Dezember 2024
}| Region | Locales |
|---|---|
| Default | en, zh-cn (exported from hora.dart) |
| European | de, fr, es, it, pt, nl, pl, ru, uk, cs, sk, hu, ro, bg, el, tr, sv, da, no, fi, et, lv, lt, ... |
| Asian | zh, zh-tw, zh-hk, ja, ko, th, vi, id, ms, tl-ph, km, lo, my, bn, hi, ta, te, kn, ml, gu, mr, pa-in, ne, ... |
| Middle Eastern | ar, ar-sa, ar-eg, fa, he, ur, ku, ... |
| Other | sw, yo, am, ti, eo, tlh, ... |
Full list: See lib/src/locales/ directory.
Set a default locale for all new Hora instances:
// Set global locale
Hora.globalLocale = const HoraLocaleZhCn();
// New instances use global locale
final h1 = Hora.now(); // Uses HoraLocaleZhCn
final h2 = Hora.of(year: 2024, month: 12, day: 5); // Uses HoraLocaleZhCn
// Override with explicit locale
final h3 = Hora.now(locale: const HoraLocaleEn()); // Uses HoraLocaleEnfinal h = Hora.now(locale: const HoraLocaleEn());
print(h.format('MMMM')); // December
final h2 = h.withLocale(const HoraLocaleZhCn());
print(h2.format('MMMM')); // 十二月class HoraLocaleEs extends HoraLocale {
const HoraLocaleEs();
@override
String get code => 'es';
@override
List<String> get months => const [
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
];
@override
List<String> get monthsShort => const [
'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun',
'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'
];
@override
List<String> get weekdays => const [
'Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'
];
@override
List<String> get weekdaysShort => const ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
@override
List<String> get weekdaysMin => const ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sá'];
@override
int get weekStart => DateTime.monday; // Week starts on Monday
@override
HoraRelativeTime get relativeTime => const HoraRelativeTime(
future: 'en %s',
past: 'hace %s',
s: 'unos segundos',
m: 'un minuto',
mm: '%d minutos',
h: 'una hora',
hh: '%d horas',
d: 'un día',
dd: '%d días',
mo: 'un mes',
mos: '%d meses',
y: 'un año',
yy: '%d años',
);
@override
String ordinal(int n, [String? unit]) => '${n}º';
}Hora provides convenient extensions:
// DateTime extension
DateTime.now().toHora()
// Duration extension
Duration(days: 5).toHoraDuration()
// String parsing
'2024-06-15'.toHora()
// Int timestamps
1718409600.asUnixSeconds
1718409600000.asUnixMillis
// Range generation
start.rangeTo(end, step: TemporalUnit.day) // Iterable<Hora>
start.take(10, step: TemporalUnit.day) // 10 consecutive days
// Min/Max
[h1, h2, h3].earliest // Hora?
[h1, h2, h3].latest // Hora?
// Builder methods
Hora.now()
.startOf(TemporalUnit.day)
.copyWith(month: 6)
.copyWith(year: 2024)Hora comes with a rich set of plugins that extend its functionality. All plugins are available via extension methods, requiring no separate registration.
Format dates using locale-specific format strings:
import 'package:hora/hora.dart';
import 'package:hora/src/plugins/localized_format.dart';
final h = Hora.of(year: 2024, month: 6, day: 15, hour: 14, minute: 30);
// Using predefined presets
h.localizedFormat('LT') // "2:30 PM"
h.localizedFormat('LTS') // "2:30:45 PM"
h.localizedFormat('L') // "06/15/2024"
h.localizedFormat('LL') // "June 15, 2024"
h.localizedFormat('LLL') // "June 15, 2024 2:30 PM"
h.localizedFormat('LLLL') // "Saturday, June 15, 2024 2:30 PM"
// With different locales
final zhLocale = const HoraLocaleZhCn();
h.withLocale(zhLocale).localizedFormat('LL') // Uses Chinese formatCalculate week-year values for fiscal/ISO calendar reporting:
import 'package:hora/hora.dart';
import 'package:hora/src/plugins/week_year.dart';
final h = Hora.of(year: 2024, month: 1, day: 1);
// ISO week-year (default)
h.weekYear() // 2024
h.weekOfWeekYear() // 1
h.weeksInWeekYear() // 52
// US week configuration (Sunday start)
h.weekYear(WeekYearConfig.us)
h.weekOfWeekYear(WeekYearConfig.us)
// Set week-year
h.setWeekYear(2025)Dynamically modify locale settings at runtime:
import 'package:hora/hora.dart';
import 'package:hora/src/plugins/update_locale.dart';
// Create updated locale from base
final customLocale = const HoraLocaleEn().update(
weekStart: DateTime.monday,
months: ['Jan', 'Feb', 'Mar', ...],
);
// Update relative time settings
final rtLocale = const HoraLocaleEn().updateRelativeTime(
s: 'just now',
m: 'a minute ago',
);
// Update format strings
final fmtLocale = const HoraLocaleEn().updateFormats(
lt: 'HH:mm',
ll: 'YYYY年M月D日',
);
// Use updated locale
final h = Hora.now(locale: customLocale);Create and manipulate Hora instances using Map objects:
import 'package:hora/hora.dart';
import 'package:hora/src/plugins/object_support.dart';
// Create from object
final h = HoraObject.from({
'year': 2024,
'month': 6,
'day': 15,
'hour': 14,
'minute': 30,
});
// Add using object
h.addObject({'days': 5, 'hours': 3})
// Subtract using object
h.subtractObject({'months': 1})
// Set using object
h.setObject({'hour': 10, 'minute': 0})
// Get/set by key
h.getByKey('month') // 6
h.setByKey('day', 20)Hora includes many more plugins:
| Plugin | Description |
|---|---|
advancedFormat |
Extended format tokens (Q, Do, k, etc.) |
buddhistEra |
Buddhist Era calendar support |
businessDay |
Business day calculations |
calendar |
Calendar-style date formatting |
customParseFormat |
Parse dates with custom format strings |
duration |
Advanced duration handling |
fiscalYear |
Fiscal year calculations |
localeData |
Access locale data programmatically |
minMax |
Find min/max in date collections |
precision |
Precision-aware date comparisons |
recurrence |
Recurring date patterns |
relativeTime |
Human-readable relative time |
timezone |
Timezone support |
weekOfYear |
Week of year calculations |
| Feature | Hora | Day.js | moment.js |
|---|---|---|---|
| Immutable | ✅ | ✅ | ❌ |
| Type-safe | ✅ (Dart) | ❌ | ❌ |
| Tree-shakable | ✅ | ✅ | ❌ |
| Null-safe | ✅ | N/A | N/A |
| Plugin system | ✅ (dart extensions) | ✅ | ✅ |
| Locale support | ✅ | ✅ | ✅ |
| Calendar durations | ✅ | Plugin | ✅ |
We welcome contributions! Please see CONTRIBUTING.md for details.
MIT License - see LICENSE for details.
Made with ❤️ by Flutter Candies
