Dates, Numbers, Currency and Time Zones
A date, a number or an amount of money has one value and many presentations. The stored instant 2026-03-04T21:30:00Z is 4 March, 4:30 PM for a user in New York and 5 March, 00:30 for a user in Riyadh. The number 1234567.89 is 1,234,567.89 in en-US, 1.234.567,89 in de-DE, 12,34,567.89 in hi-IN and ١٬٢٣٤٬٥٦٧٫٨٩ in ar-SA. The locale, a language plus a region written as a BCP 47 tag such as ar-SA or hi-IN, decides the calendar, digits, separators, month names and currency symbol; the time zone decides which calendar day and clock time an instant falls on.
Testing this area means keeping the value and its presentation apart. For every field that shows, accepts, stores or compares such a value you check what is stored (one canonical form), what each locale displays (produced by a formatting library, not by string templates) and what happens to a value a user types in their own format. The character encoding lesson covered how text survives its journey through the system; this lesson covers the values whose meaning depends on where the user is.
Why it matters
These defects cost money and trust directly. A date parsed in the wrong order books a flight on 3 April instead of 4 March. An amount parsed with the wrong decimal separator turns 1.500 into one and a half riyals or into fifteen hundred. A report grouped by UTC day puts an order placed at 00:30 in Riyadh on the previous day, so the daily totals never reconcile with the bank. VAT rounded per line when the rule says on the total makes an invoice one halala (a hundredth of a riyal) off from the accounting records, and someone then has to find and explain that difference.
For a tester the difficulty is that these defects are invisible in your own environment. From Riyadh with an English browser you see Western digits, a +03:00 zone without daylight saving time and one calendar. The user in Jeddah with Arabic-Indic digits and the Hijri calendar, the user in Kolkata at +05:30 and the user in London whose clocks change in March hit failures you never provoked. Locale and time zone must be chosen deliberately as test conditions, like a browser or a device.
One value, many presentations
Every locale-sensitive value has a canonical form that the product should store and transmit: an instant as ISO 8601 (or RFC 3339, its profile for internet protocols) in UTC or with an explicit offset, 2026-03-04T21:30:00Z or 2026-03-05T00:30:00+03:00; a number as a plain decimal with a dot, 1234567.89; an amount as a decimal plus an ISO 4217 currency code, {"amount": "42.95", "currency": "SAR"}. The presentation is produced at the edge by a formatting library built on the locale data of the Unicode CLDR project: the Intl API in browsers and Node.js, APIs based on ICU (International Components for Unicode) on Android and iOS.
A locale is a BCP 47 language tag: en-US, en-GB, ar-SA, hi-IN. The language subtag picks words and names; the region subtag adjusts the defaults for digits, separators, calendar and first day of the week. The currency is not one of them: it travels with the amount as its ISO 4217 code. Unicode extensions override a default explicitly: ar-SA-u-nu-latn asks for Western digits, ar-SA-u-ca-islamic-umalqura for the Umm al-Qura calendar, hi-IN-u-nu-deva for Devanagari digits. The figure shows how far the presentations of one date, one number and one amount drift apart while the stored values do not change at all.
Intl for seven locale tags: the order of the parts, the digits, the separators, the month name, the calendar and the currency position all change while the stored values stay the same.| Locale tag | Short datedateStyle: short | Long datedateStyle: long | Numberstyle: decimal | Amountstyle: currency |
|---|---|---|---|---|
| Stored valueISO 8601 date, plain decimal, amount with its ISO 4217 code | 2026-03-04 | 2026-03-04 | 1234567.89 | 1500.00 SAR |
en-USEnglish, United States: month before day | 3/4/26 | March 4, 2026 | 1,234,567.89 | SAR 1,500.00 |
en-GBEnglish, United Kingdom: day before month | 04/03/2026 | 4 March 2026 | 1,234,567.89 | SAR 1,500.00 |
de-DEGerman, Germany: separators swapped, code after the amount | 04.03.26 | 4. März 2026 | 1.234.567,89 | 1.500,00 SAR |
hi-INHindi, India: Devanagari month name, lakh and crore grouping | 4/3/26 | 4 मार्च 2026 | 12,34,567.89 | SAR 1,500.00 |
ar-SAArabic, Saudi Arabia: Arabic-Indic digits, Arabic separators, symbol after the amount | ٤/٣/٢٠٢٦ | ٤ مارس ٢٠٢٦ | ١٬٢٣٤٬٥٦٧٫٨٩ | ١٬٥٠٠٫٠٠ ر.س. |
ar-JOArabic, Jordan: same digits and amount, Levantine month name | ٤/٣/٢٠٢٦ | ٤ آذار ٢٠٢٦ | ١٬٢٣٤٬٥٦٧٫٨٩ | ١٬٥٠٠٫٠٠ ر.س. |
ar-SA-u-ca-islamic-umalquraArabic, Saudi Arabia, Umm al-Qura calendar requested: another date, the same number | ١٥/٩/١٤٤٧ هـ | ١٥ رمضان ١٤٤٧ هـ | ١٬٢٣٤٬٥٦٧٫٨٩ | ١٬٥٠٠٫٠٠ ر.س. |
- Stored once, in the canonical form
- Unicode extension the product requests explicitly
Output of Node.js 22 (ICU 78, CLDR 48); another runtime or CLDR version can differ. The Arabic cells keep the invisible right-to-left marks (U+200F) and no-break spaces (U+00A0) the formatter inserts.
- Stored: is the canonical value correct and unambiguous, with its offset or zone and its currency code?
- Displayed: does every supported locale render it through the library, with the digits, separators, calendar and symbol the product decided on?
- Entered: is a value typed in the user's own format parsed with that locale, normalised or rejected with a clear message, and never silently misread?
Dates, calendars and time zones
A date presentation has three independent dimensions. The order of the parts: 3/4/26 is 4 March in en-US and 04/03/2026 is 4 March in en-GB, so the string 03/04/2026 names a different day in each. The digits and names: Arabic-Indic digits, localised month names, and in Levantine Arabic (ar-JO, ar-SY) March is آذار where Saudi Arabic uses مارس. The calendar: in Saudi Arabia the Umm al-Qura calendar runs beside the Gregorian one, 4 March 2026 is 15 Ramadan 1447 AH, and a Hijri month has 29 or 30 days, so the month ends of the two calendars differ.
A time zone is not part of the locale: it belongs to the user, the device or the account. An instant is one point in time; its wall-clock time and even its calendar day depend on the zone. Asia/Riyadh is UTC+03:00 without daylight saving time, Asia/Kolkata is UTC+05:30, Asia/Kathmandu is UTC+05:45, and Europe/London and America/New_York change their offset twice a year. Store instants in UTC or with an explicit offset, identify zones by their IANA name rather than by an offset, convert for display only, and test the moments where the day changes.
- Test values: the last day of a month and of a year in both calendars, 29 February 2028, 23:59 and 00:00 in the user's zone, and the change days of a zone that observes daylight saving time.
- Date-only values such as a date of birth or a due date have no zone and must not shift when the server or the viewer is somewhere else.
- Timestamps in seconds and in milliseconds get mixed up: a value a thousand times too small lands in January 1970, a thousand times too large in a far future year.
One instant, stored once: 2026-03-04T21:30:00Z
Zone (IANA) Offset Local wall-clock time Calendar day
UTC +00:00 2026-03-04 21:30 4 March
Europe/London +00:00 2026-03-04 21:30 4 March (BST from 29 March)
America/New_York -05:00 2026-03-04 16:30 4 March (EDT from 8 March)
Asia/Riyadh +03:00 2026-03-05 00:30 5 March
Asia/Kolkata +05:30 2026-03-05 03:00 5 March
Asia/Kathmandu +05:45 2026-03-05 03:15 5 March
A report grouped by "day" lists this order on 4 March in London
and on 5 March in Riyadh. Both are right; the report must name its zone.Try it: ISO 8601 Converter Try it: Timestamp Converter Try it: Date Test Data Generator
Numbers, digits and currency
A number presentation has a digit system and two separators: en-US writes 1,234,567.89, de-DE 1.234.567,89, fr-FR 1 234 567,89 with a narrow no-break space, hi-IN 12,34,567.89 in the Indian grouping, where the groups after the first three digits have two digits (one lakh is 1,00,000, that is 100,000, and one crore is 1,00,00,000, ten million), ar-SA ١٬٢٣٤٬٥٦٧٫٨٩ with Arabic-Indic digits and the Arabic separators U+066C and U+066B. The same string therefore has different meanings: 1.500 is one and a half in en-US and fifteen hundred in de-DE. Input must be parsed with the locale the user typed in, and runtimes disagree about Arabic-Indic digits: in JavaScript Number("٥٠٠") is NaN, while in Python int("٥٠٠") returns 500. The product must therefore decide, and the requirement must say, whether such digits are normalised to 0–9 or rejected with a clear message.
Money adds a currency and a rounding rule. An amount is a decimal plus an ISO 4217 code, and ISO 4217 also gives each currency's minor unit, the number of decimal places: two for SAR and INR, none for JPY, three for KWD, BHD, JOD and OMR, so "two decimals" is not a universal rule. Binary floating point cannot hold most decimal amounts exactly: in JavaScript 0.1 + 0.2 gives 0.30000000000000004 and 3 * 12.45 gives 37.349999999999994. Amounts belong in integers of minor units or in a decimal type, and the requirement must say where rounding happens and by which rule. The symbol and its position come from the locale, not from a template: SAR 1,500.00 in en-US, 1.500,00 SAR in de-DE, ١٬٥٠٠٫٠٠ ر.س. in ar-SA, where the string also carries right-to-left marks (U+200F) at both ends and a no-break space before the symbol, which is why a hand-typed expected string fails while the screen looks right. On screen the same marks keep the amount and its symbol in right-to-left order even next to Latin or neutral text, so ر.س. appears to the left of the number although the digits come first in the string itself; the RTL testing guide, the next step in the roadmap, explains the Unicode Bidirectional Algorithm behind this.
- Identifiers are not quantities: phone numbers, IBANs, order numbers and national ID numbers are never grouped, rounded or shown in localised digits.
- A refund of -42.95 must round exactly like the charge it reverses; where the minus sign and the percent sign go, and which invisible marks surround them, also comes from the locale.
- Never add amounts in different currencies; a total that sums SAR and INR lines is a data defect, not a formatting question.
Order: 3 x 12.45 SAR, VAT 15 %
Rule A, VAT on the subtotal: 37.35 x 0.15 = 5.6025 -> 5.60 total 42.95
Rule B, VAT per line: 12.45 x 0.15 = 1.8675 -> 1.87 x 3 = 5.61 total 42.96
Both are "15 % VAT rounded to 2 decimals". The requirement must name the rule,
and the test must use amounts on which the two rules differ.Try it: Decimal Dataset Generator Try it: Arabic Test Data Generator
Worked example: one order, three users
An order API returns the record below. Three people open the confirmation page: Noura in Jeddah (ar-SA with Western digits chosen in the product settings, zone Asia/Riyadh), Priya in Kolkata (hi-IN, Asia/Kolkata) and the finance team in London (en-GB, Europe/London). Every expected value is derived from the stored record with the same library the product uses, never typed from memory.
- Noura: placed on 5 March 2026 at 00:30; total
42.95 ر.س., Western digits because the setting says so, the symbol placed by the ar-SA formatter, the whole string correct inside a right-to-left paragraph. - Priya: placed on 5 March 2026 at 03:00; total
SAR 42.95, the ISO code because the hi-IN formatter shows the code for the riyal and the product does not convert currencies. - Finance: placed on 4 March 2026 at 21:30; total
SAR 42.95. The daily report for 4 March contains this order while Noura's order list shows it under 5 March; both are right, and the report header must name its zone. - Defects to look for:
4/3/2026or3/5/2026on Noura's page (the UTC day, or en-US order), VAT5.61(rounded per line),٤٢٫٩٥although the setting says Western digits,SAR42.95glued together by a template so that every locale sees the same string, and a changed total after the page re-parses its own formatted string.
{
"order_id": "ORD-2026-0042",
"placed_at": "2026-03-04T21:30:00Z",
"subtotal": "37.35",
"vat": "5.60",
"total": "42.95",
"currency": "SAR"
}Common mistakes
- Typing expected strings by hand and failing on the invisible characters a formatter inserts (U+200F, U+00A0, U+202F), or passing because the test strips everything, including the separators that matter.
- Reporting a "wrong date" that is correct in the user's zone, or accepting a wrong one because nobody checked the zone; record the stored instant, the zone and the locale together.
- Testing from one place only: Riyadh at +03:00 without daylight saving time never shows the DST and half-hour defects that users in Kolkata, London or New York hit.
- Accepting "two decimals, rounded" as a specification without asking where rounding happens and by which rule, then computing expected totals in a spreadsheet that rounds differently.
- Localising identifiers: phone numbers grouped like amounts, order numbers in Arabic-Indic digits, an IBAN with thousands separators.
- Assuming what
ar-SAproduces: the defaults for digits and calendar come from the CLDR version inside each runtime and have changed over the years, so browser, server and mobile app can disagree unless the product requests them explicitly.
The QA perspective
On a real team this work starts in the requirements review. For every date, number and amount field you ask how it is stored, how it is shown in each supported locale and how input is parsed, and you write the answers into the acceptance criteria. Ask the developers which formatting library and which CLDR or ICU version each runtime ships, ask the product owner which digits and calendar the product uses per locale and whether users can override them, and ask finance where VAT is rounded. The answers become test conditions, locale × zone × runtime, chosen like browsers in a compatibility matrix.
During execution, generate expected values with the same library rather than by hand, and keep a small set of instants and amounts that expose the differences: a time just after midnight in Riyadh, a date on a DST change day, an amount whose per-line and total rounding differ, a number typed with Arabic-Indic digits. A good bug report here contains the raw stored value, the locale tag, the zone identifier, the runtime version, the expected presentation and how you derived it; without them the developer cannot tell a formatting defect from a wrong stored value.
Practice exercise
Derive the expected values for a year-end order
An order API returns the record below: three units of one office printer at 38,450.35 SAR each, placed on the last evening of the year in UTC. The rule says: "VAT of 15 % is calculated on the order subtotal and rounded half up to two decimals; dates are shown in the customer's time zone; amounts are formatted for the customer's locale, with Western digits for Arabic." The finance team reports in UTC.
Two customers place this same order at the same instant: one in Riyadh (ar-SA, Asia/Riyadh) and one in Kolkata (hi-IN, Asia/Kolkata). Derive the expected subtotal, VAT and total, the date and time each customer and the finance report should show, and the checks you would run on the Arabic and Hindi confirmation pages. Then write one negative test for the quantity field and note what is still unclear in the rule.
{
"order_id": "ORD-2026-0917",
"placed_at": "2026-12-31T20:15:00Z",
"lines": [{ "sku": "PRN-400", "qty": 3, "unit_price": "38450.35" }],
"currency": "SAR"
}- Compute subtotal, VAT and total with the stated rule and with per-line rounding, so you know whether the two can be told apart on this order.
- Convert the stored instant to
Asia/RiyadhandAsia/Kolkataand note the calendar day and year in each, then in UTC for the finance report. - List what to verify on the ar-SA page (digits, separators, symbol position, direction, invisible characters) and on the hi-IN page (grouping, code or symbol).
- Write the negative test for a quantity typed as
٣and the open questions for the product owner and finance.
Show the model answer
Subtotal 3 × 38,450.35 = 115,351.05; VAT 115,351.05 × 0.15 = 17,302.6575, rounded half up to 17,302.66; total 132,653.71. Per line, 5,767.5525 rounds to 5,767.55, times 3 is 17,302.65 and the total 132,653.70, so 132,653.70 on any page or invoice is a defect against the stated rule. Compute in a decimal type or in halalas: in JavaScript 3 * 38450.35 gives 115351.04999999999, so code that truncates instead of rounding shows a subtotal of 115,351.04.
Riyadh (+03:00): 31 December 2026, 23:15. Kolkata (+05:30): 1 January 2027, 01:45. Finance report in UTC: 31 December 2026, 20:15. The same order belongs to 2026 for the Riyadh customer and for finance, and to 2027 for the Kolkata customer, so check every place that derives a year: the invoice date, an order list grouped by year, a "this year" filter and the annual report. If the Riyadh page shows the Umm al-Qura date, it is 22 Rajab 1448.
ar-SA page: Western digits as the rule says (132,653.71, not ١٣٢٬٦٥٣٫٧١), the symbol placed by the formatter, the string readable in a right-to-left context, the invisible marks present without breaking the layout, and the expected string taken from the same formatting call. hi-IN page: the total is above one lakh, so Indian grouping must show SAR 1,32,653.71; SAR 132,653.71 means the page used Western grouping or a hard-coded format. The riyal appears as the code SAR because the product does not convert currencies.
Negative test: a quantity ٣ typed on the Arabic page is either normalised to 3 with the subtotal unchanged at 115,351.05, or rejected with a message in Arabic; it must never become 0, NaN or an empty order. Open questions: which zone decides the tax period and the invoice year of an order placed at the turn of the year; does "half up" also apply to negative refund amounts; which zone applies when the customer travels.
Frequently asked questions
Should every date be stored in UTC?
Store instants (when something happened or will happen) in UTC or with an explicit offset, and convert for display. Date-only values such as a date of birth, a due date or a public holiday have no zone and must stay plain dates. For events scheduled in local time far ahead, keep the local time with the IANA zone identifier, because offset rules can change before the event.
Is the time zone part of localisation testing?
It is tested together with it, but it is not a locale property. A hi-IN user can sit in Riyadh and an en-GB user in Kolkata. Treat locale and zone as two independent test conditions and combine them deliberately, the way you combine browser and operating system.
Why does my assertion fail when the amount on screen looks right?
Formatted strings contain characters you cannot see: right-to-left marks (U+200F) around Arabic amounts, a no-break space (U+00A0) before a symbol, a narrow no-break space (U+202F) as the French grouping separator. Generate the expected string with the same formatting API and locale, or normalise both sides deliberately and say so in the test.
Does ar-SA always mean Arabic-Indic digits and the Hijri calendar?
No. The defaults come from the CLDR data in each runtime and have changed between versions; Node.js 22 and current Chromium, for example, give ar-SA Arabic-Indic digits with the Gregorian calendar. A product should request what it wants explicitly (-u-nu-latn, -u-ca-islamic-umalqura), and you should test what the product configured, on every runtime it ships.
References
- Unicode CLDR ProjectThe Common Locale Data Repository: the locale data for date, time, number and currency formats and plural rules that most platforms use.
- Time Zone DatabaseThe tz database of the world's time zones and their historical offset and daylight-saving rules, updated several times a year.
- ISO 4217 — Currency codesThe three-letter currency codes (SAR, INR, USD…) and their minor units — the reference for currency fields and formatting tests.
- RFC 3339: Date and Time on the Internet: TimestampsThe Internet timestamp profile of ISO 8601 (2026-09-23T10:15:00Z) and its offset rules.
Practise with Mutqan tools
Convert a date and time between timezones (Riyadh, Amman, Delhi, UTC, New York, London…).
Parse and generate ISO 8601 date-time strings, durations and intervals.
Convert timestamps (seconds, milliseconds, microseconds) to human dates and back, in any timezone.
Generate date datasets: random dates in a range, leap days, month ends, DST edges and multiple formats.
Generate decimal / floating-point datasets with fixed precision, currency-style amounts and rounding edge cases.
Generate Arabic names, addresses, sentences, numbers (Arabic-Indic digits) and RTL edge cases for localisation testing.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.