Hora Dart:我为什么从 jiffy 用户变成了新日期库的作者

作为一个 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,看起来很熟悉:

dart 复制代码
// jiffy 的 API 看起来不错
Jiffy().add(days: 7).format('yyyy-MM-dd')

但真正用起来却发现差强人意

  1. 高级功能支持有限

    • 没有内置的业务日计算(需要自己实现周末和节假日逻辑)
    • 月份边界处理有时不够直观(1月31日加1个月会得到2月28日)
    • 插件系统不如 Hora 完善,扩展性有限
  2. 不可变性保证不足:虽然大部分操作是新的,但某些边缘情况下可能修改原对象

  3. 国际化复杂度:配置相对复杂,需要手动加载语言包,不如 Hora 的 tree-shakable 设计优雅

从 day.js 到 Hora:理念与实践

day.js 的成功在于它优雅解决了以下问题:

  1. API 简洁 - dayjs().format() 而不是复杂的配置
  2. 插件机制 - 可按需扩展,保持核心精简
  3. 不可变性 - 每次操作返回新实例
  4. 链式调用 - 直观的方法链

而 Hora 在此基础上,利用 Dart 的优势更进一步:

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 提供了比其他库更全面的日期时间处理能力:

dart 复制代码
// 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 采用不可变设计,所有操作都返回新实例:

dart 复制代码
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:

dart 复制代码
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 区分固定时长单位日历相关单位

dart 复制代码
// 固定时长单位(使用 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 实现插件机制,无需显式注册:

dart 复制代码
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:业务时间计算

dart 复制代码
// 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:智能的时间单位处理

dart 复制代码
// 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 实现,无需显式注册:

dart 复制代码
// 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:高级相对时间功能

dart 复制代码
// 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:自定义格式解析

dart 复制代码
// 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

dart 复制代码
// 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');  // 格式化选项有限

真实项目案例:项目管理应用

在开发一个项目管理工具时,我遇到了这样的需求:

dart 复制代码
// 需求:计算任务截止日期,排除周末和节假日
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:全球电商系统

dart 复制代码
// 处理不同时区的订单截止时间
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:企业财务系统

dart 复制代码
// 财务报表和季度结算
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 订阅管理

dart 复制代码
// 处理订阅周期和计费
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?

  1. 功能完整:覆盖企业级应用的各种日期时间需求
  2. 插件生态:20+ 插件,业务日、财年、时区、重复事件等
  3. 开发友好:清晰的 API 设计,丰富的示例代码
  4. 轻量高效:零外部依赖,tree-shakable,纯 Dart 实现

快速开始

bash 复制代码
dart pub add hora
dart 复制代码
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 迁移

dart 复制代码
// jiffy
Jiffy().add(days: 7).format('yyyy-MM-dd')

// Hora
Hora.now().add(7, TemporalUnit.day).format('YYYY-MM-DD')

从 intl 迁移

dart 复制代码
// intl
DateFormat('yyyy-MM-dd').format(DateTime.now())

// Hora
Hora.now().format('YYYY-MM-DD')

Hora vs jiffy:功能对比总览

为了让你更清楚地了解 Hora 的独特优势,这里是一个详细的对比:

🔥 jiffy 根本没有的功能:

功能类别 Hora jiffy 生产价值
业务日计算 ✅ 完整支持 ❌ 无 金融、企业应用必备
时区转换 ✅ 内置支持 ❌ 需要额外包 全球化应用必备
财年管理 ✅ 多国财年 ❌ 无 财务系统必备
重复事件 ✅ 复杂模式 ❌ 基础模式 订阅系统必备
自定义解析 ✅ 灵活配置 ❌ 有限支持 数据导入必备
日历生成 ✅ 月历/年历 ❌ 无 调度系统必备
详细时间差 ✅ 多种格式 ❌ 基础格式 分析报表必备
节假日管理 ✅ 多国日历 ❌ 无 本地化应用必备

💡 为什么这些功能很重要?

在实际的企业级应用中,你迟早会遇到这些需求:

  1. 业务日计算 - 订单处理、物流配送的时效承诺
  2. 时区处理 - 全球用户的同步操作
  3. 财年管理 - 财务报表、预算规划
  4. 重复事件 - 订续计费、定期提醒
  5. 自定义解析 - 处理各种来源的数据

jiffy 只能帮你做基础的日期操作,而 Hora 让你能够构建完整的企业级应用。

写在最后

从 jiffy 用户到 Hora 作者,这段开发经历让我学到了很多。我开始只是想解决自己在项目中的痛点,没想到最终创造了一个功能完整的日期库。

Hora 的核心设计理念其实很简单:

  1. 实用至上 - 解决真实的业务问题,而不是为了功能而功能
  2. 渐进增强 - 从简单的格式化开始,按需引入高级功能
  3. 保持简单 - 复杂的内部实现,简单的外部接口
  4. 性能优先 - 树枝摇动、零依赖,不影响应用性能

如果你也遇到了和我类似的困扰,希望 Hora 能帮你节省时间,让你专注于业务逻辑而不是日期处理的细节。

这个项目还在持续改进中,欢迎任何反馈和建议。


🎯 适用场景

  • 企业级应用:需要复杂的业务日期计算
  • 跨平台项目:Flutter Web、Mobile、Desktop 统一 API
  • 国际化产品:143+ 语言开箱即用
  • 性能敏感应用:轻量级实现,tree-shakable

🚀 立即开始

bash 复制代码
dart pub add hora

GitHub : github.com/fluttercand...
Pub.dev : pub.dev/packages/ho...
文档 : 完整 API 文档

如果 Hora 对你的项目有帮助,欢迎给项目点个 ⭐️ Star,你的支持是我持续开发的动力!同时也别忘了在 Pub.dev 上点个 👍 Like,让更多开发者发现这个库!

让我们一起,用 Hora 让日期时间处理变得简单而优雅。

相关推荐
karshey1 小时前
【前端】iView表单校验失效:Input已填入时,报错为未填入
前端·view design
写代码的皮筏艇2 小时前
React中的'插槽'
前端·javascript
韩曙亮2 小时前
【Web APIs】元素可视区 client 系列属性 ② ( 立即执行函数 )
前端·javascript·dom·client·web apis·立即执行函数·元素可视区
用户4445543654262 小时前
Android协程底层原理
前端
秋邱2 小时前
AR 技术创新与商业化新方向:AI+AR 融合,抢占 2025 高潜力赛道
前端·人工智能·后端·python·html·restful
xiaoyan20152 小时前
自研2025版flutter3.38实战抖音app短视频+聊天+直播商城系统
android·flutter·dart
www_stdio2 小时前
JavaScript 原型继承与函数调用机制详解
前端·javascript·面试
kirk_wang2 小时前
为OpenHarmony移植Flutter Printing插件:一份实战指南
flutter·移动开发·跨平台·arkts·鸿蒙
羽沢312 小时前
vue3 + element-plus 表单校验
前端·javascript·vue.js