jalali_date
Persian (Jalali, Shamsi) calendar for Dart.
It converts between Gregorian and Jalali dates in both directions, knows leap years and month lengths, does date arithmetic, parses and formats dates with Persian month and weekday names and Persian digits, and has everything a date picker needs: Saturday-first weeks, month anchors, day ranges, and bounds.
It reads dates written with a pattern or with month names, writes relative time (3 روز پیش), dates in Persian words, and parts of the day (صبح, عصر), counts whole months and years, and gives the moment of Nowruz (سال تحویل).
The conversion is a port of jalaali-js, so century years and leap cycles are handled correctly.
It also gives the lunar Hijri date as Iran's official calendar counts it, and writes and reads years in the Shahanshahi numbering when you ask for it.
Iranian official holidays are in the companion package iranian_holidays: it calculates them offline on the device, the lunar Hijri ones included, or fetches them from an online source you choose (holidayapi.ir, Calendarific, or Google Calendar), and can fall back from online to offline.
It is pure Dart with no dependencies and works on the Web, Flutter, and the Dart VM.
Also known as the Shamsi, Khorshidi, Solar Hijri, Hijri Shamsi, or Iranian calendar. Keywords: تقویم شمسی، تقویم جلالی، تقویم خورشیدی، هجری شمسی، هجری قمری، سال شاهنشاهی، تعطیلات رسمی ایران، تاریخ فارسی، تبدیل تاریخ میلادی به شمسی و شمسی به میلادی.
معرفی به فارسی
jalali_date یک بستهی خالص Dart برای تقویم هجری شمسی (جلالی، خورشیدی) است.
تاریخ میلادی را به شمسی و شمسی را به میلادی تبدیل میکند، سال کبیسه و طول ماهها را میداند و تاریخ را با نام فارسی ماهها و روزهای هفته و ارقام فارسی نمایش میدهد.
جمع و تفریق روز و ماه و سال، خواندن تاریخ از متن (با ارقام فارسی یا لاتین)، هفتهی شنبه تا جمعه و بازهی تاریخ را هم دارد و برای ساختن انتخابگر تاریخ (date picker) آماده است.
تاریخ نوشتهشده با الگو یا نام ماه (مثل ۴ فروردین ۱۴۰۳) را میخواند، زمان نسبی (۳ روز پیش)، تاریخ با حروف و بخشهای روز (صبح، عصر، شب) را مینویسد، ماهها و سالهای کامل (مثلاً سن) را میشمارد و لحظهی تحویل سال را میدهد.
تاریخ هجری قمری را مطابق تقویم رسمی ایران هم میدهد و در صورت نیاز سال را به شاهنشاهی مینویسد و میخواند.
تعطیلات رسمی ایران در بستهی همراه iranian_holidays است: آنها را بدون اینترنت روی خود دستگاه محاسبه میکند، تعطیلات قمری هم، یا از یک منبع آنلاین به انتخاب شما (holidayapi.ir، Calendarific یا Google Calendar) میگیرد، و اگر منبع آنلاین در دسترس نباشد میتواند به محاسبهی آفلاین برگردد.
الگوریتم تبدیل از jalaali-js گرفته شده و برای سالهای قرن میلادی و چرخههای کبیسه درست کار میکند.
به Flutter وابسته نیست و روی وب، Flutter و Dart VM اجرا میشود.
final date = PersianDate.fromGregorian(2021, 3, 21);
print(date); // 1400/01/01
print(date.format('EEEE d MMMM yyyy')); // یکشنبه 1 فروردین 1400
print(date.toString(persianDigits: true)); // ۱۴۰۰/۰۱/۰۱
print(PersianDate.parse('۱۳۵۷/۱۱/۲۲').toDateTime()); // 1979-02-11 00:00:00.000
Installation
jalali_date is published on pub.dev:
dart pub add jalali_date
Or with Flutter:
flutter pub add jalali_date
Then import the single library:
import 'package:jalali_date/jalali_date.dart';
Quick reference
Everything is exported from package:jalali_date/jalali_date.dart: the immutable PersianDate class, the PersianDateRange class, the HijriDate and IranianHijri classes, the toPersianDigits, toLatinDigits, and toPersianWords functions, the JalaliEra enum, and the DateTime.toPersianDate() and DateTime.toHijriDate() extensions.
Every class and member is described in the API reference.
Creating
| Member | Description |
|---|---|
PersianDate(int year, int month, int day) |
From Jalali values, date-only; throws ArgumentError for an invalid date |
PersianDate.withTime(year, month, day, [hour, minute, second]) |
From Jalali values with a local time of day |
PersianDate.now() |
Current local date and time |
PersianDate.fromDateTime(DateTime) |
From a DateTime; keeps time of day and time zone |
DateTime.toPersianDate() |
Extension, the same as fromDateTime |
PersianDate.fromGregorian(int year, int month, int day) |
From Gregorian values; overflow rolls over like DateTime |
PersianDate.fromGregorianString(String) |
From an ISO 8601 Gregorian string, via DateTime.parse |
PersianDate.parse(String, {era}), tryParse(String, {era}) |
From a Jalali string such as 1403/01/01 or ۱۴۰۳-۱-۱ ۱۴:۳۰ |
PersianDate.parseFormat(String, String pattern, {era}), tryParseFormat(...) |
From text written with a format pattern, such as ۴ فروردین ۱۴۰۳ |
PersianDate.fromJson(String), toJson() |
yyyy-MM-dd for storage and jsonEncode |
PersianDate.min, PersianDate.max |
First and last supported day |
copyWith({year, month, day}) |
Copy with fields replaced; validates, never clamps |
Reading
| Member | Description |
|---|---|
year, month, day |
Jalali year, month (1 to 12), and day of month |
shahanshahiYear |
year plus 1180, the Shahanshahi (Imperial) year |
hasTime, hour, minute, second |
Time of day; the getters throw StateError when hasTime is false |
dayPeriod |
Part of the day: نیمهشب, بامداد, صبح, ظهر, عصر, شب |
isLeapYear, daysInYear |
Whether Esfand has 30 days; 365 or 366 |
PersianDate.nowruzMoment(int year) |
The March equinox that begins the year (سال تحویل), in UTC |
monthLength |
31, 30, or 29 |
dayOfYear |
1 to 366 |
quarter, season, seasonName |
1 to 4, and بهار to زمستان |
monthName, monthNameLatin, monthNameDari |
فروردین, Farvardin, حمل (Afghanistan) |
weekday |
ISO weekday, 1 is Monday and 7 is Sunday, like DateTime.weekday |
weekdayPersian |
1 is Saturday and 7 is Friday, the column in a Persian calendar grid |
weekdayName, weekdayNameShort, weekdayNameLatin |
شنبه, ش, Shanbeh |
weekOfYear |
Saturday-first week number, week 1 contains 1 Farvardin |
isWeekend |
Friday |
isToday |
Same day as PersianDate.now() |
monthNames, monthNamesLatin, monthNamesDari, weekdayNames, weekdayNamesShort, weekdayNamesLatin, seasonNames |
Static name lists; weekday lists are indexed by ISO weekday minus one |
Calculating
| Member | Description |
|---|---|
addDays(int), addMonths(int), addYears(int) |
Negative values go back; months and years clamp the day |
differenceInDays(PersianDate) |
Whole days from the other date to this one |
differenceInMonths(PersianDate), differenceInYears(PersianDate) |
Whole Jalali months or years, counted like addMonths |
firstDayOfMonth, lastDayOfMonth, firstDayOfYear, lastDayOfYear |
Anchors |
startOfWeek(), endOfWeek() |
Saturday and Friday of the same week |
daysUntil(PersianDate end, {inclusive}) |
Lazy sequence of days |
PersianDateRange(start, end) |
Inclusive range with length, days, contains, overlaps |
Comparing and converting
| Member | Description |
|---|---|
==, compareTo, <, <=, >, >= |
By day only; the time of day is ignored |
isBefore, isAfter, isSameDay, isSameMonth, isSameYear |
Readable comparisons |
toDateTime() |
Gregorian DateTime |
toHijri() |
Lunar Hijri date, see Hijri calendar |
toString({showDate, showTime, showHour, showMinute, showSecond, persianDigits, era}) |
yyyy/MM/dd, optionally with HH:mm:ss |
format(String pattern, {persianDigits, era}) |
Custom string, tokens listed below |
toPersianDigits(String), toLatinDigits(String) |
Digit conversion, Persian and Arabic-Indic input |
toPersianWords(int, {ordinal}) |
هزار و چهارصد و سه, بیست و چهارم |
relativeTo(PersianDate, {numeric, persianDigits}), fromNow(...) |
دیروز, 3 روز پیش, ماه آینده |
Supported range: Jalali -61/01/01 to 3177/10/11, which is Gregorian 560-03-20 to 3798-12-31.
Usage
Creating dates
// From Jalali values. Throws ArgumentError for invalid dates.
final a = PersianDate(1400, 1, 1);
// From Jalali values with a local time of day.
final b = PersianDate.withTime(1400, 1, 1, 14, 30);
// From the current date and time.
final c = PersianDate.now();
// From a DateTime. The time of day and time zone are kept.
final d = PersianDate.fromDateTime(DateTime.utc(2018, 7, 7, 21, 43, 44));
final e = DateTime.now().toPersianDate();
// From Gregorian values. Out-of-range values roll over like DateTime.
final f = PersianDate.fromGregorian(2021, 3, 21);
// From an ISO 8601 Gregorian string, parsed with DateTime.parse.
final g = PersianDate.fromGregorianString('2018-07-07T21:43:44.909Z');
print(a.year); // 1400
print(a.month); // 1
print(a.day); // 1
Date-only and time of day
A PersianDate is either date-only or carries a time of day.
Dates made with PersianDate(...), parse without a time, copyWith, arithmetic, and the anchors are date-only.
Dates made from a DateTime, with PersianDate.withTime, or by parse with a time carry one.
final date = PersianDate.withTime(1403, 1, 4, 9, 5);
print(date.hasTime); // true
print(date.hour); // 9
print(date.addDays(1).hasTime); // false
print(PersianDate(1403, 1, 4).hasTime); // false
hour, minute, second, toString(showTime: true), and the time tokens of format throw StateError on a date-only instance.
Jalali to Gregorian
print(PersianDate(1357, 11, 22).toDateTime()); // 1979-02-11 00:00:00.000
A date created from a DateTime returns that exact DateTime, with its time and time zone.
A date-only instance returns local midnight of that day.
Parsing
PersianDate.parse('1403/01/01'); // date-only
PersianDate.parse('1403-1-1'); // date-only
PersianDate.parse('۱۴۰۳/۰۱/۰۱ ۱۴:۳۰'); // with a local time of day
PersianDate.parse('1403-01-01T14:30:15'); // with seconds
PersianDate.tryParse('1400/12/30'); // null, 1400 is not a leap year
parse accepts / or - between the parts (the same one twice), one- or two-digit month and day, an optional HH:mm or HH:mm:ss, and ASCII, Persian, or Arabic-Indic digits.
It throws FormatException for anything else, including invalid dates; tryParse returns null instead.
The output of toString, toJson, and format('yyyy/MM/dd') parses back.
Parsing with a pattern
PersianDate.parseFormat('۴ فروردین ۱۴۰۳', 'd MMMM yyyy'); // 1403/01/04
PersianDate.parseFormat('1403 Farvardin 4', 'yyyy MMMM d'); // 1403/01/04
PersianDate.parseFormat('سه شنبه ۷ فروردين ۱۴۰۳', 'EEEE d MMMM yyyy'); // 1403/01/07
PersianDate.parseFormat('1403/01/04 2:30 ب.ظ', 'yyyy/MM/dd h:mm a'); // with 14:30
PersianDate.tryParseFormat('دوشنبه ۷ فروردین ۱۴۰۳', 'EEEE d MMMM yyyy'); // null, 7 Farvardin 1403 is a Tuesday
parseFormat reads text written with the format tokens and quoting, so PersianDate.parseFormat(date.format(pattern), pattern) gives the date back.
The pattern needs yyyy, a month, and a day; MM and dd need two digits, while M and d take one or two.
MMMM reads Persian, Latin, and Dari month names, and EEEE, E, D, Q, a, and B must agree with the date.
Persian and Arabic-Indic digits, Arabic ي and ك, and سهشنبه written with a space, a zero-width non-joiner, or neither are all accepted.
It throws FormatException for text that does not match and ArgumentError for a pattern that cannot identify a date; tryParseFormat returns null for the first.
Date arithmetic
final date = PersianDate(1403, 6, 31);
print(date.addDays(1)); // 1403/07/01
print(date.addMonths(1)); // 1403/07/30, the day is clamped
print(PersianDate(1403, 12, 30).addYears(1)); // 1404/12/29
print(PersianDate(1404, 1, 1).differenceInDays(PersianDate(1403, 1, 1))); // 366
print(PersianDate(1405, 8, 5).differenceInMonths(PersianDate(1405, 2, 20))); // 5
print(PersianDate(1400, 12, 29).differenceInYears(PersianDate(1370, 12, 30))); // 30
addMonths and addYears clamp the day to the length of the target month.
This differs from DateTime, which rolls over into the next month.
Results are date-only and throw ArgumentError outside the supported range.
differenceInMonths and differenceInYears count whole months and years the same way, so date.addMonths(n).differenceInMonths(date) is always n.
Someone born on 1370/12/30 is 30 on 1400/12/29, the last day of a year without 30 Esfand.
Temporal's persian calendar counts that as 29 years, 11 months, and 29 days, because the day of the month is still smaller.
Comparing dates
final last = PersianDate(1399, 12, 30);
final first = PersianDate(1400, 1, 1);
print(last < first); // true
print(last.isBefore(first)); // true
print(first == PersianDate.fromGregorian(2021, 3, 21)); // true
print(first.isSameMonth(PersianDate(1400, 1, 31))); // true
Equality and ordering use the day only; the time of day is ignored.
Lists of PersianDate sort with list.sort().
Relative time
final today = PersianDate(1405, 8, 5);
print(PersianDate(1405, 8, 4).relativeTo(today)); // دیروز
print(PersianDate(1405, 8, 2).relativeTo(today)); // 3 روز پیش
print(PersianDate(1405, 9, 5).relativeTo(today)); // ماه آینده
print(PersianDate(1403, 8, 5).relativeTo(today, persianDigits: true)); // ۲ سال پیش
print(PersianDate(1405, 8, 4).relativeTo(today, numeric: true)); // 1 روز پیش
print(PersianDate.withTime(1405, 8, 5, 9, 30)
.relativeTo(PersianDate.withTime(1405, 8, 5, 12, 0))); // 2 ساعت پیش
relativeTo counts calendar days: days up to a week, weeks while less than a whole month has passed, then months and years counted like differenceInMonths.
When both dates have a time of day and are less than a day apart, it uses hours and minutes instead, and اکنون under a minute.
fromNow() is relativeTo(PersianDate.now()).
The words are the CLDR ones that Intl.RelativeTimeFormat('fa') uses in browsers; numeric: true always writes a number.
Leap years, months, and seasons
final date = PersianDate(1403, 7, 1);
print(date.isLeapYear); // true
print(date.monthLength); // 30
print(date.dayOfYear); // 187
print(date.quarter); // 3
print(date.seasonName); // پاییز
print(date.lastDayOfMonth); // 1403/07/30
print(date.lastDayOfYear); // 1403/12/30
Moment of Nowruz
final moment = PersianDate.nowruzMoment(1405);
print(moment); // 2026-03-20 14:45:55.823Z
print(moment.add(const Duration(hours: 3, minutes: 30))); // 18:15:55 in Iran
nowruzMoment gives the March equinox that begins a Jalali year, the moment of سال تحویل, in UTC; add 3 hours and 30 minutes for Iran Standard Time.
It is calculated from the VSOP87 theory of the Earth's orbit and the observed rotation of the Earth (Delta T, 1974 to 2026).
For 2003 to 2026 every result is within 4 seconds of the moment the University of Tehran announced, and from 1981 within 3 seconds of NASA JPL's DE440 ephemeris; years far from today are less certain, because the Earth's rotation cannot be predicted.
The year begins on the day of the equinox when it comes before noon in Iran, and on the next day otherwise; from 1200 to 1699 that rule gives exactly the package's 1 Farvardin.
Persian names
final date = PersianDate.fromGregorian(1979, 2, 11);
print(date.monthName); // بهمن
print(date.monthNameLatin); // Bahman
print(date.monthNameDari); // دلو, the Dari name used in Afghanistan
print(date.weekday); // 7 (DateTime.sunday)
print(date.weekdayPersian); // 2, Sunday is the second day of the Persian week
print(date.weekdayName); // یکشنبه
print(date.weekdayNameShort); // ی
weekday follows DateTime.weekday (1 is Monday, 7 is Sunday), so it compares with DateTime.friday and friends.
weekdayPersian counts from Saturday, which is what a Persian calendar grid needs.
The static weekday name lists are indexed by weekday - 1.
Persian digits
final date = PersianDate(1403, 1, 4);
print(date.toString(persianDigits: true)); // ۱۴۰۳/۰۱/۰۴
print(date.format('d MMMM yyyy', persianDigits: true)); // ۴ فروردین ۱۴۰۳
print(toPersianDigits('12:30')); // ۱۲:۳۰
print(toLatinDigits('۱۴۰۳')); // 1403
Persian words
print(PersianDate(1403, 5, 24).format('EEEE dw MMMM yw'));
// چهارشنبه بیست و چهارم مرداد هزار و چهارصد و سه
print(toPersianWords(1403)); // هزار و چهارصد و سه
print(toPersianWords(30, ordinal: true)); // سیام
The dw token writes the day as an ordinal and yw writes the year, as formal letters and contracts do.
toPersianWords works for any int: a leading thousand is هزار rather than یک هزار, ordinals are یکم, سوم, and سیام, and negative numbers start with منفی.
toString
final date = PersianDate.fromGregorianString('2018-07-07T21:43:44.909Z');
print(date.toString()); // 1397/04/16
print(date.toString(showTime: true)); // 1397/04/16 21:43:44
print(date.toString(showTime: true, showSecond: false)); // 1397/04/16 21:43
print(date.toString(showDate: false, showTime: true)); // 21:43:44
The flags are showDate, showTime, showHour, showMinute, showSecond, and persianDigits.
Every part is zero-padded to two digits.
Hiding everything throws ArgumentError.
format
final date = PersianDate.withTime(1400, 1, 1, 14, 5);
print(date.format('d MMMM yyyy')); // 1 فروردین 1400
print(date.format('EEEE yyyy/MM/dd')); // یکشنبه 1400/01/01
print(date.format('h:mm a')); // 2:05 ب.ظ
print(date.format('h:mm B')); // 2:05 عصر
print(date.format("'ساعت' HH:mm")); // ساعت 14:05
| Token | Output |
|---|---|
yyyy |
year, at least four digits |
yy |
last two digits of the year |
yw |
year in Persian words |
MMMM |
Persian month name |
MM |
month, two digits |
M |
month |
dd |
day, two digits |
d |
day |
dw |
ordinal day in Persian words |
EEEE |
Persian weekday name |
E |
one-letter Persian weekday name |
D |
day of the year |
Q |
quarter, 1 to 4 |
HH |
hour (0 to 23), two digits |
H |
hour (0 to 23) |
hh |
hour (1 to 12), two digits |
h |
hour (1 to 12) |
mm |
minute, two digits |
m |
minute |
ss |
second, two digits |
s |
second |
a |
ق.ظ before noon, ب.ظ from noon |
B |
part of the day: صبح, ظهر, عصر, شب |
BBBB |
the same, بعدازظهر instead of ظهر |
B and dayPeriod follow CLDR's hours for Persian, the same words browsers write with Intl.DateTimeFormat('fa', {dayPeriod: 'short'}): نیمهشب from 0:00, بامداد from 1:00, صبح from 4:00, ظهر from 12:00, عصر from 13:00, and شب from 19:00.
BBBB matches dayPeriod: 'long'.
The longest token wins, and every other character is copied as is.
Put literal text in single quotes, and write two single quotes for one: format("d 'o''clock'") gives 1 o'clock.
Time tokens throw StateError on a date-only instance.
Shahanshahi years
final date = PersianDate(1405, 1, 4);
print(date.shahanshahiYear); // 2585
print(date.format('d MMMM yyyy', era: JalaliEra.shahanshahi)); // 4 فروردین 2585
print(date.toString(era: JalaliEra.shahanshahi)); // 2585/01/04
print(PersianDate.parse('2585/01/04', era: JalaliEra.shahanshahi)); // 1405/01/04
The Shahanshahi (شاهنشاهی, Imperial) numbering is the Solar Hijri year plus 1180.
Pass era: JalaliEra.shahanshahi to format, toString, parse, or tryParse to write or read years that way.
Only the year tokens yyyy and yy change; months, days, and names are the same in both eras.
year always holds the Solar Hijri year, and toJson always writes it.
No era name is printed; add one as quoted text, for example format("yyyy 'شاهنشاهی'", era: JalaliEra.shahanshahi).
parse with the Shahanshahi era accepts years 1119 to 4357 and throws FormatException outside them.
JSON
final date = PersianDate(1403, 1, 4);
print(jsonEncode({'date': date})); // {"date":"1403-01-04"}
print(PersianDate.fromJson('1403-01-04')); // 1403/01/04
toJson writes yyyy-MM-dd and drops the time of day; store toDateTime().toIso8601String() when the time matters.
json_serializable uses PersianDate.fromJson and toJson for PersianDate fields without any annotation.
Hijri calendar
final ashura = PersianDate(1405, 4, 4).toHijri();
print(ashura); // 10 محرم 1448
print(ashura.format('EEEE d MMMM yyyy')); // پنجشنبه 10 محرم 1448
print(HijriDate(1447, 10, 1).toPersianDate()); // 1405/01/01
print(DateTime(2026, 6, 25).toHijriDate().format('yyyy/MM/dd')); // 1448/01/10
print(HijriDate(1447, 12, 1).monthLength); // 29
HijriDate is the lunar Hijri (قمری) date as Iran's official calendar counts it.
From Muharram 1265 (1848) to Shawwal 1448 (Jalali 1405/12), every month starts on the day of the official record: the calendars of the University of Tehran's Calendar Center from 1423 (2002) on, and newspapers and other historical sources before that, as collected by qamari.
Where the crescent was seen on a different day than the calendar had predicted, the day it was seen counts.
Later months start the day after the evening their new crescent can be seen in Iran, calculated from the positions of the Sun and the Moon with Yallop's visibility criterion at eight Iranian cities.
Against the 259 months published from 1427 to 1448, that calculation alone gets 242 right and is one day off for 17.
What it cannot do:
-
A month officially starts when the crescent is actually seen, which is announced only days before. The calculation can be one day off from that announcement. Pass the announced start to an
IranianHijriof your own:final announced = IranianHijri( // 1 Muharram 1448 announced for 1405/03/27. monthStarts: {(1448, 1): PersianDate(1405, 3, 27)}, ); print(announced.fromPersianDate(PersianDate(1405, 4, 5))); // 10 محرم 1448 -
Only Iran's calendar is implemented; Umm al-Qura and the tabular Islamic calendar are not.
-
After the official record the dates are a calculation; expect about one month in fifteen to be a day off until the next calendar is published.
| Member | Description |
|---|---|
HijriDate(int year, int month, int day) |
From Hijri values; throws ArgumentError for a day that does not exist |
HijriDate.fromPersianDate(PersianDate), PersianDate.toHijri() |
From a Jalali date |
HijriDate.fromDateTime(DateTime), DateTime.toHijriDate() |
From the calendar day of a DateTime |
year, month, day |
Hijri year, month (1 is Muharram), and day |
monthName, monthNames |
محرم to ذوالحجه |
monthLength |
29 or 30 |
weekday, weekdayName, weekdayNameShort |
ISO weekday and Persian names, like PersianDate |
toPersianDate(), toDateTime() |
Jalali date, and local midnight of the day |
==, compareTo, <, <=, >, >=, isBefore, isAfter |
By year, month, and day |
format(String pattern, {persianDigits}) |
Tokens yyyy, yy, yw, MMMM, MM, M, dd, dw, d, EEEE, E, quoting like PersianDate.format |
toString() |
day monthName year, for example 10 محرم 1448 |
HijriDate.min, HijriDate.max |
The Hijri dates of PersianDate.min and PersianDate.max |
IranianHijri.official |
The calendar every HijriDate conversion uses |
IranianHijri({monthStarts}) |
A calendar with announced month starts; fromPersianDate, fromDateTime, date, monthStart, monthLength |
A HijriDate created by a calendar with announced starts keeps that calendar's day in toPersianDate, toDateTime, and weekday.
Equality and ordering compare year, month, and day only.
Ranges
final range = PersianDateRange(PersianDate(1403, 1, 1), PersianDate(1403, 1, 13));
print(range.length); // 13
print(range.contains(PersianDate(1403, 1, 4))); // true
for (final day in range.days) {
print(day.format('E d'));
}
print(PersianDate(1403, 12, 29).daysUntil(PersianDate(1404, 1, 1)).length); // 3
Building a date picker
For a ready-made Flutter date picker and range picker, use jalali_calendar_picker. This package has no widgets, but it has the calendar logic a picker of your own needs. A month grid with Saturday in the first column:
List<PersianDate?> monthCells(int year, int month) {
final first = PersianDate(year, month, 1);
return [
for (var i = 1; i < first.weekdayPersian; i++) null, // leading blanks
...first.daysUntil(first.lastDayOfMonth),
];
}
final header = [
for (var i = 0; i < 7; i++) PersianDate(1403, 1, 4).addDays(i).weekdayNameShort,
]; // ش ی د س چ پ ج, 1403/01/04 is a Saturday
Other building blocks:
- Navigation:
addMonths(1),addMonths(-1),addYears(1); the day is clamped, so moving from 1403/06/31 lands on 1403/07/30. - Selection limits:
PersianDate.min,PersianDate.max,isBefore,isAfter, or aPersianDateRangeandcontains. - Highlights:
isToday,isWeekend(Friday),isSameDayfor the selected day. - Range selection:
PersianDateRangewithoverlapsanddays. - Text input:
PersianDate.tryParseaccepts Persian digits from a Persian keyboard. - Labels:
monthName,weekdayNameShort,format('MMMM yyyy', persianDigits: true). - Right-to-left layout is the widget's job; in Flutter, wrap the grid in
Directionality(textDirection: TextDirection.rtl, ...).
Related packages
These live in this repository under packages/ and are released separately.
- iranian_holidays: Iranian official holidays, calculated offline including the lunar Hijri ones, or from an online source you choose (holidayapi.ir, Calendarific, or Google Calendar).
- jalali_calendar_picker: Material 3 date picker and range picker for Flutter, with Saturday-first weeks, Persian digits, and right-to-left layout.
- iranian-calendar (npm): the lunar Hijri calendar and holidays of this package and iranian_holidays for JavaScript and TypeScript, giving the same days.
Notes for code generators and AI assistants
- Import only
package:jalali_date/jalali_date.dart; never import fromsrc/. PersianDateis immutable; there are no setters. UsecopyWith,addDays,addMonths, or a constructor.PersianDate(y, m, d)takes Jalali values; usePersianDate.fromGregorian(y, m, d)for Gregorian values.- Months are 1 to 12, never 0-based.
weekdayis ISO (Monday is 1); useweekdayPersianfor Saturday-first numbering. The name lists are indexed byweekday - 1.==ignores the time of day; comparetoDateTime()when the moment matters.- Check
hasTimebefore readinghour,minute,second, or using time tokens. addMonthsclamps; it never rolls over likeDateTime.- Esfand has 30 days only in leap years; use
monthLengthinstead of hard-coding 29. parseandparseFormatalready accept Persian digits; usepersianDigits: trueortoPersianDigitsfor output.- To read text such as
۴ فروردین ۱۴۰۳, usePersianDate.parseFormat(text, 'd MMMM yyyy'), not a hand-written regex. - For ages and terms use
differenceInMonthsordifferenceInYears; dividingdifferenceInDaysby 30 or 365 gives wrong answers. - For text such as
3 روز پیشorدیروز, userelativeToorfromNow. - For Iranian holidays use the iranian_holidays package (
OfflineHolidaySource, or an online source); never hard-code holiday dates, because the lunar Hijri ones move every year.
Supported range
Jalali -61/01/01 to 3177/10/11, which is Gregorian 560-03-20 to 3798-12-31.
In the Shahanshahi numbering that is 1119/01/01 to 4357/10/11, and in the lunar Hijri calendar -64/10/05 to 3274/12/10.
Every constructor, parse, and the arithmetic methods throw outside that range.
Leap years
Leap years follow jalaali-js, and from 1200 to 1699 they agree with the official rule: the year begins on the day of the March equinox when it comes before noon in Iran, and on the next day otherwise (see nowruzMoment).
ICU, which Intl in browsers and Node and Java's ICU4J use, and Python's persiantools give the same days from 1300 to 1500 but differ from 1502 on (Gregorian 2123), because they use a 33-year rule with a table of corrections.
For 1502 and 1503 the difference is whether 1503 begins on 2124-03-20 or 2124-03-21; the equinox comes about 4 minutes after noon, closer than the Earth's rotation can be predicted a century ahead, so the official calendar for that year is not settled.
The old 2820-year rule, still found in some code, is wrong as early as 1403 and 1404.
Migrating
From 1.2.x
dw,yw,B, andBBBBare nowformattokens; quote them to keep them literal:format("'dw'").
From 1.0.x
formattreats a single quote as the start of literal text, anda,E,D,Q,h, andhhare now tokens. Quote them to keep them literal:format("'Q' Q").
From 0.1.x
- Dart 3 is required, and the package no longer depends on Flutter.
toString()pads the date, so1396/1/1is now1396/01/01.year,month, anddayare final; create a newPersianDateinstead of changing them.PersianDate(year, month, day)throwsArgumentErrorfor invalid dates.- The
ldfield, themonthsLengthandkabiseMonthsLengthtables, and the publicgreToJal()are gone. UseisLeapYearandmonthLengthinstead. - Conversion results changed for some dates, because 0.1.x was wrong around Gregorian century years and Jalali leap cycles.
Credits
- The conversion algorithm is a port of jalaali-js v1.2.8, MIT, Copyright (c) 2020 Behrang Norouzinia. See THIRD_PARTY_NOTICES.md.
- The first version was based on Java code by breceivemail.
- Time-part flags on
toStringby AliRn76.
License
Apache-2.0. See LICENSE.
Libraries
- jalali_date
- Persian (Jalali, Shamsi, Khorshidi) calendar: conversion between Gregorian and Jalali dates, leap years, month lengths, and formatting with Persian month and weekday names.