GuideFintechGCC

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).

markdown
| 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

typescript
// ❌ Floating-point arithmetic — DO NOT use for money
const price = 1.10 // AED
const tax = 0.05 // AED
const total = price + tax
console.log(total) // → 1.1499999999999999 (NOT 1.15)
// ❌ Rounding AFTER float arithmetic still hides precision loss
console.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.

typescript
// ✅ 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)

typescript
// ── 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.

typescript
// ── 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.

bash
# Fetch live AED→KWD rate
curl "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
# }
# }
# }
typescript
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
# ✅ Python: use the built-in decimal module for monetary arithmetic
from decimal import Decimal, ROUND_HALF_UP, getcontext
# Increase precision for intermediate calculations
getcontext().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

python
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

python
import os
import requests
from 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 → KWD
print(convert_currency("1000.00", "AED", "KWD")) # e.g. "83.540"

Common mistakes & fixes

typescript
// ─── Common mistakes and fixes ───────────────────────────────────────────────
// ❌ MISTAKE 1: Parsing a currency string with parseFloat
const 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 explicitly
function 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 money
const 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

PostgreSQL / MySQL: use NUMERIC(19, 4) or store as BIGINT subunits with a separate currency CHAR(3) column.
MongoDB: use NumberDecimal (128-bit), not Double.
Stripe / payment processors: always submit amounts as integers in the currency's smallest unit (e.g. fils for AED, fils for KWD).
Avoid FLOAT / DOUBLE column types for monetary values — they lose precision silently.

Related resources