Arabic number words

Spell numbers in Modern Standard Arabic with the right gender, case and vowel marks, up to 10^27, and write the ordinals from 1 to 99. Every option is shown with real output.

On this page
  1. What it does
  2. Quick start
  3. All options
  4. Gender and counted nouns
  5. Case
  6. Vowel marks
  7. Hundreds and spelling
  8. Large numbers
  9. Negative numbers
  10. Ordinals from 1 to 99
  11. Errors
  12. Where the rules come from

What it does#

In Arabic, the form of a number depends on what it counts. «ثلاثة كتب» (three books) and «ثلاث سيارات» (three cars) use different forms of three. A number also changes with its grammatical case, and it has its own rules for 11 to 19, for 200 and for 2000. NumberToWords::convert($n, 'ar', $options) handles these. Without options, it gives the plain counting form, the same text as in earlier releases.

The options are an RtlyKit\Number\ArabicOptions object, or an array with the same keys. NumberToWords::ordinal() writes the ordinals from 1 to 99. Persian has no options. Passing options with fa throws an exception.

Quick start#

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

use RtlyKit\Number\ArabicOptions;
use RtlyKit\Number\NumberToWords;

echo NumberToWords::convert(3, 'ar'), "\n";                                       // ثلاثة
echo NumberToWords::convert(1250, 'ar', ['mode' => 'noun', 'gender' => 'f']), "\n";   // ألف ومئتان وخمسون
echo NumberToWords::convert(-5, 'ar', new ArabicOptions(negative: 'ناقص')), "\n";  // ناقص خمسة
echo NumberToWords::ordinal(21), "\n";                                            // الحادي والعشرون
echo NumberToWords::ordinal(21, 'ar', ['gender' => 'f']), "\n";                   // الحادية والعشرون

A wrong key, value or type in the options throws InvalidNumberException with the code invalid_argument. A raw TypeError or ValueError never escapes.

All options#

OptionValuesDefaultWhat it does
modecount, nouncountcount writes a bare number (answers, tables) and always uses the masculine forms. noun means a counted noun follows right after the number
genderm, fmThe gender of the singular of the counted noun. Ignored in count mode
casenom, acc, gennomThe grammatical case of the whole number. It changes the letters of two, the tens, the hundreds-duals and the thousands-duals
diacriticsnone, casenonecase adds the case endings (short vowels and tanwin)
hundredsmi_a, ma_i_ami_aThe spelling مئة or مائة
joinHundredstrue, falsetrueثلاثمئة (joined) or ثلاث مئة (spaced)
negativeone word, 1 to 64 bytesسالبThe word before a negative number. No spaces or control characters
billionmilyar, bilyonmilyarThe name of 109: مليار or بليون
definitetrue, falsetrueOrdinals only: with the article (الأول) or without it (أول)

Each value also has a constant on ArabicOptions, such as ArabicOptions::MODE_NOUN, GENDER_FEMININE, CASE_GENITIVE, DIACRITICS_CASE, HUNDREDS_MA_I_A and BILLION_BILYON. ArabicOptions::fromArray() builds the object from an array.

Gender and counted nouns#

Set mode to noun when a counted noun follows the number, and give the gender of the singular of that noun. The numbers 3 to 10 take the opposite gender of the noun. The numbers 11 to 19 agree with it. The number 1, the number 2 and the units in 21 to 99 follow the noun's gender too.

$m = ['mode' => 'noun', 'gender' => 'm'];   // masculine noun, for example كتاب
$f = ['mode' => 'noun', 'gender' => 'f'];   // feminine noun, for example سيارة

echo NumberToWords::convert(3, 'ar', $m), "\n";    // ثلاثة
echo NumberToWords::convert(3, 'ar', $f), "\n";    // ثلاث
echo NumberToWords::convert(13, 'ar', $m), "\n";   // ثلاثة عشر
echo NumberToWords::convert(13, 'ar', $f), "\n";   // ثلاث عشرة
echo NumberToWords::convert(11, 'ar', $m), "\n";   // أحد عشر
echo NumberToWords::convert(11, 'ar', $f), "\n";   // إحدى عشرة
echo NumberToWords::convert(21, 'ar', $f), "\n";   // واحدة وعشرون

In noun mode, the last word of the phrase is in construct with the noun. So 200 becomes «مئتا», 2000 becomes «ألفا» and two million becomes «مليونا». You decide the gender of a plural or collective noun, because the library does not guess it. The number words never contain the noun. The numbers 1 and 2 follow their noun in Arabic, so you place them yourself.

echo NumberToWords::convert(200, 'ar'), "\n";                          // مئتان
echo NumberToWords::convert(200, 'ar', ['mode' => 'noun']), "\n";      // مئتا
echo NumberToWords::convert(2000, 'ar', ['mode' => 'noun']), "\n";     // ألفا

Case#

The case option changes the letters of the words that change with case, even without vowel marks: اثنان and اثنين, عشرون and عشرين, مئتان and مئتين, ألفان and ألفين, and اثنا عشر and اثني عشر.

echo NumberToWords::convert(2, 'ar', ['case' => 'acc']), "\n";                         // اثنين
echo NumberToWords::convert(20, 'ar', ['case' => 'gen']), "\n";                        // عشرين
echo NumberToWords::convert(200, 'ar', ['case' => 'acc']), "\n";                       // مئتين
echo NumberToWords::convert(2000, 'ar', ['case' => 'acc']), "\n";                      // ألفين
echo NumberToWords::convert(12, 'ar', ['mode' => 'noun']), "\n";                       // اثنا عشر
echo NumberToWords::convert(12, 'ar', ['mode' => 'noun', 'case' => 'gen']), "\n";      // اثني عشر

Vowel marks#

With diacritics set to case, the library writes the case ending on the last letter of each word that has one. It writes nothing else: no inner vowels and no hamza changes. The marks come in the standard order, with the vowel before the shadda.

echo NumberToWords::convert(125, 'ar', ['diacritics' => 'case']), "\n";
// مئةٌ وخمسةٌ وعشرونَ
echo NumberToWords::convert(125, 'ar', ['diacritics' => 'case', 'case' => 'acc']), "\n";
// مئةً وخمسةً وعشرينَ
echo NumberToWords::convert(125, 'ar', ['diacritics' => 'case', 'case' => 'gen', 'mode' => 'noun']), "\n";
// مئةٍ وخمسةٍ وعشرينَ

A word that is joined to a following noun or number takes the short vowel (damma, fatha or kasra). A word that is not joined takes tanwin. The tens take the endings «ـُونَ» and «ـِينَ». Vowel marks are not defined for ordinals, so ordinal() with diacritics: 'case' throws.

Hundreds and spelling#

echo NumberToWords::convert(300, 'ar'), "\n";                                       // ثلاثمئة
echo NumberToWords::convert(300, 'ar', ['hundreds' => 'ma_i_a']), "\n";             // ثلاثمائة
echo NumberToWords::convert(300, 'ar', ['joinHundreds' => false]), "\n";            // ثلاث مئة
echo NumberToWords::convert(800, 'ar', ['joinHundreds' => false]), "\n";            // ثماني مئة
echo NumberToWords::convert(100, 'ar', ['hundreds' => 'ma_i_a']), "\n";             // مائة

Both مئة and مائة are in use, and so are the joined and spaced forms. We chose the shorter, original spelling مئة and the joined form as defaults. The options give you the others.

Large numbers#

The library uses the short scale. The names are ألف, مليون, مليار (or بليون with billion: 'bilyon'), تريليون, كوادريليون, كوينتيليون, سكستيليون (1021) and سبتيليون (1024). The limit is every integer below 1027. The numbers 3 to 10 of a scale use the plural (آلاف, ملايين, مليارات, تريليونات and so on). Two of a scale is the singular with ان or ين.

echo NumberToWords::convert(5000, 'ar'), "\n";                                // خمسة آلاف
echo NumberToWords::convert(11000, 'ar'), "\n";                               // أحد عشر ألف
echo NumberToWords::convert(1000000000, 'ar'), "\n";                          // مليار
echo NumberToWords::convert(1000000000, 'ar', ['billion' => 'bilyon']), "\n"; // بليون
echo NumberToWords::convert(2000000000, 'ar'), "\n";                          // ملياران
echo NumberToWords::convert('1000000000000', 'ar'), "\n";                     // تريليون
echo NumberToWords::convert('1000000000000000000000000', 'ar'), "\n";         // سبتيليون
echo NumberToWords::convert(102000, 'ar'), "\n";                              // مئة ألف وألفان

The last line shows a choice we made. A group of hundreds plus 1 to 10 in front of a scale word is split into two parts, as in «مئة ألف وألفان» for 102 000. The shorter «مئة وألفان» could also be read as 100 plus 2000, so we avoid it. Groups with 11 to 99, or none, stay together («مئة وأحد عشر ألف» for 111 000).

A number of 1027 or more throws InvalidNumberException with the code number_too_large and the context limit = 10^27 - 1.

Negative numbers#

A negative number starts with «سالب». Use the negative option to change the word. Zero is never negative.

echo NumberToWords::convert(-5, 'ar'), "\n";                              // سالب خمسة
echo NumberToWords::convert(-5, 'ar', ['negative' => 'ناقص']), "\n";      // ناقص خمسة

Ordinals from 1 to 99#

NumberToWords::ordinal($n, 'ar', $options) returns the ordinal. Its default locale is ar. The options that count are gender, case and definite. The numbers 11 to 19 have both parts agree with the noun, and have no case.

echo NumberToWords::ordinal(1), "\n";                                              // الأول
echo NumberToWords::ordinal(1, 'ar', ['gender' => 'f']), "\n";                     // الأولى
echo NumberToWords::ordinal(3), "\n";                                              // الثالث
echo NumberToWords::ordinal(3, 'ar', ['gender' => 'f']), "\n";                     // الثالثة
echo NumberToWords::ordinal(11), "\n";                                             // الحادي عشر
echo NumberToWords::ordinal(11, 'ar', ['gender' => 'f']), "\n";                    // الحادية عشرة
echo NumberToWords::ordinal(21, 'ar', ['case' => 'gen']), "\n";                    // الحادي والعشرين
echo NumberToWords::ordinal(2, 'ar', ['definite' => false]), "\n";                 // ثان
echo NumberToWords::ordinal(2, 'ar', ['definite' => false, 'case' => 'acc']), "\n"; // ثانيا
echo NumberToWords::ordinal(99), "\n";                                             // التاسع والتسعون

Zero and negative numbers throw invalid_number. A number above 99 throws number_too_large. Another locale throws UnsupportedLocaleException. For Persian ordinals, use Format::ordinal() from Digits and number formatting.

Errors#

SituationExceptionCode
Unknown localeUnsupportedLocaleExceptionunsupported_locale
Option key, value or type invalid. Options with fa. diacritics on ordinalsInvalidNumberExceptioninvalid_argument
Not an integer, a fraction, NaN or INFInvalidNumberExceptioninvalid_number, non_finite_number
String over 4096 bytesInvalidNumberExceptioninput_too_long
convert() of 1027 or more, ordinal above 99InvalidNumberExceptionnumber_too_large
Ordinal of 0 or lessInvalidNumberExceptioninvalid_number
try {
    NumberToWords::convert(5, 'ar', ['gender' => 'x']);
} catch (\RtlyKit\Exceptions\InvalidNumberException $e) {
    echo $e->getMessage(), ' [', $e->getErrorCode()->value, "]\n";
    // Invalid option "gender": expected one of m, f. [invalid_argument]
}

Where the rules come from#

The forms follow Arabic grammar pages, a dictionary and one language-academy decision, all listed in the sources section. Known-answer tests cover each rule. A few outputs, such as the split groups for 102 000 and the plural forms of the large scale names, still wait for review by a native speaker.

Related: Number to words for Persian and the reverse direction, and Limits.