一个用于使用 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 可以通过提供 year、month 和 day 等方式进行实例化。
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 访问 year、month、day。您可以使用 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。您可以将 hour、minute 和其他时间详细信息传递给参数。还有 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(...) 方法或现有对象上的 withYear、withMonth 和 withDay 方法。这些方法可以链式调用。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 日期之间的距离。请注意,- 运算符用于其他目的。或者您可以使用 distanceTo 和 distanceFrom 方法。
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 添加或减去天数。保证会得到一个边界有效的日期。例如,它会根据需要进入下一个月或下一年,并且不会发生闰年崩溃。
您可以使用 addYears、addMonths 和 addDays 向 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/TwoDigitDay 或 YYYY/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 方法对它们进行比较。您也可以使用比较运算符对它们进行比较。它们还支持 equals 和 hashCode 函数。因此,您可以安全地使用 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);
}