DEV Community

Mahmoud Ibrahim — Confileo
Mahmoud Ibrahim — Confileo

Posted on

Spelling numbers in Arabic is harder than it looks: the four rules behind tafqeet

Most "number to words" libraries translate digits one at a time. That works in English and fails in Arabic on the very first cheque. I maintain an Arabic number-to-words converter (تفقيط) used for invoices and cheques, and these are the rules that broke every naive version I wrote.

1. Units come before tens

English says twenty-five. Arabic says five and twenty: خمسة وعشرون. Any loop that walks the digits left to right and concatenates words produces «عشرون وخمسة», which a bank clerk reads as wrong at a glance.

2. Three to ten take the opposite gender of the noun

This is the rule everyone forgets. One and two agree with the counted noun; three to ten take the other gender:

count masculine noun (جنيه) feminine noun (ليرة)
1 جنيه واحد ليرة واحدة
3 ثلاثة جنيهات ثلاث ليرات
10 عشرة جنيهات عشر ليرات

So the words for 3–10 depend on the currency, not only on the digit. A converter that hard-codes «ثلاثة» is correct for pounds and wrong for lira.

3. The counted noun changes form with the last two digits

This is the part that makes a lookup table impossible:

last two digits of the amount form of the noun example
3–10 plural ثلاثة آلاف، سبعة جنيهات
11–99 singular, accusative (tanween) أحد عشر ألفاً، خمسون جنيهاً
00 (100, 1,000…) singular مائة جنيه، ألف جنيه

The same rule applies to the scale words themselves, which is why 3,000 is «ثلاثة آلاف» but 11,000 is «أحد عشر ألفاً» and 100,000 is «مائة ألف».

4. Two hundred loses its ن in front of a noun

«مائتان» on its own, but مائتا ألف and مائتا جنيه when a noun follows (the dual in a construct loses its final nun). A converter that emits «مائتان ألف» is one character off, and that character is exactly what a proof-reader looks for.

A version that gets these right

const ONES = ['', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية', 'تسعة'];
const TEENS = ['عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر',
               'ستة عشر', 'سبعة عشر', 'ثمانية عشر', 'تسعة عشر'];
const TENS = ['', '', 'عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون'];
const HUNDREDS = ['', 'مائة', 'مائتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة',
                  'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة'];

// 1..999, masculine, nominative (the cheque convention)
function below1000(n) {
  const parts = [];
  const h = Math.floor(n / 100), r = n % 100;
  if (h) parts.push(HUNDREDS[h]);
  if (r >= 20) {
    const u = r % 10, t = Math.floor(r / 10);
    parts.push(u ? `${ONES[u]} و${TENS[t]}` : TENS[t]);   // units before tens
  } else if (r >= 10) parts.push(TEENS[r - 10]);
  else if (r) parts.push(ONES[r]);
  return parts.join(' و');
}

// Pick the noun form from the last two digits (rule 3), and fix مائتان (rule 4).
function counted(k, forms) {
  if (k === 1) return forms.one;
  if (k === 2) return forms.two;
  const r = k % 100;
  const noun = r >= 3 && r <= 10 ? forms.few : r >= 11 ? forms.many : forms.one;
  const words = r === 0 && Math.floor(k / 100) % 10 === 2 && k < 1000 ? 'مائتا' : below1000(k);
  return `${words} ${noun}`;
}

const THOUSAND = { one: 'ألف', two: 'ألفان', few: 'آلاف', many: 'ألفاً' };
const MILLION = { one: 'مليون', two: 'مليونان', few: 'ملايين', many: 'مليوناً' };

function tafqeet(n) {
  if (n === 0) return 'صفر';
  const m = Math.floor(n / 1e6), t = Math.floor(n / 1e3) % 1000, rest = n % 1000;
  const parts = [];
  if (m) parts.push(counted(m, MILLION));
  if (t) parts.push(counted(t, THOUSAND));
  if (rest) parts.push(below1000(rest));
  return parts.join(' و');
}

tafqeet(25);        // خمسة وعشرون
tafqeet(3000);      // ثلاثة آلاف
tafqeet(11000);     // أحد عشر ألفاً
tafqeet(200000);    // مائتا ألف
tafqeet(1250750);   // مليون ومائتان وخمسون ألفاً وسبعمائة وخمسون
Enter fullscreen mode Exit fullscreen mode

That covers the integer part in the masculine. A real cheque line still needs the currency noun chosen by rule 3, the feminine table for feminine currencies, the fraction («وخمسون قرشاً»), and the framing banks expect: فقط ... لا غير.

Try it on your own numbers

The code above is open source as arabic-tafqeet (MIT, one file, tests included). The full version — pound, riyal, dirham, dinar and dollar, the fraction, the فقط/لا غير framing, and words back to digits — runs as a free converter: تفقيط الأرقام — Arabic number to words for cheques and invoices. There is an English number to words tool on the same site if you need both languages on one invoice.

If you find a number it spells wrong, tell me in the comments — that is how every rule above was found.

Top comments (0)