Skip to content

Latest commit

 

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bard Framework Jalali Date

Maven Central Javadocs Quality Gate Status License

A Jalali (Shamsi / Persian) calendar for Java, built as a mirror of java.time.

If you know LocalDate, you already know LocalDateJalali — the same factory methods, the same plus/minus/with family, the same Temporal interfaces, the same formatter model. No java.util.Calendar, no third-party date type leaking into your domain, no new mental model.

LocalDateJalali today = LocalDateJalali.now();          // 1404-06-17
LocalDate       gregorian = today.toLocalDate();        // 2025-09-08

Only runtime dependency is slf4j-api. Nothing else from Bard Framework is required — this library is usable entirely on its own.

Why this library exists

Most Jalali libraries for Java give you a converter and a formatter: a class with getYear(), toGregorian(), maybe a format(pattern). That is enough for printing a date, and not enough for anything else. The moment your domain needs to compute with dates — add three months to a subscription, take the last day of the billing period, sort a list, measure a duration, feed a value into an API that expects a Temporal — you are back to writing conversion code by hand, and you have introduced a second date type your codebase must be careful about.

This library takes the other route: it implements the JSR-310 contracts. LocalDateJalali is a Temporal, a TemporalAdjuster, a ChronoLocalDate and Comparable, and ChronologyJalali is a real Chronology. Everything built on the java.time SPI therefore works, unchanged:

ChronoUnit.MONTHS.between(from, to);
date.with(TemporalAdjusters.lastDayOfMonth());
dates.stream().sorted().toList();
Duration.between(startJalali.atStartOfDay(), endJalali.atStartOfDay());

The API surface is deliberately a mirror, not an invention. If you have to look up how to do something, the answer is "the same way you would with LocalDate" — the factory names, the plus/minus/with family, the formatter model and even the ISO constants are the same. Nothing new to learn, nothing extra to remember at a call site.

Install

<dependency>
    <groupId>org.bardframework</groupId>
    <artifactId>jalali-date</artifactId>
    <version>5.6.7</version>
</dependency>

Java 21 or newer.

Quick start

LocalDateJalali today = LocalDateJalali.now();     // 1404-06-17
LocalDate       iso   = today.toLocalDate();       // 2025-09-08

That is the whole idea: a Jalali date is a first-class java.time value. Everything below follows from that.

A tour

Creating a date

LocalDateJalali.now();                              // from the system clock
LocalDateJalali.now(ZoneId.of("Asia/Tehran"));      // in a specific zone
LocalDateJalali.of(1404, 1, 1);                     // 1404-01-01, Nowruz
LocalDateJalali.of(1370, MonthJalali.MEHR, 12);     // 1370-07-12
LocalDateJalali.ofYearDay(1404, 172);               // 1404-06-17
LocalDateJalali.ofEpochDay(20339);                  // 1404-06-17

LocalDateJalali.of("1404-06-17");                   // lenient: keeps the digits, ignores separators
LocalDateJalali.parse("1404-06-17");                // strict ISO layout
LocalDateJalali.parse("1404/06/17", DateTimeFormatterJalali.ofPattern("yyyy/MM/dd"));

LocalDateJalali.of(LocalDate.of(2025, 3, 21));      // from a Gregorian date → 1404-01-01

Reading it

LocalDateJalali d = LocalDateJalali.of(1404, 6, 17);

d.getYear();            // 1404
d.getMonthValue();      // 6
d.getMonth();           // SHAHRIVAR
d.getDayOfMonth();      // 17
d.getDayOfYear();       // 172
d.getDayOfWeek();       // MONDAY  (java.time.DayOfWeek)
d.isLeapYear();         // false
d.lengthOfMonth();      // 31
d.lengthOfYear();       // 365
d.get(ChronoField.YEAR);

Moving around

d.plusDays(21);                       // 1404-07-08
d.plusMonths(1);                      // 1404-07-17
d.plusYears(1);                       // 1405-06-17
d.minusWeeks(2);                      // 1404-06-03
d.plus(Period.ofMonths(3));           // 1404-09-17

d.withYear(1405);
d.withMonth(12);
d.withDayOfMonth(1);

Because it is a real Temporal, the standard adjusters work — and they respect the Jalali month lengths:

d.with(TemporalAdjusters.firstDayOfMonth());       // 1404-06-01
d.with(TemporalAdjusters.lastDayOfMonth());        // 1404-06-31   ← Shahrivar has 31 days
d.with(TemporalAdjusters.firstDayOfNextMonth());   // 1404-07-01
d.with(TemporalAdjusters.lastDayOfYear());         // 1404-12-29
d.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));  // 1404-06-21

Comparing and measuring

d.isBefore(LocalDateJalali.of(1405, 1, 1));   // true
d.isAfter(LocalDateJalali.of(1403, 1, 1));    // true
d.isEqual(LocalDateJalali.of(1404, 6, 17));   // true
d.compareTo(other);                            // Comparable, so sorting just works

ChronoUnit.DAYS.between(birthday, today);      // 12758
d.until(LocalDateJalali.of(1405, 8, 20));                     // P1Y2M3D
d.until(LocalDateJalali.of(1405, 8, 20), ChronoUnit.MONTHS);  // 14

Time, offsets and zones

d.atStartOfDay();                                         // 1404-06-17T00:00
LocalDateTimeJalali.of(d, LocalTime.of(9, 30));           // 1404-06-17T09:30
ZonedDateTimeJalali.of(dateTime, ZoneId.of("Asia/Tehran"));
// 1404-06-17T09:30+03:30[Asia/Tehran]

ZonedDateTimeJalali.now(ZoneId.of("Asia/Tehran"));
OffsetDateTimeJalali.now();
LocalDateTimeJalali.of(LocalDateTime.now()).toLocalDateTime();   // round-trips exactly

Year, month and year-month

YearJalali.of(1403).isLeap();               // true
YearMonthJalali.of(1404, 12).lengthOfMonth();  // 29
YearMonthJalali.of(1404, 12).atEndOfMonth();   // 1404-12-29
MonthJalali.SHAHRIVAR.length(false);           // 31
MonthJalali.ESFAND.plus(1);                    // FARVARDIN — wraps around the year
MonthJalali.SHAHRIVAR.getDisplayName(TextStyle.FULL, faLocale);   // شهریور

Types

This library java.time counterpart
LocalDateJalali LocalDate
LocalDateTimeJalali LocalDateTime
ZonedDateTimeJalali ZonedDateTime
OffsetDateTimeJalali OffsetDateTime
YearJalali, YearMonthJalali, MonthDayJalali Year, YearMonth, MonthDay
MonthJalali MonthFARVARDINESFAND
ChronologyJalali Chronology — the leap-year rule and the epoch
DateTimeFormatterJalali, DateTimeFormatterBuilderJalali DateTimeFormatter, DateTimeFormatterBuilder
IsoFieldsJalali, TemporalQueriesJalali IsoFields, TemporalQueries
time.zone.*, TextStyle, DecimalStyle, SignStyle the matching java.time support types

LocalDateJalali implements Temporal, TemporalAdjuster, ChronoLocalDate and Comparable, so ChronoUnit, TemporalAdjusters and anything else built on the java.time SPI work unchanged. All types are immutable and thread-safe.

The calendar

Month lengths follow the Jalali rule, and lengthOfMonth(), the validity checks and TemporalAdjusters all agree on it:

Months Length
Farvardin – Shahrivar (1–6) 31
Mehr – Bahman (7–11) 30
Esfand (12) 29, or 30 in a leap year
LocalDateJalali.of(1404, 6, 17).with(TemporalAdjusters.lastDayOfMonth());   // 1404-06-31
LocalDateJalali.of(1404, 12, 1).with(TemporalAdjusters.lastDayOfMonth());   // 1404-12-29
LocalDateJalali.of(1403, 12, 1).with(TemporalAdjusters.lastDayOfMonth());   // 1403-12-30

Two things to know

DateTimeFormatterJalali is a separate type, not a subclass of java.time.format.DateTimeFormatter. So you format by calling the formatter, not the date:

DateTimeFormatterJalali.ofPattern("yyyy/MM/dd").format(date);   // ✔
// date.format(jalaliFormatter)                                 // ✘ won't compile

Parsing works from either direction — parse accepts both formatter types:

LocalDateJalali.parse("1404/06/17", DateTimeFormatterJalali.ofPattern("yyyy/MM/dd"));

Text is Jalali, not Gregorian. parse("1404-06-17") reads the year 1404 of the Jalali calendar, so formatting and parsing round-trip:

String text = DateTimeFormatterJalali.ISO_LOCAL_DATE.format(LocalDateJalali.of(1404, 6, 17));
LocalDateJalali.parse(text).equals(LocalDateJalali.of(1404, 6, 17));   // true

Changed in 6.1 — month lengths. LocalDateJalali.lengthOfMonth() previously returned the Gregorian month lengths (31, 28, 31, 30, …). Anything built on it was wrong: the last day of Shahrivar came back as the 30th instead of the 31st, and TemporalAdjusters.lastDayOfMonth() on Esfand threw DateTimeException. Date construction and lengthOfYear() were always correct, so the bug only surfaced through lengthOfMonth and the adjusters built on it.

Changed in 6.1 — month display names. MonthJalali.getDisplayName(...) built its text with java.time's formatter builder, which resolves month number 6 to the Gregorian June — so SHAHRIVAR.getDisplayName(FULL, fa) returned «ژوئن». It now uses the Jalali text provider and returns «شهریور», matching what the MMMM pattern has always produced.

Changed in 6.1 — parsing. Before that, LocalDateJalali.parse, ZonedDateTimeJalali.parse and OffsetDateTimeJalali.parse (the single-argument overloads) parsed their input with the Gregorian ISO formatter, so parse("1404-06-17") returned 0783-03-27 and a format→parse round trip silently changed the date. LocalDateTimeJalali was always correct. All four now read Jalali text. If you were relying on the old behaviour, parse into LocalDate yourself and convert with LocalDateJalali.of(LocalDate).

Formatting

DateTimeFormatterJalali mirrors DateTimeFormatterofPattern, withLocale, withChronology, and the ISO constants (ISO_LOCAL_DATE, ISO_LOCAL_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_ZONED_DATE_TIME, RFC_1123_DATE_TIME, …). DateTimeFormatterBuilderJalali is there when a pattern is not enough.

LocalDateJalali d = LocalDateJalali.of(1404, 6, 17);
Locale fa = Locale.forLanguageTag("fa");

DateTimeFormatterJalali.ofPattern("yyyy/MM/dd").format(d);                    // 1404/06/17
DateTimeFormatterJalali.ISO_LOCAL_DATE.format(d);                            // 1404-06-17
DateTimeFormatterJalali.ofPattern("d MMM yyyy").withLocale(fa).format(d);     // 17 شهریور 1404
DateTimeFormatterJalali.ofPattern("EEEE d MMMM yyyy").withLocale(fa).format(d);
// دوشنبه 17 شهریور 1404

DateTimeFormatterJalali.ofPattern("yyyy/MM/dd HH:mm")
        .format(LocalDateTimeJalali.of(d, LocalTime.of(9, 30)));             // 1404/06/17 09:30

new DateTimeFormatterBuilderJalali()
        .appendValue(ChronoField.YEAR, 4).appendLiteral('/')
        .appendValue(ChronoField.MONTH_OF_YEAR, 2)
        .toFormatter().format(d);                                            // 1404/06

Names follow the locale — Persian for fa, transliterated Latin elsewhere:

pattern fa en
MMMM شهریور Shahrivar
EEEE دوشنبه DoShanbe

Digits are always ASCII. For Persian-Indic digits (۱۴۰۴), convert the formatted string yourself or supply a DecimalStyle.

Conversion

The calendars are anchored at 0001-01-01 Jalali = 0622-03-22 Gregorian, and every conversion is exact and reversible:

LocalDateJalali.of(LocalDate.now());     // Gregorian → Jalali
date.toLocalDate();                      // Jalali → Gregorian
LocalDateJalali.ofEpochDay(epochDay);
date.toEpochDay();

LocalDateTimeJalali.of(LocalDateTime) and .toLocalDateTime() do the same while preserving the time component.

Using it in a Bard application

You rarely call this library directly in a Bard app — it is wired in for you:

  • bard-form-parent date fields and table headers format through it, so a Persian user sees Jalali dates while the database keeps ISO values.
  • bard-commons common-utils-persian uses it for Persian date helpers.
  • On the client, @bard/angular ships a matching BardDateAdapterJalali for the Material date picker.

Interoperability notes

  • Storage. Keep ISO/Gregorian in the database and convert at the edges. toLocalDate() and of(LocalDate) are exact, so nothing is lost — and your data stays queryable by every other tool.
  • JSON. Serialise as ISO or epoch and convert in the DTO, or register a converter. A Jalali string on the wire is a choice you can make for a UI-facing API, but not one to make by accident.
  • Sorting and indexing work directly: LocalDateJalali is Comparable and its natural order is chronological.
  • java.util.Date / Calendar are not supported and deliberately so — go through java.time (Date.toInstant()LocalDateLocalDateJalali).

Building and testing

mvn clean verify

The suite covers the calendar rules (month lengths, leap years, adjusters), text parsing and formatting, and the month enum — plus a batch test that generates 10,000 random Jalali dates between years 1200 and 1500, converts each to Gregorian and back, and asserts the round trip. That last one is the cheapest way to catch an off-by-one in the leap-year rule.

Contributing

Pull requests are welcome — see CONTRIBUTING.md. Add JUnit tests for your change and run mvn clean test before submitting.

Additional resources

License

Apache License 2.0.

About

Jalali (Shamsi) calenadar implementation in java for parsing, validating, manipulating, converting and formatting persian dates (like java 8 LocalDate(Time))

Topics

Resources

Contributing

Stars

5 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages