Upgrade guide

Changes that can affect your code before 1.0, with before and after examples and the steps to migrate, plus the highlights of the changelog for the 0.x series.

On this page
  1. From 0.1.x to 0.2.0
  2. From the early pre-release builds (before 0.1.0): namespaced helpers
    1. 1. Global helper functions are no longer defined by default
    2. 2. Other changes to be aware of
  3. Changelog highlights for the 0.x series
    1. Behaviour that changed
    2. Added
    3. Fixed

Before 1.0.0, a minor release may contain breaking changes. Each one is listed here with the steps to migrate. The policy is on API stability. This page starts with the newest version. The last section covers the early pre-release builds, from before 0.1.0.

From 0.1.x to 0.2.0#

Most applications need no change. Check the items below. Items 9 to 12 change what the library returns for some inputs.

  1. ext-mbstring is no longer required. composer.json used to ask for the extension. Now it needs only PHP. There is nothing to do. If you added ext-mbstring to your own composer.json only for this package, you can remove it.
  2. Float formatting. Format::withSeparator() and format_number() now print a float as the shortest decimal that reads back as the same float, cut to 15 significant digits. Binary noise is gone. Strings are unchanged and stay exact. Pass a string when you need more digits.
CallBeforeAfter
withSeparator(-1234567.891)-۱٬۲۳۴٬۵۶۷٫۸۹۱۰۰۰۰۰۰۰۶۱۴۶۷-۱٬۲۳۴٬۵۶۷٫۸۹۱
withSeparator(0.1 + 0.2)۰٫۳۰٫۳ (unchanged)
withSeparator(-0.0)۰۰

If you format the result of float arithmetic, round first (round($x, 2)) or pass a string.

  1. NumberToWords::fromWords() is strict. It used to add the words up in any order.
InputBeforeAfter
دو صد102InvalidNumberException (invalid_number_words)
بیست یک21InvalidNumberException
صد و بیست و120InvalidNumberException
پنج و بیست25InvalidNumberException
سی و پنج3535

Write و between parts and keep the order hundreds, tens, units. Everything convert() returns still parses.

  1. Slugify::make() throws RtlyKitException (invalid_argument, argument = text) for text that is not valid UTF-8. Before, only the separator was checked. Catch RtlyKitThrowable or clean the input first.
  2. Hijri::format() and Hebrew::format() reject a pattern longer than 256 bytes with InvalidDateException (input_too_long, argument = format, limit = 256). Jalali::format() already did. The limit is in Hijri::MAX_FORMAT_LENGTH and Hebrew::MAX_FORMAT_LENGTH.
  3. A trailing backslash in a format pattern is dropped in all three calendars. format('Y\') returns just the year.
  4. IranHolidays::all(), allTitles() and allFixed() work for every Jalali year from -620 to 9377. Before, years before the Hijri epoch and the last day of 9377 threw invalid_date. Where Islamic holidays cannot be derived, only the fixed holidays are returned. For a year outside the range they throw InvalidDateException (date_out_of_range, context year, min, max).
  5. Error messages that repeat your input cut it to 40 characters and stay valid UTF-8. If you match on message text, match on getErrorCode() instead.
  6. Official holiday dates for 1380 to 1405. In these Jalali years the static methods (IranHolidays, is_iran_holiday()) now return the published dates instead of the Umm al-Qura estimate. Other years are unchanged.
DayBefore (estimate)After (official)
1404/01/10عید فطرno holiday
1404/01/11تعطیل عید فطرعید فطر
1404/01/01جشن نوروز + شهادت امام علیجشن نوروز
1404/12/29ملی شدن صنعت نفت + عید فطرملی شدن صنعت نفت
1405/01/01جشن نوروز + تعطیل عید فطرجشن نوروز + عید فطر

In official years only the titles in the official table appear. Years 1380 to 1393 and 1395 are reported: the dates come from one source, and the list of days may be incomplete. To get the old behaviour for all years, use HolidayCalendar::default()->withOfficialData(false). To see where a year comes from, use IranHolidays::sourceOf($year). More in Holiday calendar.

  1. High-latitude prayer times. PrayerTimes has a new HighLatitudeRule. The default is AngleBased. It acts only where a Fajr or Isha time would be missing, or farther from sunrise or sunset than the rule allows. Below about 44 degrees north nothing changes. North of about 44 to 46 degrees, around the June solstice, some values differ. A time that was null now has a value.
Place and day (MWL)Before (same as None)After (default)
Stockholm, 2026-06-21Fajr null, Isha nullFajr 01:54, Isha 23:40
Munich, 2026-06-21Fajr 01:50, Isha 00:12Fajr 02:51, Isha 23:32

To get the old output, call $prayer->withHighLatitudeRule(HighLatitudeRule::None). nextPrayer() now also finds a prayer that falls after midnight. See High latitudes.

  1. Mobile validation. The shape check is the same, so no input changes validity. Two things are new. Every result has an allocated detail and Mobile::isAllocated() returns it as a bool. The operator table follows the numbering plan more closely: the Shatel Mobile key 998 is narrowed to 09981 and 09982, the Aptel key is widened from 99910 to 9991, and 0923, 0931, 0932 and 0934 are added.
NumberOperator beforeOperator after
09981234567شاتل موبایلشاتل موبایل
09983112345شاتل موبایلnull (allocated, holder not confirmed)
09231234567nullرایتل
09321234567nullتالیا

If you compare the whole details() array, expect the extra key allocated.

  1. Arabic number words. convert($n, 'ar') without options gives the same text as before for every number below 109. It now also accepts numbers up to 1027, where it used to throw number_too_large. New options and ordinals are on the Arabic number words page.
  2. Platforms. PHP 8.2 to 8.5, Laravel 11, 12 and 13, and Carbon 3 are supported. See API stability.

From the early pre-release builds (before 0.1.0): namespaced helpers#

1. Global helper functions are no longer defined by default#

Breaking change. Earlier pre-release builds defined jdate(), to_persian(), is_national_code() and the other helpers as global functions as soon as Composer loaded the package. That could clash with your code or another package, so it was removed. The helpers now live in the RtlyKit namespace and nothing is defined globally.

The 27 helpers: jdate, hdate, hebrew_date, to_persian_digits, to_english_digits, to_persian, to_english, is_national_code, is_sheba, is_bank_card, is_mobile, is_postal_code, is_vehicle_plate, validate_national_code, validate_sheba, validate_bank_card, validate_mobile, validate_postal_code, validate_vehicle_plate, number_to_words, normalize_text, contains_rtl, text_direction, is_iran_holiday, prayer_times, format_number and ordinal.

Option A: import what you use (recommended).

use function RtlyKit\jdate;
use function RtlyKit\is_national_code;

echo jdate('2026-03-21')->format('Y/m/d');   // 1405/01/01

Or call the fully qualified name: \RtlyKit\jdate('2026-03-21').

Option B: bring back the old short global names. Call Globals::register() once, for example in your bootstrap file:

\RtlyKit\Globals::register();

echo jdate('2026-03-21')->format('Y/m/d');   // 1405/01/01, works as before

register() defines only the names that are still free. It never replaces an existing function and never throws. It returns the names it skipped because something else already uses them. You can call it more than once:

$skipped = \RtlyKit\Globals::register();
if ($skipped !== []) {
    error_log('RTLY-Kit globals skipped: '.implode(', ', $skipped));   // use \RtlyKit\name() for those
}

The Laravel service provider does not register globals either. In Blade, write {{ \RtlyKit\jdate($post->created_at)->format('j F Y') }}, or call Globals::register() from your AppServiceProvider::register(). More on Helpers and globals.

2. Other changes to be aware of#

ChangeWhat to do
Validators take any value. All six implement RtlyKit\Contracts\Validator (validate(mixed): Result, isValid(mixed): bool). Non-string input (null, arrays, objects) returns an invalid Result with error invalid_type. Input over 4096 bytes returns input_too_long. They never throw for bad input.Remove defensive try blocks and type checks around validators. See Validators overview.
Exceptions carry codes. getErrorCode() (an ErrorCode enum) and getContext() exist on every library exception.Nothing to change. Match on the code and not on the message text. See Error handling.
Slugify::make() throws RtlyKitException (input_too_long or invalid_argument) for an invalid UTF-8 separator or one longer than 64 bytes.This matters only if you pass a separator that users control. Catch RtlyKitThrowable.
Calendars share a contract. Jalali, Hijri and Hebrew implement RtlyKit\Contracts\CalendarDate. Comparisons and diffIn*() accept a CalendarDate or any DateTimeInterface, make() accepts a CalendarDate, and equals(), isBefore(), isAfter() are new aliases.Existing calls keep working. See Convert and compare.
Reference data moved to resources/data/*.php.If you read the old table classes directly (they were never public API), switch to public classes such as Hijri, Sheba::getBankName() or NationalCode::getLocation().
Makkah Isha in Ramadan changed. PrayerTimes::METHOD_MAKKAH now returns Isha as Maghrib + 120 minutes during Ramadan (Umm al-Qura practice) and + 90 minutes otherwise. The previous build always used 90.Ramadan Isha moves 30 minutes later. This is a bug fix. Update any stored expected values. See the example below.

A check of the Makkah behaviour (2026-03-01 is in Ramadan, 2026-03-21 is not):

use RtlyKit\Prayer\PrayerTimes;

$mecca = PrayerTimes::forCity('mecca', PrayerTimes::METHOD_MAKKAH);
foreach (['2026-03-01', '2026-03-21'] as $d) {
    $t = $mecca->getTimes(new DateTimeImmutable($d));
    echo $d, ' maghrib ', $t['maghrib'], ' isha ', $t['isha'], "\n";
}
// 2026-03-01 maghrib 18:25 isha 20:25
// 2026-03-21 maghrib 18:32 isha 20:02

Changelog highlights for the 0.x series#

These are the points of the changelog that users see. The repository's CHANGELOG.md has the full list.

Behaviour that changed#

  • Out-of-range calendar input throws. Years, timestamps and huge add*() and sub*() amounts outside the supported ranges raise InvalidDateException and not a TypeError or a wrapped-around date. Ranges: Jalali -620 to 9377, Hijri 1 to 9665, Hebrew 3762 to 13759 (Limits).
  • make() string rules are fixed. A year below 1700 (Hebrew: 3000 or more) is read in the calendar of the class. Everything else is read as Gregorian. Blank strings throw, and null is now. A Gregorian ISO string with a year below 1700 is read as Jalali or Hijri, so pass a DateTimeImmutable for such dates (FAQ).
  • Holidays. Official dates for Jalali 1380 to 1405 (see above). For other years, Islamic holidays are reported only for AH 1300 to 1500, and a year outside the Jalali range throws InvalidDateException.
  • Number input. NumberToWords::convert() and Format::ordinal() accept whole floats (3.0) and throw InvalidNumberException for fractional floats, NaN and INF.
  • Input caps (security): strings over 4096 bytes are rejected by NumberToWords and Format, and Format::withSeparator() rejects numbers over 1000 characters. They throw InvalidNumberException (input_too_long).
  • Data tables keep confirmed entries. NationalCode::getLocation() covers 547 prefixes and returns null for the rest. The bank BIN (39), Sheba code (38) and operator tables keep only entries confirmed by two sources. A name that was returned before may now be null. BankCard rejects numbers made of one repeated digit.
  • Prayer-time core rewritten from standard astronomical equations. The difference from the earlier implementation was at most 2 minutes on evening events (mean 0.5 minute or less) across the comparison grid. Daylight saving is applied per event, and the $elevation constructor argument is new.

Added#

  • A pure-PHP Hebrew calendar (Hebrew) with no ext-calendar needed. The Hijri calendar was rebuilt around the Umm al-Qura table for AH 1300 to 1500, with a HijriVariant::Tabular fallback.
  • Structured validation: Result (isValid(), errors(), details()) and validate_*() helpers with stable error codes.
  • NumberToWords::fromWords(), numbers up to 21 digits, negatives, and Arabic number words (number_to_words($n, 'ar')).
  • Normalizer::fixHalfSpace() and clean(), Detector::isRtlLocale() and isHebrew(), IranHolidays::getTitles(), isBusinessDay() and nextBusinessDay(), and PrayerTimes::nextPrayer().
  • Laravel: the Jalali facade and the factory in the container, auto-discovery, six validation rules with fa, en and ar messages, and JalaliCast.
  • Carbon macros toJalali, jformat, createFromJalali, toHijri, toHebrew, createFromHijri, createFromHebrew.
  • The exception hierarchy under RtlyKitThrowable, the ErrorCode enum, Globals::register(), and the CalendarDate and Validator contracts.

Fixed#

  • Makkah-method Isha in Ramadan (described above).
  • Format::ordinal() for 30 and 23. Format::withSeparator() accepts Persian and Arabic digits without rounding. Mobile accepts the 0098, 98 and bare 9xxxxxxxxx forms. Sheba accepts the bare 24-digit form.
  • Prayer-time handling of time zones around daylight saving, and errors for an unknown method or Asr factor.
  • The duplicate hdate() helper declaration.
Note. While the library is below 1.0.0, pin a minor range (for example ~0.2.0), read this page before each minor upgrade, and run your own tests against any date and prayer-time outputs that you store.