Every Western grouping convention differs only in which character separates groups of three. The Indian system differs in how big the groups are, and that is a different kind of change — one that a find-and-replace cannot fix and a model cannot pattern-match its way to.
The grouping rule
Start from the right of the integer part. The first group is three digits. Every group after that is two digits. So the separator positions counted from the decimal point are after 3, 5, 7, 9 digits and so on, rather than after 3, 6, 9, 12.
That is the entire rule, and it is worth stating in that bare form because most explanations bury it under vocabulary. The words lakh and crore are consequences of the rule rather than the rule itself: a lakh is 105, which is where the first two-digit group ends, and a crore is 107, where the second does. The system keeps going — 109 is an arab and 1011 a kharab — but in ordinary Indian English usage crore is the largest unit in regular circulation, and larger amounts are written as multiples of it (“1,000 crore” rather than one arab).
One number, both systems
Take the integer 123456789. In the international system it groups as:
international 123,456,789
^ ^
separators after 3 and 6 digits
Indian 12,34,56,789
^ ^ ^
separators after 3, 5 and 7 digits
The two agree on the rightmost separator and on nothing after that. This is the important observation for debugging: a number below 100,000 is written identically in both systems, so a test suite built from small values will pass while every real figure in the application is wrong. The divergence starts at exactly 1,00,000 — one lakh — and it grows. At nine digits the two strings do not share a single separator position beyond the first.
Read the Indian grouping back as words and it decomposes cleanly: 12 crore, 34 lakh, 56 thousand, 789. That is why the group sizes are what they are. Each separator is a real unit boundary in the spoken system, which is the same relationship that commas have to thousand/million/billion in English. Neither system is grouping arbitrarily; they are grouping to match how the number is said.
Lakh and crore as words, not just separators
Indian English writes large amounts in mixed form far more readily than British or American English does. “₹2.5 crore” is ordinary register in a newspaper, a company filing or a property listing, in the way that “$25 million” is — not an informal shorthand for a proper figure written elsewhere. A generated Indian English text that renders every amount as a full digit string is not wrong, exactly, but it reads as translated-from-American, which is one of the more reliable tells that a page was not written for its audience.
Two details matter if you generate these forms. First, the unit words are not pluralised with -s in standard usage: “5 crore”, not “5 crores”, though the plural does appear in some registers. Second, the abbreviations are worth knowing because they turn up in source documents you may be parsing: Rs or the rupee sign ₹ for currency, and L or lac for lakh in informal writing, especially in classified advertising. A parser that does not know lac will silently drop the multiplier.
Where generated output breaks
The specific failure is a hybrid. A model asked for an amount in Indian English will very often produce the right words and the wrong digits — “the project cost ₹123,456,789, or about 12.3 crore”. The vocabulary is learned from Indian English text, of which there is a lot; the grouping is learned from the overwhelming mass of digit strings in the training data, which are grouped in threes. Those are two different signals and the stronger one wins on the digits.
A related error is arithmetic rather than formatting: converting between the systems and dropping a factor of ten, because 1 crore is 10 million and it is very easy to write 100 million. Any generated text that gives both a digit string and a crore figure should have the two checked against each other rather than trusted individually; when they disagree it is usually the conversion, not the grouping, that is wrong.
The reading direction has its own failure. Parsing an Indian-formatted string with code that assumes uniform three-digit grouping does not throw; it produces a wrong number. A routine that strips separators and then re-groups in threes to “normalise” a figure will turn 12,34,56,789 back into 123,456,789, which is arithmetically identical and now formatted for the wrong audience, and a routine that validates separator positions against a three-digit rule will reject the correct string as malformed. Both are common in code that was written for one market and later pointed at another, and neither is visible until somebody in India looks at the output.
There is also a range question that catches financial software. Two hundred crore is 2 × 109, which exceeds the largest integer a 32-bit signed field can hold, and Indian amounts written in rupees reach that range far more readily than the same real values written in dollars or euros do. A currency amount stored in minor units — paise — reaches it a hundred times sooner. That is not a locale bug, but it is a bug that appears when a system is localised for India and nowhere else, so it belongs on the same checklist.
The same class of bug appears in Chinese large-number formatting, where the grouping unit is 104 — 万 — and the mismatch with three-digit grouping is even sharper, since no separator position lines up at all above 10,000.
Getting it right
Do not ask the model. The Indian grouping is a solved formatting problem with a table behind it, and every ICU-backed formatter has the table. In CLDR the en-IN and hi-IN decimal patterns are #,##,##0.### — note the two comma positions in the pattern itself, which is how the format string expresses a non-uniform grouping — against #,##0.### for locales that group uniformly. The Unicode consortium documents the pattern syntax in UTS #35, Part 3: Numbers.
In practice that means one call in whatever language you are in:
new Intl.NumberFormat("en-IN").format(123456789)
// "12,34,56,789"
new Intl.NumberFormat("en-IN", {
style: "currency", currency: "INR", maximumFractionDigits: 0
}).format(25000000)
// "₹2,50,00,000"
The seam is therefore: the model produces a number, your code produces the string. If you need the crore wording as well, compute it — divide by 107, round to the precision you want, and interpolate the word — rather than letting a generator produce a figure and a wording independently and hoping they agree. That is the same discipline described in getting the decimal comma or point right by locale, and it is the only version of this that stays correct as amounts change.
Top comments (0)