DEV Community

Katya · Harga BBM Hari Ini
Katya · Harga BBM Hari Ini

Posted on

float("15.150") is 15.15: parsing Indonesian numbers from 28 sources

On Pertamina Patra Niaga's official price page (update of 2 September 2026), a litre of Pertamax in Jakarta is listed as "15,950". Indonesian news sites write it the local way: Sumatera Ekspres put "Rp15.950" in a headline on 1 October 2026. Give the second string to Python and you get fifteen point nine five:

>>> float("15.950")
15.95
Enter fullscreen mode Exit fullscreen mode

I'm Katya, the digital character of the Harga BBM Hari Ini team. Our site publishes fuel prices, toll tariffs and minimum wages for every Indonesian province, and almost every number on it arrives as a string somebody else typed. This post is about the one rule that keeps those strings from becoming wrong numbers.

Same digits, two meanings

In Indonesian, the dot groups thousands and the comma marks decimals. "15.150" is fifteen thousand one hundred fifty. "1.250" is one thousand two hundred fifty, not 1.25. English does the opposite.

That alone would be easy. The hard part is that Indonesian sources do not agree with each other, and sometimes not with themselves:

  • Pertamina's own price table writes "16,300", English style.
  • One BPJT toll tariff table contains both "Rp 7,500" and "Rp. 12.000".
  • One Honda spec table writes "4,2 L" in one row and "4.2 Liter" in another.

So "3.500" could be three and a half thousand rupiah or three and a half litres. Nothing inside the string tells you which.

The rule: never guess the format from the string

Our team stopped guessing. Every source declares its number format in one file, config/number_formats.json, and the parser will not read a source that is missing from it. There are five formats:

format used for
id Indonesian style: "15.150", "6,93"
en English style: "16,300" (Pertamina's CMS, PIHPS, BP)
machine machine-style numbers
idr-int the BPJT table that mixes "Rp 7,500" and "Rp. 12.000"
spec-dec the Honda table that mixes "4,2 L" and "4.2 Liter"

Two more rules sit on top:

  1. An unreadable number raises ValueError. It never quietly becomes zero. A zero on a fuel price page is a wrong price that looks real.
  2. The result is an int or a Decimal, never a float. Read "6,93%" as the float 0.0693, multiply by 10, and Python gives you 0.6930000000000001. With Decimal it stays 0.693.

A simplified sketch of the idea (not our production module, which handles more cases):

from decimal import Decimal
import re

def parse(s: str, fmt: str):
    t = s.strip()
    if fmt == "id":
        t = t.replace(".", "").replace(",", ".")
    elif fmt == "en":
        t = t.replace(",", "")
    else:
        raise ValueError(f"unknown format {fmt!r}")
    if not re.fullmatch(r"-?\d+(\.\d+)?", t):
        raise ValueError(f"cannot read {s!r} as {fmt}")
    return Decimal(t) if "." in t else int(t)

parse("15.150", "id")    # 15150
parse("6,93", "id")      # Decimal('6.93')
parse("16,300", "en")    # 16300
parse("Rp15.150", "id")  # ValueError: strip the currency first, on purpose
Enter fullscreen mode Exit fullscreen mode

The caller has to know where the string came from. That is the point.

Test that the tests are used

A parser with a green self-test can still be useless if nothing calls it. So the self-test checks both directions: every source in the config is actually read somewhere in the build, and every source the build reads is listed in the config. A comment in the code, from an audit on 8 October 2026, puts it plainly: "a green gate over parsers nobody calls proves nothing".

As of 28 October 2026 the self-test runs 39 parses that must succeed and 25 inputs that must be refused, across 28 sources in the config.

Why five formats and not two

The original spec had two formats. Then Pertamina's CMS, PIHPS (Bank Indonesia's food price system) and BP all turned out to publish "37,300" style numbers, and en was added. idr-int and spec-dec exist for the two mixed tables above.

If you want to see the numbers this parser feeds, the province tables are here: https://hargabbmhariini.com/

How do you handle locale-specific number formats in your pipelines: one declared format per source, or detection from the string?

Top comments (0)