
作为一个 Flutter 开发者,你是否也曾为日期处理而烦恼?在 jiffy、intl、date_format 之间徘徊,却总觉得少点什么?
实际上,在开发 Hora 时,我最初的灵感来源于 JavaScript 生态中备受好评的 day.js。day.js 以其简洁的 API 和强大的插件系统征服了前端开发者。我心想:为什么 Dart 生态不能有一个类似但更优秀的日期库呢?
于是我开始将 day.js 的核心功能,使用 Dart 的语言特性重新实现:
-
Sealed Classes - 替代 JavaScript 的字符串枚举
-
Extension Methods - 实现 day.js 的插件机制
-
Pattern Matching - 处理复杂的日期逻辑
-
Null Safety - 避免 day.js 中的空值问题
-
Tree-shakable - 让性能超越 day.js
这不仅仅是一次简单的移植,而是对现代日期处理理念在 Dart 生态中的完整实践。
为什么需要另一个日期库?
说实话,开发 Hora 的初衷源于我自己的痛点。作为一名曾经的 jiffy 用户,在实际项目中使用时遇到了很多让人困扰的问题:
我的 jiffy 使用经历
最初选择 jiffy 是因为它模仿了著名的 moment.js API,看起来很熟悉:
// jiffy 的 API 看起来不错
Jiffy().add(days: 7).format('yyyy-MM-dd')
但真正用起来却发现差强人意:
-
高级功能支持有限:
- 没有内置的业务日计算(需要自己实现周末和节假日逻辑)
- 月份边界处理有时不够直观(1月31日加1个月会得到2月28日)
- 插件系统不如 Hora 完善,扩展性有限
-
不可变性保证不足:虽然大部分操作是新的,但某些边缘情况下可能修改原对象
-
国际化复杂度:配置相对复杂,需要手动加载语言包,不如 Hora 的 tree-shakable 设计优雅
从 day.js 到 Hora:理念与实践
day.js 的成功在于它优雅解决了以下问题:
-
API 简洁 - dayjs().format() 而不是复杂的配置
-
插件机制 - 可按需扩展,保持核心精简
-
不可变性 - 每次操作返回新实例
-
链式调用 - 直观的方法链
而 Hora 在此基础上,利用 Dart 的优势更进一步:
// day.js 风格的 API,但更安全
final now = Hora.now();
final result = now
.add(1, TemporalUnit.month)
.startOf(TemporalUnit.day)
.format('YYYY-MM-DD');
// 使用 Dart 的 sealed class 确保类型安全
TemporalUnit unit = TemporalUnit.parse('month'); // 编译时检查
// 通过 Extension 实现插件,比 day.js 更优雅
import 'package:hora/src/plugins/business_day.dart';
final businessDay = now.addBusinessDays(5);
痛定思痛,决定自己动手
在分析了现有库的痛点后,我意识到 Dart 生态需要一个功能更全面的日期库:
-
intl:功能强大,但 API 相对复杂,体积较大,初始化重
-
date_format:轻量,但功能单一,仅支持格式化,缺乏操作能力
-
jiffy:API 友好但高级功能支持不足,缺少企业级特性
我想创造一个功能完整的日期库,满足从简单格式化到复杂业务计算的各种需求。这就是 Hora 的诞生故事。
Hora 的核心优势
1. 🎯 丰富的功能特性
Hora 提供了比其他库更全面的日期时间处理能力:
// Hora - 功能强大且易用
final now = Hora.now();
final nextWeek = now.add(1, TemporalUnit.week);
// 支持复杂的业务日计算
final businessDays = now.addBusinessDays(10);
// 财年和季度计算
final fiscalYear = now.fiscalYear(startMonth: 4);
final fiscalQuarter = now.fiscalQuarter(startMonth: 4);
// 重复事件模式
final meetings = Recurrence.weekly(
start: now,
daysOfWeek: {DateTime.monday, DateTime.friday},
);
// jiffy - 基础功能有限
final jiffyNextWeek = Jiffy().add(weeks: 1);
// date_format - 只支持格式化,不支持操作
2. 🔒 不可变性设计
Hora 采用不可变设计,所有操作都返回新实例:
final date = Hora.of(year: 2024, month: 12, day: 8);
final modified = date.copyWith(hour: 10);
print(date.hour); // 0 - 原始实例不变
print(modified.hour); // 10 - 新实例
这避免了在复杂应用中因意外修改导致的状态问题。
3. 🌍 完善的国际化支持
Hora 内置 143 种语言支持,并且设计为 tree-shakable:
import 'package:hora/hora.dart';
import 'package:hora/src/locales/ja.dart'; // 按需导入
// 默认英文
final us = Hora.now();
print(us.format('MMMM D, YYYY')); // December 8, 2024
// 切换到中文
final cn = Hora.now(locale: const HoraLocaleZhCn());
print(cn.format('YYYY年M月D日')); // 2024年12月8日
// 切换到日文
final jp = Hora.now(locale: const HoraLocaleJa());
print(jp.format('YYYY年M月D日')); // 2024年12月8日
相比之下:
-
intl 需要额外配置,初始化较重
-
jiffy 支持的语言较少
-
date_format 几乎没有国际化支持
4. 📅 智能的时间单位处理
Hora 区分固定时长单位和日历相关单位:
// 固定时长单位(使用 Duration)
final exactWeek = now.add(7, TemporalUnit.day); // 精确的 7 天
// 日历相关单位(考虑月份长度、闰年等)
final nextMonth = now.add(1, TemporalUnit.month); // 智能处理月份变化
// HoraDuration 更是支持混合单位
final duration = HoraDuration(
years: 1,
months: 6,
days: 15
);
print(duration.humanize()); // "a year and 6 months"
5. 🔌 灵活的插件系统
Hora 采用 Dart Extension 实现插件机制,无需显式注册:
import 'package:hora/src/plugins/week_year.dart';
final h = Hora.now();
print(h.weekYear()); // 通过扩展方法调用
print(h.weekOfWeekYear()); // 无需额外配置
6. ⚡ 轻量且高性能
-
零外部依赖:只依赖 Dart 内置的
meta 包
-
Tree-shakable:未使用的代码会被编译器优化掉
-
纯 Dart 实现:无平台限制,支持 Flutter Web、Server、Desktop
7. 📊 完整的插件生态
Hora 通过 20+ 个插件覆盖了企业级应用的各种需求:
- business_day:业务日计算
- recurrence:重复事件
- calendar:日历生成
- relative_time:相对时间
- custom_parse_format:自定义解析
- timezone:时区处理
- fiscal_year:财年计算
- week_year:ISO 周年
- 等等...
功能对比表
| 特性 |
Hora |
intl |
date_format |
jiffy |
| 不可变性 |
✅ 完全不可变 |
⚠️ DateTime 可变 |
⚠️ DateTime 可变 |
⚠️ 部分可变 |
| 国际化 |
✅ 143+ 语言,Tree-shakable |
✅ 完整但较重 |
❌ 不支持 |
✅ 有限支持 |
| 插件系统 |
✅ 20+ 插件,无侵入 |
❌ 无 |
❌ 无 |
⚠️ 基础扩展 |
| 高级功能 |
✅ 业务日、重复事件、日历 |
❌ 无 |
❌ 无 |
⚠️ 基础功能 |
| 链式操作 |
✅ 流畅 API |
❌ 不支持 |
❌ 不支持 |
✅ 支持 |
| 时间单位 |
✅ 区分固定时长和日历单位 |
⚠️ 需手动计算 |
❌ 不支持 |
⚠️ 概念模糊 |
| 格式解析 |
✅ 自定义格式解析 |
⚠️ 有限支持 |
❌ 不支持 |
⚠️ 基础支持 |
| 时区支持 |
✅ 内置支持 |
❌ 需要额外包 |
❌ 不支持 |
❌ 无 |
| 财年计算 |
✅ 多国财年支持 |
❌ 无 |
❌ 不支持 |
❌ 无 |
| 依赖 |
✅ 仅 meta 包 |
❌ 较多依赖 |
✅ 0 |
⚠️ 轻量 |
实际应用示例
场景1:业务时间计算
// Hora - 内置业务日计算,一行搞定
import 'package:hora/src/plugins/business_day.dart';
final start = Hora.of(year: 2024, month: 12, day: 20); // 周五
final end = start.addBusinessDays(5); // 自动跳过周末和节假日
print(end.format('YYYY-MM-DD')); // 2024-12-27
// jiffy - 需要手动实现
var current = start;
int businessDays = 0;
while (businessDays < 5) {
current = current.add(1, TemporalUnit.day);
if (current.weekday <= 5) businessDays++; // 需要手动判断周末
}
// intl - 完全没有业务日功能
场景2:智能的时间单位处理
// Hora - 区分固定时长和日历单位
final jan31 = Hora.of(year: 2024, month: 1, day: 31);
// 固定时长 - 精确的 7 天
final exact7Days = jan31.add(7, TemporalUnit.day);
print(exact7Days.format('YYYY-MM-DD')); // 2024-02-07
// 日历单位 - 智能处理月份
final nextMonth = jan31.add(1, TemporalUnit.month);
print(nextMonth.format('YYYY-MM-DD')); // 2024-02-29 (闰年2月末)
// jiffy - 容易混淆,需要自己计算
final jiffy1 = Jiffy.parse('2024-01-31').add(days: 7); // 类似固定时长
final jiffy2 = Jiffy.parse('2024-01-31').add(months: 1); // 但逻辑不透明
场景4:强大的插件系统
Hora 的插件系统通过 Dart Extension 实现,无需显式注册:
// Calendar 插件 - 生成日历
import 'package:hora/src/plugins/calendar.dart';
final monthCal = Hora.now().monthCalendar();
print('该月有 ${monthCal.weekCount} 周');
// Recurrence 插件 - 复杂的重复事件
import 'package:hora/src/plugins/recurrence.dart';
// 每个周一和周五的会议
final meeting = Recurrence.weekly(
start: Hora.now(),
daysOfWeek: {DateTime.monday, DateTime.friday},
);
print('接下来5次会议时间:');
meeting.take(5).forEach(print);
// 自定义重复模式 - 如每3天一次
final custom = Recurrence.custom(
start: Hora.now(),
generator: (current) => current.add(3, TemporalUnit.day),
);
场景5:高级相对时间功能
// Relative Time 插件 - 比基础 fromNow 更强大
import 'package:hora/src/plugins/relative_time.dart';
final past = Hora.now().subtract(5, TemporalUnit.day);
// 自定义阈值配置
final config = RelativeTimeConfig(
thresholds: RelativeTimeThresholds.strict,
withoutSuffix: false,
);
// 详细的时间差分解
final diff = past.diffFromNowDetailed();
print(diff.format()); // "5 days, 0 hours, 0 minutes, 0 seconds ago"
print(diff.formatCompact()); // "5d"
// 短格式,适合 UI 显示
print(past.relativeFromNowShort()); // "-5d"
场景6:自定义格式解析
// Custom Parse Format 插件
import 'package:hora/src/plugins/custom_parse_format.dart';
// 解析各种格式的日期
final date1 = HoraParser.parse('25/12/2024', 'DD/MM/YYYY');
final date2 = HoraParser.parseMultiple(
'2024-12-25',
['YYYY-MM-DD', 'DD/MM/YYYY', 'MM-DD-YYYY'],
);
// 支持复杂格式
final complex = HoraParser.parse(
'Thursday, December 25, 2024 2:30 PM',
'dddd, MMMM D, YYYY h:mm A',
);
// 其他库需要自己实现这些复杂的解析逻辑
场景7:流畅的链式 API
// Hora - 真正的流畅操作
final result = Hora.now()
.startOf(TemporalUnit.month) // 月初
.addBusinessDays(10) // 加10个工作日
.withLocale(const HoraLocaleJa()) // 切换到日语
.format('YYYY年M月D日'); // 格式化
// jiffy - 链式支持但功能有限
final jiffyResult = Jiffy()
.startOf('month')
.add(days: 10) // 不能区分工作日
.format('yyyy-MM-dd'); // 格式化选项有限
真实项目案例:项目管理应用
在开发一个项目管理工具时,我遇到了这样的需求:
// 需求:计算任务截止日期,排除周末和节假日
import 'package:hora/src/plugins/business_day.dart';
// 设置节假日
final usHolidays = HolidayCalendar.usCommon.merge(
HolidayCalendar(fixedHolidays: [
DateTime(2024, 12, 24), // 公司额外假期
]),
);
// 任务分配
final taskStart = Hora.of(year: 2024, month: 12, day: 20);
final deadline = taskStart.addBusinessDays(
10,
BusinessDayConfig(holidays: usHolidays),
);
// 生成里程碑报告
final milestones = Recurrence.weekly(
start: taskStart,
daysOfWeek: {DateTime.friday},
count: 5,
).map((date) => {
'week': date.isoWeek,
'date': date.format('YYYY-MM-DD'),
'deliverables': List.generate(5, (i) =>
date.addBusinessDays(i)
),
});
// 这在 jiffy 中需要数百行代码来实现!
生产环境用例1:全球电商系统
// 处理不同时区的订单截止时间
import 'package:hora/src/plugins/timezone.dart';
// 订单在纽约下午6点截止
final nycTz = HoraTimezone.common['EST']!;
final orderDeadline = Hora.now()
.withTimezone(nycTz)
.copyWith(hour: 18, minute: 0, second: 0);
// 转换为各地时区显示
final tokyoTime = orderDeadline.inTimezone(HoraTimezone.common['JST']!);
final londonTime = orderDeadline.inTimezone(HoraTimezone.common['GMT']!);
final sydneyTime = orderDeadline.inTimezone(HoraTimezone.common['AEST']!);
// 批量处理不同时区的促销活动
final promotions = [
{'city': 'New York', 'tz': HoraTimezone.common['EST']!},
{'city': 'London', 'tz': HoraTimezone.common['GMT']!},
{'city': 'Tokyo', 'tz': HoraTimezone.common['JST']!},
{'city': 'Sydney', 'tz': HoraTimezone.common['AEST']!},
].map((loc) {
final midnight = Hora.nowIn(loc['tz'])
.endOf(TemporalUnit.day)
.add(1, TemporalUnit.second);
return {
'city': loc['city'],
'promoEnd': midnight.format('YYYY-MM-DD HH:mm:ss'),
'localTime': midnight.wallClockIn(loc['tz'] as HoraTimezone),
};
});
// jiffy 根本没有内置的时区支持!
生产环境用例2:企业财务系统
// 财务报表和季度结算
import 'package:hora/src/plugins/fiscal_year.dart';
// 不同国家的财年设置
final usGovConfig = FiscalYearConfig.usGovernment; // 10月1日开始
final ukConfig = FiscalYearConfig.ukTax; // 4月6日开始
final jpConfig = FiscalYearConfig.japan; // 4月1日开始
// 生成财年报表
final now = Hora.now();
final reports = {
'US Gov': {
'fiscalYear': now.fiscalYearWithConfig(usGovConfig),
'fiscalQuarter': now.fiscalQuarterWithConfig(usGovConfig),
'period': now.fiscalPeriod(config: usGovConfig),
'progress': '${(now.fiscalYearProgress(config: usGovConfig) * 100).toInt()}%',
'daysRemaining': now.daysRemainingInFiscalYear(config: usGovConfig),
},
'UK': {
'fiscalYear': now.fiscalYearWithConfig(ukConfig),
'fiscalQuarter': now.fiscalQuarterWithConfig(ukConfig),
'period': now.fiscalPeriod(config: ukConfig),
'progress': '${(now.fiscalYearProgress(config: ukConfig) * 100).toInt()}%',
},
'Japan': {
'fiscalYear': now.fiscalYearWithConfig(jpConfig),
'fiscalQuarter': now.fiscalQuarterWithConfig(jpConfig),
'period': now.fiscalPeriod(config: jpConfig),
},
};
// 生成财年日历
final fyCalendar = now
.startOfFiscalYearWithConfig(usGovConfig)
.yearCalendar();
// jiffy:抱歉,不支持财年计算
生产环境用例3:SaaS 订阅管理
// 处理订阅周期和计费
import 'package:hora/src/plugins/recurrence.dart';
class SubscriptionPlan {
final String id;
final String name;
final Recurrence billingCycle;
final Map<String, dynamic> features;
const SubscriptionPlan({
required this.id,
required this.name,
required this.billingCycle,
required this.features,
});
}
// 定义订阅计划
final plans = [
SubscriptionPlan(
id: 'basic',
name: 'Basic Plan',
billingCycle: Recurrence.monthly(
start: Hora.now(),
interval: 1,
),
features: {'seats': 5, 'storage': '100GB'},
),
SubscriptionPlan(
id: 'pro',
name: 'Pro Plan',
billingCycle: Recurrence.monthly(
start: Hora.now(),
interval: 1,
),
features: {'seats': 20, 'storage': '1TB'},
),
SubscriptionPlan(
id: 'enterprise',
name: 'Enterprise',
billingCycle: Recurrence.yearly(
start: Hora.now(),
interval: 1,
),
features: {'seats': -1, 'storage': 'unlimited'},
),
];
// 生成未来12个账期
class BillingService {
List<Map<String, dynamic>> generateBillingSchedule(SubscriptionPlan plan) {
return plan.billingCycle
.take(12)
.map((billingDate) => {
'date': billingDate.format('YYYY-MM-DD'),
'period_start': billingDate.format('YYYY-MM-DD'),
'period_end': billingDate
.add(1, TemporalUnit.month)
.subtract(1, TemporalUnit.day)
.format('YYYY-MM-DD'),
'days_in_period': billingDate.daysInMonth,
'is_business_day': billingDate.isBusinessDay(),
})
.toList();
}
}
// 处理试用期和付费转换
class TrialService {
Hora calculateTrialEnd(Hora signupDate, Duration trialDuration) {
return signupDate.add(
Duration.inMilliseconds(trialDuration.inMilliseconds),
TemporalUnit.day);
}
Hora calculateFirstBillingDate(Hora trialEnd) {
// 确保第一个账期不是周末
return trialEnd.addBusinessDays(1);
}
}
// jiffy:需要手动处理所有重复逻辑和边界情况
为什么选择 Hora?
-
功能完整:覆盖企业级应用的各种日期时间需求
-
插件生态:20+ 插件,业务日、财年、时区、重复事件等
-
开发友好:清晰的 API 设计,丰富的示例代码
-
轻量高效:零外部依赖,tree-shakable,纯 Dart 实现
快速开始
dart pub add hora
import 'package:hora/hora.dart';
void main() {
// 创建时间实例
final now = Hora.now();
// 时间操作
final nextMonth = now.add(1, TemporalUnit.month);
// 格式化输出
print(now.format('YYYY-MM-DD HH:mm:ss'));
// 相对时间
final birthday = Hora.of(year: 1990, month: 6, day: 15);
print(birthday.fromNow()); // "34 years ago"
}
迁移指南
从其他库迁移到 Hora 非常简单:
从 jiffy 迁移
// jiffy
Jiffy().add(days: 7).format('yyyy-MM-dd')
// Hora
Hora.now().add(7, TemporalUnit.day).format('YYYY-MM-DD')
从 intl 迁移
// intl
DateFormat('yyyy-MM-dd').format(DateTime.now())
// Hora
Hora.now().format('YYYY-MM-DD')
Hora vs jiffy:功能对比总览
为了让你更清楚地了解 Hora 的独特优势,这里是一个详细的对比:
🔥 jiffy 根本没有的功能:
| 功能类别 |
Hora |
jiffy |
生产价值 |
| 业务日计算 |
✅ 完整支持 |
❌ 无 |
金融、企业应用必备 |
| 时区转换 |
✅ 内置支持 |
❌ 需要额外包 |
全球化应用必备 |
| 财年管理 |
✅ 多国财年 |
❌ 无 |
财务系统必备 |
| 重复事件 |
✅ 复杂模式 |
❌ 基础模式 |
订阅系统必备 |
| 自定义解析 |
✅ 灵活配置 |
❌ 有限支持 |
数据导入必备 |
| 日历生成 |
✅ 月历/年历 |
❌ 无 |
调度系统必备 |
| 详细时间差 |
✅ 多种格式 |
❌ 基础格式 |
分析报表必备 |
| 节假日管理 |
✅ 多国日历 |
❌ 无 |
本地化应用必备 |
💡 为什么这些功能很重要?
在实际的企业级应用中,你迟早会遇到这些需求:
-
业务日计算 - 订单处理、物流配送的时效承诺
-
时区处理 - 全球用户的同步操作
-
财年管理 - 财务报表、预算规划
-
重复事件 - 订续计费、定期提醒
-
自定义解析 - 处理各种来源的数据
jiffy 只能帮你做基础的日期操作,而 Hora 让你能够构建完整的企业级应用。
写在最后
从 jiffy 用户到 Hora 作者,这段开发经历让我学到了很多。我开始只是想解决自己在项目中的痛点,没想到最终创造了一个功能完整的日期库。
Hora 的核心设计理念其实很简单:
-
实用至上 - 解决真实的业务问题,而不是为了功能而功能
-
渐进增强 - 从简单的格式化开始,按需引入高级功能
-
保持简单 - 复杂的内部实现,简单的外部接口
-
性能优先 - 树枝摇动、零依赖,不影响应用性能
如果你也遇到了和我类似的困扰,希望 Hora 能帮你节省时间,让你专注于业务逻辑而不是日期处理的细节。
这个项目还在持续改进中,欢迎任何反馈和建议。
🎯 适用场景
- ✅ 企业级应用:需要复杂的业务日期计算
- ✅ 跨平台项目:Flutter Web、Mobile、Desktop 统一 API
- ✅ 国际化产品:143+ 语言开箱即用
- ✅ 性能敏感应用:轻量级实现,tree-shakable
🚀 立即开始
dart pub add hora
GitHub: github.com/fluttercand…
Pub.dev: pub.dev/packages/ho…
文档: 完整 API 文档
如果 Hora 对你的项目有帮助,欢迎给项目点个 ⭐️ Star,你的支持是我持续开发的动力!同时也别忘了在 Pub.dev 上点个 👍 Like,让更多开发者发现这个库!
让我们一起,用 Hora 让日期时间处理变得简单而优雅。