GCC Currency Precision & Rounding
Avoid floating-point errors and comply with CBUAE / SAMA financial standards when handling AED, SAR, KWD, BHD, OMR, QAR, and EGP in e-commerce and fintech applications.
GCC currency subunits at a glance
Each GCC currency has a defined smallest unit. Three of the seven currencies use 3 decimal places — a common source of bugs when developers assume all currencies behave like USD (2 dp).
| Currency | Code | Minor unit | Subunit name | Decimal places ||----------|------|-----------|--------------|----------------|| UAE Dirham | AED | Fils | 1 AED = 100 fils | 2 || Saudi Riyal | SAR | Halala | 1 SAR = 100 halalas | 2 || Kuwaiti Dinar | KWD | Fils | 1 KWD = 1 000 fils | 3 || Bahraini Dinar| BHD | Fils | 1 BHD = 1 000 fils | 3 || Omani Rial | OMR | Baisa | 1 OMR = 1 000 baisas| 3 || Qatari Riyal | QAR | Dirham | 1 QAR = 100 dirhams | 2 || Egyptian Pound| EGP | Piastre | 1 EGP = 100 piastres| 2 |Why IEEE 754 floats fail for money
// ❌ Floating-point arithmetic — DO NOT use for moneyconst price = 1.10 // AEDconst tax = 0.05 // AEDconst total = price + taxconsole.log(total) // → 1.1499999999999999 (NOT 1.15) // ❌ Rounding AFTER float arithmetic still hides precision lossconsole.log((price + tax).toFixed(2)) // → "1.15" (appears OK but cumulative // errors will surface in batch sums)Node.js — integer subunit arithmetic (AED / SAR / QAR / EGP)
Store all amounts as integer subunits (fils or halalas). Convert to a decimal string only when displaying to the user or sending to an API.
// ✅ Store and operate in the smallest subunit (fils / halalas)// then convert to display units only at the last moment. /** Convert an AED decimal string/number to fils (integer). * Uses exponential notation to avoid float-multiply drift * (e.g. naive 1.005 * 100 → 100.4999... in IEEE 754). */function toFils(aed: number | string): bigint { return BigInt(Math.round(Number(`${Number(aed)}e2`)))} /** Format fils back to an AED string for display / API output. */function fromFils(fils: bigint): string { const absVal = fils < 0n ? -fils : fils const sign = fils < 0n ? "-" : "" const whole = absVal / 100n const frac = absVal % 100n return `${sign}${whole}.${String(frac).padStart(2, "0")}`} // ── Example: cart total ───────────────────────────────────────────────────────const items = [ { price: "1.10", qty: 3 }, // AED 3.30 { price: "0.05", qty: 1 }, // AED 0.05] const totalFils = items.reduce( (sum, item) => sum + toFils(item.price) * BigInt(item.qty), 0n,) console.log(fromFils(totalFils)) // → "3.35" ✓Node.js — multi-currency support (KWD, BHD, OMR with 3 dp)
// ── KWD uses 3 decimal places (1 KWD = 1 000 fils) ───────────────────────── const SUBUNIT: Record<string, number> = { AED: 100, SAR: 100, QAR: 100, EGP: 100, KWD: 1000, BHD: 1000, OMR: 1000,} function toSubunits(amount: number | string, currency: string): bigint { const scale = SUBUNIT[currency] if (!scale) throw new Error(`Unknown currency: ${currency}`) // Exponential trick avoids float-multiply drift (e.g. 0.001 * 1000 → 0.9999...) return BigInt(Math.round(Number(`${Number(amount)}e${Math.log10(scale)}`)))} function fromSubunits(subunits: bigint, currency: string): string { const scale = SUBUNIT[currency] if (!scale) throw new Error(`Unknown currency: ${currency}`) const scaleBig = BigInt(scale) const absVal = subunits < 0n ? -subunits : subunits const sign = subunits < 0n ? "-" : "" const whole = absVal / scaleBig const frac = absVal % scaleBig const decimals = Math.log10(scale) // 2 or 3 return `${sign}${whole}.${String(frac).padStart(decimals, "0")}`} console.log(fromSubunits(toSubunits("12.500", "KWD"), "KWD")) // → "12.500"console.log(fromSubunits(toSubunits("0.001", "KWD"), "KWD")) // → "0.001"Rounding rules & VAT
CBUAE and SAMA mandate half-up rounding for consumer-facing amounts. UAE VAT is 5 %; KSA VAT is 15 %. Round each line item (net, VAT, gross) independently so that displayed figures sum correctly — but never round a value and then feed the rounded result into further float arithmetic.
// ── GCC rounding rules ───────────────────────────────────────────────────────// CBUAE and SAMA mandate half-up (ROUND_HALF_UP) for consumer-facing amounts.// JavaScript's Math.round() uses half-up for positive numbers, but// for negative numbers it rounds toward +∞ (i.e. half-down for negatives).// Use a dedicated rounding helper to be explicit: /** Half-up rounding to `decimals` places (positive and negative safe). * Uses exponential-notation trick to avoid float-multiply drift * (e.g. naive `1.005 * 100` → 100.4999... in IEEE 754). */function roundHalfUp(value: number, decimals: number): number { return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`)} // ── Tax calculations ──────────────────────────────────────────────────────────// UAE VAT = 5 % (Federal Decree-Law No. 8/2017)// KSA VAT = 15% (as of July 2020) function addVat(amountAed: number, vatRate = 0.05): { net: string; vat: string; gross: string} { const net = roundHalfUp(amountAed, 2) const vat = roundHalfUp(net * vatRate, 2) const gross = roundHalfUp(net + vat, 2) // avoid double-rounding error return { net: net.toFixed(2), vat: vat.toFixed(2), gross: gross.toFixed(2), }} console.log(addVat(100)) // { net: "100.00", vat: "5.00", gross: "105.00" }console.log(addVat(33.33)) // { net: "33.33", vat: "1.67", gross: "35.00" }console.log(addVat(100, 0.15))// { net: "100.00", vat: "15.00",gross: "115.00" }Node.js — multi-currency conversion via KhaleejiAPI
The Exchange Rates API returns live rates. Perform all arithmetic in integer subunits to preserve precision across currency pairs.
# Fetch live AED→KWD ratecurl "https://khaleejiapi.dev/api/v1/exchange/rates?base=AED" \ -H "Authorization: ******" # Response excerpt:# {# "data": {# "base": "AED",# "timestamp": "2026-08-18T07:00:00Z",# "rates": {# "KWD": 0.08354,# "SAR": 1.02260,# "BHD": 0.14105,# "OMR": 0.14434,# "QAR": 1.02220,# "EGP": 17.35900# }# }# }import { KhaleejiAPI } from "@khaleejiapi/sdk" const client = new KhaleejiAPI({ apiKey: process.env.KHALEEJI_API_KEY! }) // ── Multi-currency conversion with precision preservation ─────────────────────async function convertCurrency( amountStr: string, from: string, to: string,): Promise<string> { // 1. Fetch live rate from KhaleejiAPI const { data } = await client.finance.getExchangeRates({ base: from }) const rate: number = data.rates[to] if (!rate) throw new Error(`No rate for ${from}→${to}`) // 2. Work in integers to avoid floating-point drift // Use 8 decimal places of rate precision (scale by 1e8) // Requires SUBUNIT map and fromSubunits() from the multi-currency snippet above. const RATE_PRECISION = 100_000_000n const SUBUNIT_FROM = BigInt(SUBUNIT[from] ?? 100) const SUBUNIT_TO = BigInt(SUBUNIT[to] ?? 100) // Exponential trick: avoids float-multiply drift when converting to subunits const amountSubunits = BigInt(Math.round(Number(`${Number(amountStr)}e${Math.log10(Number(SUBUNIT_FROM))}`))) const rateBig = BigInt(Math.round(rate * Number(RATE_PRECISION))) // amountSubunits_to = amountSubunits_from * rate * (SUBUNIT_TO / SUBUNIT_FROM) const raw = amountSubunits * rateBig * SUBUNIT_TO / (SUBUNIT_FROM * RATE_PRECISION) return fromSubunits(raw, to)} // Convert AED 1 000.00 → KWD (3 decimal places)const kwdStr = await convertCurrency("1000.00", "AED", "KWD")console.log(`AED 1 000.00 = KWD ${kwdStr}`) // e.g. "KWD 83.540"Python — use decimal.Decimal, not float
Python's built-in decimal module provides arbitrary-precision fixed-point arithmetic and all the rounding modes required by financial regulations.
# ✅ Python: use the built-in decimal module for monetary arithmeticfrom decimal import Decimal, ROUND_HALF_UP, getcontext # Increase precision for intermediate calculationsgetcontext().prec = 28 SUBUNIT = { "AED": Decimal("100"), "SAR": Decimal("100"), "QAR": Decimal("100"), "EGP": Decimal("100"), "KWD": Decimal("1000"), "BHD": Decimal("1000"), "OMR": Decimal("1000"),} DECIMAL_PLACES = { "AED": 2, "SAR": 2, "QAR": 2, "EGP": 2, "KWD": 3, "BHD": 3, "OMR": 3,} def to_subunits(amount: str | Decimal, currency: str) -> int: """Convert a decimal amount string to the smallest subunit (integer).""" scale = SUBUNIT[currency] value = Decimal(amount) * scale # Round half-up before converting to int return int(value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) def from_subunits(subunits: int, currency: str) -> str: """Format an integer subunit count back to a decimal string.""" places = DECIMAL_PLACES[currency] fmt = "1." + "0" * places value = Decimal(subunits) / SUBUNIT[currency] return str(value.quantize(Decimal(fmt), rounding=ROUND_HALF_UP)) # ── Example: cart total ───────────────────────────────────────────────────────items = [("1.10", 3), ("0.05", 1)] # (price AED, qty) total_fils = sum(to_subunits(price, "AED") * qty for price, qty in items)print(from_subunits(total_fils, "AED")) # → "3.35" ✓ # ── KWD precision ─────────────────────────────────────────────────────────────print(from_subunits(to_subunits("12.500", "KWD"), "KWD")) # → "12.500"print(from_subunits(to_subunits("0.001", "KWD"), "KWD")) # → "0.001"Python — VAT calculations
from decimal import Decimal, ROUND_HALF_UP UAE_VAT = Decimal("0.05") # 5 %KSA_VAT = Decimal("0.15") # 15 % def add_vat(amount: str | Decimal, vat_rate: Decimal = UAE_VAT) -> dict: """Return net, VAT, and gross as Decimal strings (2 dp for AED/SAR).""" net = Decimal(amount).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) vat = (net * vat_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) gross = (net + vat).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) return {"net": str(net), "vat": str(vat), "gross": str(gross)} print(add_vat("100")) # {'net': '100.00', 'vat': '5.00', 'gross': '105.00'}print(add_vat("33.33")) # {'net': '33.33', 'vat': '1.67', 'gross': '35.00'}print(add_vat("100", KSA_VAT))# {'net': '100.00', 'vat': '15.00','gross': '115.00'}Python — exchange rate conversion via KhaleejiAPI
import osimport requestsfrom decimal import Decimal, ROUND_HALF_UP KHALEEJI_API_KEY = os.environ["KHALEEJI_API_KEY"]BASE_URL = "https://khaleejiapi.dev" SUBUNIT = { "AED": 100, "SAR": 100, "QAR": 100, "EGP": 100, "KWD": 1000, "BHD": 1000, "OMR": 1000,}DECIMAL_PLACES = { "AED": 2, "SAR": 2, "QAR": 2, "EGP": 2, "KWD": 3, "BHD": 3, "OMR": 3,} def convert_currency(amount: str, from_ccy: str, to_ccy: str) -> str: """Fetch live rate from KhaleejiAPI and convert with integer subunit arithmetic.""" resp = requests.get( f"{BASE_URL}/api/v1/exchange/rates", params={"base": from_ccy}, headers={"Authorization": f"******"}, timeout=10, ) resp.raise_for_status() rate = Decimal(str(resp.json()["data"]["rates"][to_ccy])) # Integer subunit arithmetic: convert to smallest from_ccy unit, # scale by rate, divide back to to_ccy decimal representation. from_subunit = Decimal(SUBUNIT[from_ccy]) to_subunit = Decimal(SUBUNIT[to_ccy]) places = DECIMAL_PLACES[to_ccy] fmt = "1." + "0" * places amount_subunits = int( (Decimal(amount) * from_subunit).quantize(Decimal("1"), rounding=ROUND_HALF_UP) ) # amount_subunits * rate * to_subunit / from_subunit gives to_ccy subunits converted_subunits = ( Decimal(amount_subunits) * rate * to_subunit / from_subunit ).quantize(Decimal("1"), rounding=ROUND_HALF_UP) converted = converted_subunits / to_subunit return str(converted.quantize(Decimal(fmt), rounding=ROUND_HALF_UP)) # Convert AED 1 000.00 → KWDprint(convert_currency("1000.00", "AED", "KWD")) # e.g. "83.540"****** with your actual API key. Never hard-code keys in source files — use environment variables.Common mistakes & fixes
// ─── Common mistakes and fixes ─────────────────────────────────────────────── // ❌ MISTAKE 1: Parsing a currency string with parseFloatconst badAmount = parseFloat("AED 1,234.56".replace(/[^0-9.]/g, ""))// → 1234.56 (OK here but unreliable with locale-formatted numbers) // ✅ FIX: Strip currency symbol and thousand separators explicitlyfunction parseCurrencyString(raw: string): number { // Remove everything except digits, dots, minus sign return parseFloat(raw.replace(/[^0-9.-]/g, ""))} // ❌ MISTAKE 2: Using Number.EPSILON comparisons for moneyconst isEqual = Math.abs(1.10 + 0.05 - 1.15) < Number.EPSILON // false! // ✅ FIX: Compare integers (subunits)const aFils = toFils(1.10) + toFils(0.05)const bFils = toFils(1.15)console.log(aFils === bFils) // true ✓ // ❌ MISTAKE 3: Storing amounts as floating-point in the database// CREATE TABLE invoices (amount FLOAT); -- loses precision // ✅ FIX: Store as INTEGER (subunits) or NUMERIC/DECIMAL(19,4)// CREATE TABLE invoices (amount_fils INTEGER, currency CHAR(3));// Always reconstruct the display string in application code.Database storage recommendations
NUMERIC(19, 4) or store as BIGINT subunits with a separate currency CHAR(3) column.NumberDecimal (128-bit), not Double.FLOAT / DOUBLE column types for monetary values — they lose precision silently.Related resources
- Exchange Rates API reference — live GCC & global exchange rates
- Zakat Calculator API reference — multi-asset Zakat with live gold/silver prices
- CBUAE Open Finance guide — compliance requirements for UAE fintech applications
- Regional validation guide — Emirates ID, IBAN, and phone number validation