<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Katya · Harga BBM Hari Ini</title>
    <description>The latest articles on DEV Community by Katya · Harga BBM Hari Ini (@hargabbmhariini).</description>
    <link>https://dev.to/hargabbmhariini</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4213419%2F84fae9c2-f654-4e27-a9d0-ac676757e07e.png</url>
      <title>DEV Community: Katya · Harga BBM Hari Ini</title>
      <link>https://dev.to/hargabbmhariini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hargabbmhariini"/>
    <language>en</language>
    <item>
      <title>float("15.150") is 15.15: parsing Indonesian numbers from 28 sources</title>
      <dc:creator>Katya · Harga BBM Hari Ini</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:39:29 +0000</pubDate>
      <link>https://dev.to/hargabbmhariini/float15150-is-1515-parsing-indonesian-numbers-from-28-sources-372o</link>
      <guid>https://dev.to/hargabbmhariini/float15150-is-1515-parsing-indonesian-numbers-from-28-sources-372o</guid>
      <description>&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15.950&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;15.95&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same digits, two meanings
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

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

&lt;p&gt;So "3.500" could be three and a half thousand rupiah or three and a half litres. Nothing inside the string tells you which.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: never guess the format from the string
&lt;/h2&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;format&lt;/th&gt;
&lt;th&gt;used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Indonesian style: "15.150", "6,93"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;English style: "16,300" (Pertamina's CMS, PIHPS, BP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;machine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;machine-style numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idr-int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the BPJT table that mixes "Rp 7,500" and "Rp. 12.000"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spec-dec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the Honda table that mixes "4,2 L" and "4.2 Liter"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two more rules sit on top:&lt;/p&gt;

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

&lt;p&gt;A simplified sketch of the idea (not our production module, which handles more cases):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown format &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-?\d+(\.\d+)?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cannot read &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; as &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15.150&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# 15150
&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6,93&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# Decimal('6.93')
&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16,300&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# 16300
&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rp15.150&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ValueError: strip the currency first, on purpose
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller has to know where the string came from. That is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test that the tests are used
&lt;/h2&gt;

&lt;p&gt;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".&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why five formats and not two
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;en&lt;/code&gt; was added. &lt;code&gt;idr-int&lt;/code&gt; and &lt;code&gt;spec-dec&lt;/code&gt; exist for the two mixed tables above.&lt;/p&gt;

&lt;p&gt;If you want to see the numbers this parser feeds, the province tables are here: &lt;a href="https://hargabbmhariini.com/" rel="noopener noreferrer"&gt;https://hargabbmhariini.com/&lt;/a&gt;&lt;/p&gt;

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

</description>
      <category>python</category>
      <category>data</category>
      <category>i18n</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
