financial data table

Tables, Currencies, and Percentages: What’s Really Happening Behind the Screen?

|

How should currencies and percentages be stored in a database? Currency symbol direction, financial tables, and rounding — real dilemmas with practical code solutions.

Word count: ~1900 • Reading time: 9 minutes

Tables, Currencies, and Percentage Dilemmas

What’s really happening behind the screen — and where do the bugs hide?


Note: This article stands on its own and can be read without any prerequisites, but if you’d like to understand how numbers and dates are displayed automatically by region, start with Localizing Numbers and Dates with Intl.

In this article from Zy Yazan Platform, we move into the deepest layer of the localization problem: not how numbers are displayed on screen, but how they’re stored in the database in the first place. This is where most silent financial bugs are born — the kind that never show up in testing and only surface when a real client complains.

Three dilemmas each deserve their own discussion: storing currencies, storing percentages, and currency symbol direction in the UI. Each one is a design decision with long-range consequences.

Part One: How Should Currencies Be Stored in a Database?

The question sounds simple: I have an amount of ١٢٥٠٫٧٥ Saudi riyals — how do I store it? But the answer involves a branching set of decisions.

The Problem with Floating-Point Types

The most common mistake is using FLOAT or DOUBLE to store monetary amounts. Both types use floating-point representation, which is stored internally in binary — and some decimal fractions can’t be represented with perfect precision in that system:

// The classic floating-point problem in JavaScript
console.log(0.1 + 0.2);
// → 0.30000000000000004  ← not 0.3!

// Same problem in the database:
// SELECT 0.1 + 0.2 AS result  →  0.30000000000000004
// Storing 1250.75 as FLOAT may return 1250.7499999...

In the context of a technical article, this error is interesting. In the context of an invoice with a thousand line items, it means wrong numbers on a financial document.

The Fix: DECIMAL or INTEGER

To store monetary amounts safely, you have two correct options:

Option one — DECIMAL (or NUMERIC): a fixed-precision data type where you define the precision yourself:

-- SQL: a financial amount column with 15 digits and 2 decimal places
CREATE TABLE invoices (
  id          INT PRIMARY KEY,
  amount      DECIMAL(15, 2),   -- ✅ fixed precision, safe for money
  currency    CHAR(3),          -- ISO code: SAR, AED, EGP
  created_at  TIMESTAMP
);

-- Example insert
INSERT INTO invoices (amount, currency) VALUES (1250.75, 'SAR');

Option two — INTEGER in smallest unit: the approach used by many large payment systems (Stripe, PayPal) — store the amount in the smallest monetary unit and avoid decimal fractions entirely:

// ١٢٥٠٫٧٥ riyals = 125075 halalas (smallest unit = halala = 1/100 riyal)
const amountInHalala = 125075; // INTEGER — zero decimal errors

// At display time: convert for rendering only
const amountForDisplay = amountInHalala / 100; // 1250.75
const formatted = new Intl.NumberFormat('ar-SA', {
  style: 'currency',
  currency: 'SAR'
}).format(amountForDisplay);
// ← ر.س ١٬٢٥٠٫٧٥

One caveat: this approach assumes every currency has exactly two decimal places. The Kuwaiti dinar (KWD) has three (fils = 1/1000 dinar), and the Japanese yen has none at all. Always store the decimal digit count alongside each currency.

Storing the Currency Code: Never Store the Visual Symbol

Another common mistake: storing the visible currency symbol (“ر.س” or “$”) in the database instead of the international ISO code:

❌ Wrong — what you store ✅ Right — what you should store Why
ر.س SAR The symbol varies by language — Intl handles rendering
د.إ AED ISO 4217 is a global standard that never changes
ج.م EGP Makes conversion, comparison, and reporting much easier
$ USD $ also means Australian and Canadian dollars

A database is a store of facts, not display preferences. Store the ISO code and let Intl handle the conversion to whatever the user expects to see.

Part Two: The Percentage Dilemma — 15 or 0.15?

This dilemma looks simpler on the surface, but it’s the source of recurring bugs in development teams. The question: if a discount is ١٥٪, what do you store in the database?

Two Schools and Their Consequences

School What’s stored Discount on 1000 Display with Intl
Literal value 15 1000 * (15 / 100) format(15 / 100)
Decimal fraction 0.15 1000 * 0.15 format(0.15)

Both schools are valid — the problem appears when a single team mixes them without documentation. The result is a 1500% discount instead of 15%, or a 0.15% tax instead of 15%. Either way, it’s a silent disaster inside an invoice system.

The Practical Recommendation

The school that aligns most naturally with JavaScript and the Intl API is storing the decimal fraction (0.15), because Intl.NumberFormat with style: 'percent' expects a value between 0 and 1 by default:

// ✅ Value stored in DB: 0.15
const discountRate = 0.15;

const percentFormatter = new Intl.NumberFormat('ar-SA', {
  style: 'percent',
  maximumFractionDigits: 1,
});

console.log(percentFormatter.format(discountRate));
// ← ١٥٪

// Calculate discount directly — no dividing by 100
const originalPrice  = 1000;
const discountAmount = originalPrice * discountRate; // 150
const finalPrice     = originalPrice - discountAmount; // 850

If you choose to store the literal value (15), document it explicitly in the database schema and add a mandatory conversion layer before any use:

// Value stored in DB: 15  (documented as "percentage literal")
const discountLiteral = 15;

// Mandatory conversion layer before any use
const discountRate = discountLiteral / 100; // 0.15

console.log(percentFormatter.format(discountRate));
// ← ١٥٪

Part Three: Currency Symbol Direction — Before or After the Number?

This dilemma is subtler than the previous two because it intersects both text direction (RTL/LTR) and each country’s own conventions at the same time.

There Is No Single Arabic Rule

Arab countries don’t agree on where the currency symbol goes:

Country Common Format Example Symbol Position
Saudi Arabia Symbol then number ر.س ١٬٢٥٠٫٠٠ Before number (leading)
United Arab Emirates Symbol then number د.إ ١٬٢٥٠٫٠٠ Before number (leading)
Egypt Number then symbol ١٬٢٥٠٫٠٠ ج.م After number (trailing)
Morocco Number then symbol 1.250,00 MAD After number (trailing)

The good news: Intl.NumberFormat knows these differences and applies them automatically. The problem appears when you try to override them manually, or when you build a UI that assumes a fixed symbol position.

The Problem with Manual Formatting

// ❌ Wrong — manual concatenation assumes a fixed position
function formatCurrency(amount, symbol) {
  return `${symbol} ${amount.toFixed(2)}`;
  // always puts the symbol on the left — wrong for Saudi Arabia in RTL
}

// ✅ Right — let Intl determine the position
function formatCurrency(amount, locale, currency) {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency,
  }).format(amount);
}

console.log(formatCurrency(1250, 'ar-SA', 'SAR'));
// ← ر.س ١٬٢٥٠٫٠٠  (symbol in the correct position for Saudi Arabia)

console.log(formatCurrency(1250, 'ar-EG', 'EGP'));
// ← ١٬٢٥٠٫٠٠ ج.م  (symbol in the correct position for Egypt)

When You Need Fine-Grained Control Over Symbol Position

Sometimes an invoice design requires placing the symbol in a specific spot regardless of convention. In that case, use formatToParts(), which we introduced in the previous article:

function getCurrencyParts(amount, locale, currency) {
  const parts = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).formatToParts(amount);

  // Extract parts individually
  const symbol  = parts.find(p => p.type === 'currency')?.value;
  const integer = parts.find(p => p.type === 'integer')?.value;
  const decimal = parts.find(p => p.type === 'decimal')?.value;
  const fraction= parts.find(p => p.type === 'fraction')?.value;

  return { symbol, number: `${integer}${decimal}${fraction}` };
}

const { symbol, number } = getCurrencyParts(1250.75, 'ar-SA', 'SAR');
// symbol  → 'ر.س'
// number  → '١٬٢٥٠٫٧٥'

// Now control the layout however you need in HTML

Part Four: Financial Tables — Bugs That Only Show Up When You Add

Invoice and report tables add another dimension: summing formatted numbers, or dealing with mixed-direction columns.

Never Sum Formatted Numbers — Always Sum Raw Values

const items = [
  { name: 'Design Services',      amount: 1500.00 },
  { name: 'Development Services', amount: 3200.50 },
  { name: 'Consulting',           amount:  450.00 },
];

const formatter = new Intl.NumberFormat('ar-SA', {
  style: 'currency',
  currency: 'SAR',
});

// ✅ Sum the raw numbers first
const total = items.reduce((sum, item) => sum + item.amount, 0);
// total = 5150.50

// Then format the result once for display
console.log(formatter.format(total));
// ← ر.س ٥٬١٥٠٫٥٠

// ❌ Never sum formatted strings
// 'ر.س ١٬٥٠٠٫٠٠' + 'ر.س ٣٬٢٠٠٫٥٠'  ← meaningless

The tabular-nums Property for Financial Tables

When displaying a column of financial figures in a table, visual alignment matters for quick reading. The CSS property font-variant-numeric: tabular-nums gives every digit equal width regardless of its shape:

/* Amount column in a financial table */
.amount-column {
  font-variant-numeric: tabular-nums;
  text-align: end;        /* right in LTR, left in RTL — logical, not absolute */
  font-feature-settings: "tnum" 1;  /* extra support for older fonts */
}

Notice text-align: end instead of right — a CSS logical property that adapts automatically to text direction. (We covered this in detail in our earlier series on CSS Logical Properties.)

Part Five: VAT — The Rounding Dilemma

One last issue worth raising because it causes genuine disagreements between developers and accountants: how do you calculate tax across multiple line items?

Two different approaches produce two different results:

const vatRate = 0.15; // 15% VAT
const items = [100.10, 200.20, 300.30];

// Method ①: calculate tax per item, then sum
const taxPerItem = items.map(item =>
  Math.round(item * vatRate * 100) / 100
);
// [15.02, 30.03, 45.05]
const totalTax1 = taxPerItem.reduce((a, b) => a + b, 0);
// totalTax1 = 90.10

// Method ②: sum everything first, then calculate tax once
const subtotal  = items.reduce((a, b) => a + b, 0); // 600.60
const totalTax2 = Math.round(subtotal * vatRate * 100) / 100;
// totalTax2 = 90.09

// Difference: 0.01 — trivial on one invoice, compounded across thousands

There’s no “programmatically correct” answer here — the decision is set by the tax authority in each country (ZATCA in Saudi Arabia, for example). What matters is that your choice is consistent and documented across the entire system.

financial data table


Summary and Next Step

All three dilemmas we covered share a single underlying principle: separate storage from display. The database stores the mathematical truth (DECIMAL, ISO 4217, decimal fraction). The display layer converts that truth into whatever the user in their region expects to see.

In the next article — the final workshop project — we’ll build a complete invoice system that brings everything together: a correctly structured data layer, and automatic rendering of currencies, dates, and percentages for three Arab countries at once.

Recommended next step:

Continue with: Workshop Project: A Multi-Country Invoice System


References and Sources:

  1. ISO 4217 — Currency Codes: iso.org — Currency Codes
  2. PostgreSQL Docs — Numeric Types: postgresql.org — Numeric Types
  3. Stripe’s approach to storing amounts in smallest unit: stripe.com — Zero-Decimal Currencies
  4. MDN Docs — Intl.NumberFormat.formatToParts: developer.mozilla.org
  5. VAT Regulations — ZATCA (Saudi Arabia): zatca.gov.sa
Zy Yazan Platform © 2026

Localization Series

Financial Data Localization Guide — 4 Articles

Article 1
1 / 4

Arabic vs. Indian Digits and Fonts: What’s the Difference — and Why Does It Matter in Code?

A deep look into numerical representations, font rendering variations, and their structural impacts on programming languages.

Article 2
2 / 4

Localizing Numbers and Dates with Intl.NumberFormat and Intl.DateTimeFormat

Leveraging native standardization libraries to localize dates, times, and numeric structures based on geographic locales.

Article 3
3 / 4

Tables, Currencies, and Percentages: What’s Really Happening Behind the Screen?

Behind-the-scenes exploration of structural challenges, field calculations, and display parameters for sensitive financial values.

Article 4
4 / 4

Workshop Project: A Multi-Country Invoice System for Saudi Arabia, UAE, and Egypt

Practical integration guide for aligning local accounting parameters, invoice generation rules, and layout logic for targeted Arab regions.

Series: Financial Data Localization Guide — 4 Articles  |  Zyyazan Platform © 2026

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *