一个用于使用 Jalali(Shamsi、Solar、波斯语、شمسی 或 خورشیدی)日期的 Flutter 包。您可以转换、格式化和操作 Jalali 和公历日期。

这是一个纯 Dart 包,其算法基于流行的 JavaScript 库 jalaali-js,每月下载量超过 20k。

此包具有大量单元测试,并具有高测试覆盖率,以确保其正确性。

主要特点

  • Jalali公历 和 Flutter 的 DateTime 对象之间进行转换。
  • 通过 getter 访问年、月、日、星期、儒略日数、月份长度等。
  • 使用 DateFormatter 使用简单而强大的语法格式化 Jalali 和公历日期。
  • 确保 Jalali 和公历日期的有效性。
  • 检查 Jalali 或公历年份是否为闰年。
  • 不可变的日期对象,带有 copy 方法,便于操作。
  • 使用比较运算符或 Comparable 轻松比较日期。
  • 使用 +- 运算符添加或减去天数。
  • 通过方法和 ^ 运算符查找日期之间的距离。
  • 单独或组合地添加年、月、日。
  • 具有大量单元测试的高代码覆盖率。
  • 空安全 API

最近的更改

从版本 0.16.0 开始,添加了 toUTCDateTime 方法,并且 `toDateTime` 具有更多功能。

问题和功能请求

如果您想要一个新功能,或者发现了一个问题,请在 GitHub 上创建一个 issue,以便我能看到您的请求。

用法

将其添加到您的 pubspec.yaml 文件中

dependencies:
    shamsi_date: ^latest.version

然后依赖它

import 'package:shamsi_date/shamsi_date.dart';

如果您想要扩展方法,也请依赖扩展方法

import 'package:shamsi_date/extensions.dart';

Jalali 类用于Shamsi(Jalali、波斯语、شمسی 或 خورشیدی)日期,Gregorian 类用于公历(Miladi 或 میلادی)日期。Jalali 和 Gregorian 类是 Date 的子类。

Jalali 和 Gregorian 可以通过提供 yearmonthday 等方式进行实例化。

Jalali j = Jalali(year, month, day);
Gregorian g = Gregorian(year, month, day);

如果未指定月份和日期,则默认为 1,因此 Jalali(year, month) 等同于 Jalali(year, month, 1)Gregorian(year) 等同于 Gregorian(year, 1, 1)

构造函数参数应为非空,否则将立即抛出异常。这可确保对象在创建时处于有效状态。因此,年、月、日始终是非空的。几乎所有方法、运算符、构造函数和工厂都应具有非空参数,并且它们将返回非空对象。例如,年、月、日 getter 将返回非空结果。唯一可以接受 null 参数的方法是具有可选参数的方法,如 add(...)copy(...)在空安全版本中:将静态地检查可空和非可空参数及返回类型。

所有创建的日期实例都是有效的。当使用构造函数和工厂创建日期实例,或对现有日期实例使用方法和运算符创建日期实例时,如果新日期无效(其月份或日期超出范围),或者超出可计算范围,则会抛出 DateException 异常。因此,如果您认为新日期实例可能无效或超出范围,则应将其放在 try-catch 中并捕获 DateException。可计算的最小日期是 Gregorian(560,3,20) 或等效的 Jalali(-61,1,1),可计算的最大日期是 Gregorian(3798,12,31) 或等效的 Jalali(3177,10,11)。例如

void main() {
  try {
    Jalali jv = Jalali(1398, 13, 1); // not valid!
  } on DateException catch (e) {
    // prints: DateException: Jalali month is out of valid range.
    print(e);
  }
}

Jalali 和 Gregorian 对象是不可变的。因此,使用运算符和方法会得到新对象,而不会原地修改对象,就像 String 对象一样。shamsi_date 库中的几乎所有其他对象也是不可变的。

您可以通过 Jalali 或 Gregorian 日期的 getter 访问 yearmonthday。您可以使用 weekDay getter 获取 Jalali 和 Gregorian 的星期几。星期几的范围是 1 到 7。Jalali 星期以 Shanbe 开始,Gregorian 星期以 Monday 开始。可以使用 monthLength getter 访问月份长度。月份长度对闰年敏感。您可以使用 isLeapYear() 方法检查年份是否为闰年。儒略日数也可通过 julianDayNumber getter 访问。例如

Jalali j = Jalali(1397, 5, 6);

int jy = j.year; // jy = 1397
int jm = j.month; // jm = 5
int jd = j.day; // jd = 6

int wd = j.weekDay; // wd = 1 (Shanbe)

// month length of 1397/5
// note: day value is not important for monthLength
int ml = j.monthLength; // ml = 31

// check if 1397 is a leap year
// note: month and day values are not important for isLeapYear() method
bool ly = j.isLeapYear(); // ly = false (1397 is not leap year)

// and equivalently for Gregorian date objects ...

您可以使用 toGregorian() 方法将 Jalali 日期转换为 Gregorian,使用 toJalali() 方法将 Gregorian 转换为 Jalali 日期。还有工厂方法 Jalali.fromGregorian(...)Gregorian.fromJalali(...) 可供选择。

Jalali j = Jalali(1397, 5, 6);
// convert to Gregorian:
Gregorian j2g1 = j.toGregorian(); // -> 2018/8/28
// or equivalently:
Gregorian j2g2 = Gregorian.fromJalali(j);

Gregorian g = Gregorian(2019, 10, 26);
// convert to Jalali:
Jalali g2j1 = g.toJalali(); // -> 1398/8/4
// or equivalently:
Jalali g2j2 = Jalali.fromGregorian(g);

您可以使用 fromDateTime(dateTime) 静态方法将 DateTime 对象直接转换为 Jalali 或 Gregorian 日期。使用 toDateTime() 方法将 Jalali 和 Gregorian 转换为 DateTime。您可以将 hourminute 和其他时间详细信息传递给参数。还有 toUTCDateTime 用于 UTC 日期时间。通过使用 now() 工厂获取当前的 Jalali 和 Gregorian 日期。

// convert from DateTime
Jalali j = Jalali.fromDateTime(dateTime);
Gregorian g = Gregorian.fromDateTime(dateTime);

// convert to DateTime
DateTime j2dt = j.toDateTime();
DateTime g2dt = g.toDateTime();

// you can also add hour, minute, ...
DateTime j2dt1 = j.toDateTime(13, 25, 48);
// and also convert to UTC:
DateTime j2dt2 = j.toUTCDateTime(13, 25, 48);

// get now
Jalali jNow = Jalali.now();
Gregorian gNow = Gregorian.now();

要转换 DateTime,您还可以使用扩展方法。

DateTime dt = DateTime.now();
Jalali j = dt.toJalali();
Gregorian g = dt.toGregorian();

Jalali 和 Gregorian 日期是不可变的,因此您无法原地更改它们的属性。如果您只想更改 Jalali 或 Gregorian 日期的某些字段,可以使用 copy(...) 方法或现有对象上的 withYearwithMonthwithDay 方法。这些方法可以链式调用。copy 方法一次更改所有字段。请注意,copy 和 with*() 方法不安全,您有责任避免中间步骤中的月份长度限制(例如,将 31 Farvardin 1390 的月份更改为 Esfand)或闰年崩溃(例如,在闰年的最后一天,将年份更改为非闰年)等问题。操作顺序很重要。

例如,要获取 Jalali 中本月开始的日期:(copy 方法会创建另一个对象实例,而不会修改原始对象)

Jalali j1 = Jalali.now().withDay(1); // correct way
// or by using copy method:
Jalali j2 = Jalali.now().copy(day: 1); // also correct

// DON NOT do it like this:
Jalali j3 = Jalali(Jalali.now().year, Jalali.now().month, 1); // INCORRECT

或者,如果您想获取本 Jalali 年最后一个月的最后一天

// at first go to first day of last month: (Avoid leap crash)
Jalali tmp = Jalali.now().withDay(1).withMonth(12);
// since we can be in a leap year we use monthLength for going to last day:
Jalali j = tmp.withDay(tmp.monthLength);

// or by using copy method:
Jalali tmp1 = Jalali.now().copy(month: 12, day: 1);
Jalali j1 = tmp.copy(day: tmp1.monthLength);

或者,要查找本年第二个​​月的第三天

Jalali j = Jalali.now().withDay(3).withMonth(2);

// or by using copy method:
Jalali j1 = Jalali.now().copy(month: 2, day: 3);

或者,如果您希望您的 Jalali 和 Gregorian 对象在构造函数参数提供 null 时回退到今天,您可以使用 now 工厂方法中的 copy 方法,例如对于 Jalali

Jalali j = Jalali.now().copy(year: y, month: m, day: d);
// y, m and d can be null

您可以使用 ^ 运算符查找 Jalali 和 Gregorian 日期之间的距离。请注意,- 运算符用于其他目的。或者您可以使用 distanceTodistanceFrom 方法。

int distance11 = Jalali.now() ^ Jalali(1395, 10, 1);
// or
int distance12 = Jalali.now().distanceFrom(Jalali(1395, 10, 1));
// or
int distance13 = Jalali(1395, 10, 1).distanceTo(Jalali.now());

// and similarly for Gregorian
int distance2 = Gregorian(2021) ^ Gregorian(2020);

您可以使用 +- 运算符向 Jalali 和 Gregorian 添加或减去天数。保证会得到一个边界有效的日期。例如,它会根据需要进入下一个月或下一年,并且不会发生闰年崩溃。

您可以使用 addYearsaddMonthsaddDays 向 Jalali 和 Gregorian 添加年、月或日。这些方法可以链式调用,并且不会发生范围崩溃。addDays 可以更改月份和年份。addMonths 可以更改年份。请注意,您有责任避免闰年崩溃。

如果您愿意,可以使用 add 方法将几天、几个月或几年添加到日期对象。请注意add 方法不安全,也不会修改结果以使其成为边界有效的,这是您的责任。建议使用 addYear、addMonth 和 addDay 方法而不是 add 方法。请注意,使用 addYears、addMonth 和 addDay 时,可能会将日期超出月份长度的范围。addMonth 对于月份溢出是安全的。

Jalali j1 = Jalali(1398, 8, 4);
// add days
Jalali j2 = j1 + 3; // -> 1398/8/7
// result will be manipulated to become valid:
Jalali j3 = j1 + 30; // -> 1398/9/4
Jalali j4 = j1 + 365; // -> 1399/8/4
// subtract days
Jalali j5 = j1 - 2; // -> 1398/8/2

// add years, months and days:
Jalali j6 = j1.addYears(1).addMonths(2).addDays(3); // 1399/10/7
// or:
Jalali j60 = j1.add(years: 1, months: 2, days: 3); // 1399/10/7
// add years and days only:
Jalali j7 = j1.addYears(1).addDays(3); // 1399/8/7
// or:
Jalali j70 = j1.add(years: 1, days: 3); // 1399/8/7
// add months only:
Jalali j8 = j1.addMonths(2); // 1398/10/3
// or:
Jalali j80 = j1.add(months: 2); // 1398/10/3
// if you want to subtract you can add negative value:
Jalali j9 = j1.addYears(-1); // 1397/8/3
// or:
Jalali j90 = j1.add(years: -1); // 1397/8/3

// addYears, addMonths and addDays methods are bound safe
// add(...) method is NOT bound safe

日期格式化很简单。您应该为自定义格式创建一个函数,然后将您的 Jalali 或 Gregorian 日期传递给该函数。

例如,如果您想格式化为 WeekDayName Day MonthName TwoDigitYear,请创建一个函数

String format1(Date d) {
  final f = d.formatter;

  return '${f.wN} ${f.d} ${f.mN} ${f.yy}';
}

// example output for Jalali: "پنج شنبه 21 دی 91"
// example output for Gregorian: "Thursday 10 January 13"

或者,如果您想格式化为 FourDigitYear/TwoDigitMonth/TwoDigitDayYYYY/MM/DD,请创建一个函数

String format2(Date d) {
  final f = d.formatter;

  return '${f.yyyy}/${f.mm}/${f.dd}';
}

然后像以前一样使用它。

请注意,格式化器会以英文格式化数字,因此如果您想要波斯数字,可以使用带有波斯数字的字体,或对格式化器的输出应用简单的映射,将英文数字更改为波斯数字。

Jalali 和 Gregorian 日期支持 toString() 方法。对于 Jalali,它在语义上等同于使用格式化器 Jalali(Y,M,D),这意味着

String toStringFormatter(Jalali d) {
  final f = d.formatter;

  return 'Jalali(${f.y},${f.m},${f.d})';
}

对于 Gregorian,toString() 等同于使用格式化器 Gregorian(Y,M,D)

注意:在下面的代码中,toString() 被隐式调用

void main() {
    print(Jalali.now());
    final str = 'today is: ${Georgian.now()}';
}

仅将 Jalali 和 Gregorian 日期的 toString() 用于开发目的,例如调试、日志记录等。您应该在 UI 上显示日期时使用格式化器。

另请注意,例如,您不需要对 Jalali.now().formatter.m 的格式化器输出使用 int.parse() 来访问其月份,只需使用 Jalali.now().month 即可。

DateFormatter 具有以下 getter

  • y: 年(无论其长度如何)。年应为正数。
  • yy: 两位数的年份。年份应在 1000 到 9999 之间。
  • yyyy: 四位数的年份。年份应在 0 到 9999 之间。
  • m: 月份(无论其长度如何)。
  • mm: 两位数的月份。
  • mN: 月份名称。
  • d: 日期(无论其长度如何)。
  • dd: 两位数的日期。
  • wN: 星期名称。

您可以通过 Jalali 和 Gregorian 日期对象使用 formatter getter 来获取日期格式化器。只需将此格式化器缓存到 Jalali 值中,然后使用字符串插值(如示例所示)来创建所需的输出。这种格式化方式比使用模板更强大(而且可以说更容易)。

Jalali 和 Gregorian 类是 Comparable,因此您可以使用 compareTo 方法对它们进行比较。您也可以使用比较运算符对它们进行比较。它们还支持 equalshashCode 函数。因此,您可以安全地使用 Jalali 和 Gregorian 日期的 Set 和 Map。

Jalali j1 = Jalali(1397, 1, 1);
Jalali j2 = Jalali(1397, 2, 1);

bool b1 = j1 < j2; // b1 = true
bool b2 = j1 >= j2; // b2 = false
// using Comparable compareTo
bool b3 = j1.compareTo(j2) > 0; // b3 = false (means j1 > j2 is false)
bool b4 = j1.compareTo(j2) <= 0; // b4 = true (means j1 <= j2 is true)
bool b5 = j1 == j2; // b5 = false
bool b6 = j1 != j2; // b6 = true

示例

这是一个完整的示例。如果您找不到所需的内容,可以查看 test/shamsi_date_test.dart 文件,其中包含单元测试。

import 'package:shamsi_date/shamsi_date.dart';
import 'package:shamsi_date/extensions.dart';

void main() {
  // Gregorian to Jalali conversion
  Gregorian g1 = Gregorian(2013, 1, 10);
  Jalali j1 = g1.toJalali();
  print('$g1 == $j1');
  // prints: Gregorian(2013,1,10) == Jalali(1391,10,21)
  // you can write Jalali.fromGregorian(g1) instead of g1.toJalali()

  // access year, month and day through getters
  // for Jalali:
  int j1y = j1.year; // j1y = 1391
  int j1m = j1.month; // j1m = 10
  int j1d = j1.day; // j1d = 21
  print('j1 is $j1y-$j1m-$j1d'); // prints: j1 is 1397-10-21
  // NOTE: use formatters for formatting dates
  // and for Gregorian:
  int g1y = g1.year; // g1y = 2013
  int g1m = g1.month; // g1m = 1
  int g1d = g1.day; // g1d = 10
  print('g1 is $g1y-$g1m-$g1d'); // prints: g1 is 2013-1-10
  // NOTE: use formatters for formatting dates

  // Jalali to Gregorian conversion
  Jalali j2 = Jalali(1391, 10, 21);
  Gregorian g2 = j1.toGregorian();
  print('$j2 == $g2');
  // prints: Jalali(1391,10,21) == Gregorian(2013,1,10)
  // also can use Gregorian.fromJalali(j1) instead of j1.toGregorian()

  // find weekDay
  print('$j1 has weekDay ${j1.weekDay}'); // -> 6
  // 6 means "پنج شنیه"
  print('$g1 has weekDay ${g1.weekDay}'); // -> 4
  // 4 means "Thursday"

  // find month length
  print('Jalali 1390/12 month length? '
      '${Jalali(1390, 12).monthLength}'); // -> 29
  print('Gregorian 2000/2 month length? '
      '${Gregorian(2000, 2).monthLength}'); // -> 29

  // check leap year
  print('1390 Jalali is leap year? '
      '${Jalali(1390).isLeapYear()}'); // -> false
  print('2000 Gregorian is leap year? '
      '${Gregorian(2000).isLeapYear()}'); // -> true

  // validity:
  // ALL created instances are considered VALID
  // if you think a date might invalid, use try-catch:
  try {
    Jalali jv = Jalali(1398, 13, 1); // not valid!
    print(jv); // this line is not reached
  } on DateException catch (e) {
    // prints: DateException: Jalali month is out of valid range.
    print(e);
  }
  // making leap crash will also throw exception:
  // for ex: Jalali(1394, 12, 30) will crash, since
  //  1394 is not leap year
  // creating dates out of computable range also throws DateException.

  // convert DateTime object to Jalali and Gregorian
  DateTime dateTime = DateTime.now();
  print('now is $dateTime');
  print('now is ${Gregorian.fromDateTime(dateTime)} in Gregorian');
  print('now is ${Jalali.fromDateTime(dateTime)} in Jalali');
  // convert to DateTime
  print('$j1 is ${j1.toDateTime()}');
  print('$g1 is ${g1.toDateTime()}');

  // convert Jalali and Gregorian to DateTime
  print('$j1 as DateTime is ${j1.toDateTime()}');
  print('$g1 as DateTime is ${g1.toDateTime()}');

  // find today with now() factory method
  print('now is ${Gregorian.now()} in Gregorian');
  print('now is ${Jalali.now()} in Jalali');
  // find out which jalali year is this year:
  int thisJalaliYear = Jalali.now().year;
  print('this Jalali year is $thisJalaliYear');

  // copy method
  print('$j1 with year = 1300 is ${j1.copy(year: 1300)}');
  // prints: 1391/10/21 with year = 1300 is 1300/10/21
  print('$g1 with month = 1 and day = 2 is ${g1.copy(month: 1, day: 2)}');
  // prints: 2013/1/10 with month = 1 and day = 2 is 2013/1/2

  // withYear, withMonth and withDay methods:
  // these methods can be chained
  // it is recommended to use these methods over copy method
  print('$j1 with year = 1300 is ${j1.withYear(1300)}');
  // prints: 1391/10/21 with year = 1300 is 1300/10/21
  print('$g1 with month = 1 and day = 2 is ${g1.withDay(2).withMonth(1)}');
  // prints: 2013/1/10 with month = 1 and day = 2 is 2013/1/2

  // for example for getting date at start of this month in Jalali:
  print(Jalali.now().copy(day: 1));
  // for example to find 3rd day of 2nd month of this year:
  print(Jalali.now().copy(month: 2, day: 3));
  // DON NOT do it like this:
  print(Jalali(Jalali.now().year, Jalali.now().month, 1)); // INCORRECT
  // for example if you want to get
  // the last day of the last month of this Jalali year:
  Jalali tmp = Jalali.now().copy(month: 12, day: 1);
  // since we can be in a leap year we use monthLength:
  print(tmp.copy(day: tmp.monthLength));

  // add and subtract days
  Jalali d1 = Jalali(1398, 8, 4);
  // add days
  print(d1 + 3); // -> 1398/8/7
  // result will be manipulated to become valid:
  print(d1 + 30); // -> 1398/9/4
  print(d1 + 365); // -> 1399/8/4
  // subtract days
  print(d1 - 2); // -> 1398/8/2
  // add years, months and days:
  print(d1.add(years: 1, months: 2, days: 3)); // 1399/10/7
  // add years and days only:
  print(d1.add(years: 1, days: 3)); // 1399/8/7
  // add months only:
  print(d1.add(months: 2)); // 1398/10/3
  // if you want to subtract you can add negative value:
  print(d1.add(years: -1)); // 1397/8/3
  // and also for Gregorian

  // you can find distance between two days with "^" operator
  int distance11 = Jalali.now() ^ Jalali(1395, 10);
  int distance12 = Jalali.now().distanceFrom(Jalali(1395, 10));
  int distance13 = Jalali(1395, 10).distanceTo(Jalali.now());
  print('distance $distance11 $distance12 $distance13');
  // and similarly for Gregorian

  // or you can use addYears, addMonths and addDays method
  // it is recommended to use these methods over add method
  // these methods are bound valid which means result will be
  //  manipulated to become valid, but add method is not
  print(d1.addDays(30)); // -> 1398/9/4
  print(d1.addDays(365)); // -> 1399/8/4
  print(d1.addYears(1).addMonths(2).addDays(3)); // 1399/10/7
  print(d1.addYears(1).addDays(3)); // 1399/8/7
  print(d1.addMonths(2)); // 1398/10/3
  print(d1.addYears(-1)); // 1397/8/3

  // formatting examples:

  // example one:
  String format1(Date d) {
    final f = d.formatter;

    return '${f.wN} ${f.d} ${f.mN} ${f.yy}';
  }

  print(format1(j1)); // prints: پنج شنبه 21 دی 91
  print(format1(g1)); // prints: Thursday 10 January 13

  // example one:
  String format2(Date d) {
    final f = d.formatter;

    return '${f.dd}/${f.mm}/${f.yyyy}';
  }

  print(format2(j1)); // prints: 21/10/1391
  print(format2(g1)); // prints: 10/01/2013

  // DO NOT use formatter for accessing year, month or other properties
  // of date objects they are available as getters on date objects
  // INCORRECT EXAMPLE, DO NOT USE THIS:
  int j1y1 = int.parse(j1.formatter.yyyy); // INCORRECT
  print("j1's year is $j1y1");
  // use this:
  int j1y2 = j1.year; // correct
  print("j1's year is $j1y2");
  // also using toString() for showing dates on UI is not recommended,
  // use custom formatter.

  // comparing dates examples:
  print(j1 > j2); // -> false
  print(j1.compareTo(j2) > 0); // -> false
  print(j1 <= j2); // -> true
  print(j1.compareTo(j2) <= 0); // -> true
  print(g1 >= g2); // -> true
  print(g1.compareTo(g2)); // -> 0
  print(g1 == g2); // -> true
  print(g1 != g1); // -> false

  // if you want to compare Jalali with Georgian
  // you can convert one type to another,
  // for example:
  print(j1.toGregorian() == g1); // -> true
  // but if you don't want to convert them you can use julianDayNumber
  // (this approach is not recommended)
  print(j1.julianDayNumber == g1.julianDayNumber); // -> true
  // this means that they are equal
  // you can also use other comparison operators

  // you can use extension methods for DateTime
  final dtn = DateTime.now();
  print(dtn);
  final jn = dtn.toJalali();
  print(jn);
  final gn = dtn.toGregorian();
  print(gn);
}

GitHub

https://github.com/FatulM/shamsi_date