Digits and number formatting

Convert between English, Persian and Arabic-Indic digits, format numbers with Persian separators and build Persian ordinals. Every cap and edge case is listed.

On this page
  1. Digits
    1. convert()
  2. Number formatting
    1. What it accepts
    2. Custom separators
    3. Floats and exact values
    4. Size caps
  3. Ordinals

Digits#

RtlyKit\Number\Digits converts between the three digit sets used in Iranian software: English (0-9), Persian (۰-۹, U+06F0 to U+06F9) and Arabic-Indic (٠-٩, U+0660 to U+0669). The class only swaps characters. It never throws, never touches other characters, and does not change decimal or thousands separators.

<?php
require 'vendor/autoload.php';

use RtlyKit\Number\Digits;

echo Digits::toPersian('Tel 0912 / 2024'), "\n";   // Tel ۰۹۱۲ / ۲۰۲۴
echo Digits::toPersian(1234.5), "\n";              // ۱۲۳۴.۵
echo Digits::toArabic(2026), "\n";                 // ٢٠٢٦
echo Digits::toEnglish('۱۲۳ ٤٥٦ abc'), "\n";        // 123 456 abc
echo Digits::arabicToPersian('٠١٢ 345'), "\n";      // ۰۱۲ 345
MethodInputWhat it does
toPersian($value)string, int, floatEnglish digits become Persian. Persian and Arabic-Indic digits already in a string stay as they are
toArabic($value)string, int, floatEnglish digits become Arabic-Indic
toEnglish($value)stringPersian and Arabic-Indic digits both become English
arabicToPersian($value)stringOnly Arabic-Indic digits become Persian. English digits stay
convert($value, $to)stringAny digit set to English first, then to the target

convert()#

convert() turns the text into English digits first and then writes the target set. The target name is not case sensitive. persian, fa and farsi give Persian digits. arabic and ar give Arabic-Indic digits. Anything else, including a typo, gives English.

echo Digits::convert('۱۲۳', 'ar'), "\n";        // ١٢٣
echo Digits::convert('123', 'fa'), "\n";        // ۱۲۳
echo Digits::convert('٣٤٥', 'FARSI'), "\n";     // ۳۴۵
echo Digits::convert('۱۲۳', 'x'), "\n";         // 123
Numbers as input. toPersian() and toArabic() accept int and float and convert them with a plain (string) cast. The decimal separator stays a Latin dot (۱۲۳۴.۵) and there are no thousands separators. For numbers that people read, use Format::withSeparator() below. The helpers to_persian_digits(), to_english_digits(), to_persian() and to_english() wrap these methods. See Helpers and globals.

The validators use Digits::toEnglish() to clean their input, so users can type in any digit set. See Cleaning the input.

Number formatting#

Format::withSeparator() formats a number with thousands separators and Persian digits. By default it uses the Arabic thousands separator ٬ (U+066C) and decimal separator ٫ (U+066B), which are the characters Persian typography uses. It never rounds.

use RtlyKit\Number\Format;

echo Format::withSeparator(1234567), "\n";            // ۱٬۲۳۴٬۵۶۷
echo Format::withSeparator('1,234,567.5'), "\n";      // ۱٬۲۳۴٬۵۶۷٫۵
echo Format::withSeparator('۱٬۲۳۴٫۵'), "\n";          // ۱٬۲۳۴٫۵
echo Format::withSeparator('0001234'), "\n";          // ۱٬۲۳۴
echo Format::withSeparator('+12345'), "\n";           // ۱۲٬۳۴۵
echo Format::withSeparator(1.0), "\n";                // ۱

What it accepts#

  • An int, a finite float, or a numeric string with an optional sign, digits in any of the three sets, and an optional fraction.
  • In strings, these are dropped as grouping marks: ,, ٬ (U+066C), regular spaces and the no-break space. The Arabic decimal separator ٫ (U+066B) is read as the decimal point, and so is a plain ..
  • Leading zeros of the whole part are dropped. A leading + is dropped. A leading - is kept.

Anything that is not a plain decimal after this cleanup throws InvalidNumberException. That includes 'abc', the empty string, scientific notation in a string ('1e5'), NaN and INF. The exception carries a stable error code (invalid_number, non_finite_number, input_too_long) that you read with getErrorCode(). See Error handling.

Custom separators#

The second and third arguments replace the thousands and decimal separators. The digits are still Persian:

echo Format::withSeparator(1234567, ','), "\n";            // ۱,۲۳۴,۵۶۷
echo Format::withSeparator(1234567.5, ',', '.'), "\n";     // ۱,۲۳۴,۵۶۷.۵

For English digits, convert the result back with Digits::toEnglish().

Floats and exact values#

A float is written as the shortest decimal text that reads back as the same float, cut to 15 significant digits. It never uses exponent form and does not depend on php.ini. Negative zero prints as ۰. The 15-digit cut removes binary noise, so 0.1 + 0.2 prints as ۰٫۳. A float holds only about 15 to 17 significant digits anyway. Pass a string when you need more digits, because a string is kept exactly:

echo Format::withSeparator(-1234567.891), "\n";    // -۱٬۲۳۴٬۵۶۷٫۸۹۱
echo Format::withSeparator(0.1), "\n";             // ۰٫۱
echo Format::withSeparator(0.1 + 0.2), "\n";       // ۰٫۳
echo Format::withSeparator(1 / 3), "\n";           // ۰٫۳۳۳۳۳۳۳۳۳۳۳۳۳۳۳ (15 digits)
echo Format::withSeparator(123456789.123456789), "\n";     // ۱۲۳٬۴۵۶٬۷۸۹٫۱۲۳۴۵۷ (float)
echo Format::withSeparator('123456789.123456789'), "\n";   // ۱۲۳٬۴۵۶٬۷۸۹٫۱۲۳۴۵۶۷۸۹ (string, exact)
Good to know. For money, keep amounts as strings or integers (rials) from start to end. Floats cannot store most decimals exactly, as the 1 / 3 and 123456789.123456789 lines above show. The formatter cuts a float to 15 significant digits.

Size caps#

CapApplies toError code
4096 bytesa raw string input (Persian and Arabic digits take two bytes each)input_too_long
1000 charactersthe cleaned number: whole and fraction digits togetherinput_too_long
try {
    Format::withSeparator(str_repeat('9', 1001));
} catch (\RtlyKit\Exceptions\InvalidNumberException $e) {
    echo $e->getMessage(), ' [', $e->getErrorCode()->value, "]\n";
    // Number exceeds the 1000 character limit. [input_too_long]
}

Exactly 1000 digits are formatted normally. All the caps are in one table in Limits. The helper format_number() calls Format::withSeparator() with the defaults.

Ordinals#

Format::ordinal() returns the Persian ordinal word for a non-negative integer. It builds the cardinal words with NumberToWords (see Number to words) and then applies the suffix rules:

  • 1 is the irregular «اول».
  • A cardinal ending in «سه» becomes «سوم». This also covers 23, 33 and so on.
  • A cardinal ending in «ی» (as in «سی») takes a half-space and «ام».
  • Everything else adds «م».
echo Format::ordinal(1), "\n";     // اول
echo Format::ordinal(2), "\n";     // دوم
echo Format::ordinal(3), "\n";     // سوم
echo Format::ordinal(21), "\n";    // بیست و یکم
echo Format::ordinal(23), "\n";    // بیست و سوم
echo Format::ordinal(30), "\n";    // سی‌ام
echo Format::ordinal(33), "\n";    // سی و سوم
echo Format::ordinal(100), "\n";   // صدم
echo Format::ordinal(1000), "\n";  // یک هزارم
echo Format::ordinal(0), "\n";     // صفرم
echo Format::ordinal(3.0), "\n";   // سوم

Whole floats like 3.0 are accepted. Negative numbers, fractions (2.5) and non-finite floats throw InvalidNumberException (invalid_number, or non_finite_number for INF and NaN). The helper is ordinal(). For Arabic ordinals, see Arabic number words.

Words, not digits. ordinal() returns the spelled-out word. It does not produce short numeric forms. Build those yourself with Digits::toPersian() if you need them.