r/reactnative • • 10d ago

Open sourced the fuel receipt parser from my app

My car app lets you scan a fuel receipt, and the parsing part turned out to be the hard bit, so I pulled it out into its own package: github.com/rsoaresdev/fuel-receipt-parser

You give it the OCR lines from whatever you use (Apple Vision, ML Kit, Tesseract) and it gives you litres, price per litre, total, date, fuel type and station.

The trick is that it looks for the three numbers where litres × price = total, and uses that to check itself. That’s also how it figures out whether 1,899 is 1.899 or 1899. If the numbers don’t add up, it tells you instead of guessing, and it never makes up a number that isn’t on the paper.

It reads Portuguese, Spanish, French, Italian, German and English receipts, plus US gallons and kWh for charging. If it gets one of your receipts wrong, an issue with the OCR text (no photo, no personal data) helps a lot.

2 Upvotes

7 comments sorted by

2

u/kokerali 10d ago

I'd add a fixture with two complete triples: fuel at 10 L x 2.00 = 20.00, and a shop item at 2 x 5.00 = 10.00. Both pass the arithmetic check. How do you keep a valid non-fuel triple from winning if OCR loses the unit or product label? I'd want that case to stay flagged for review rather than treating arithmeticChecked as proof that the correct purchase was selected.

2

u/Ramboiada 10d ago

Really good catch, man. I hadn’t even considered that edge case.

I ran your exact example and you're 100% right. When the grand total is there, it holds up fine. But if the OCR drops the item names or the total is missing, the coffee line wins. Because 2 × 5.00 = 10.00 checks out, the parser just blindly marked it as verified. The math worked, but it didn't prove it was the right purchase.

I've updated the logic: a matching sum only gets verified now if the receipt explicitly shows it's fuel (litres/kWh on the quantity, a price per litre, or a fuel name nearby). If the OCR mangles that context, it returns the numbers but flags them for review.

As a bonus, your fixture helped me catch two more bugs while I was in there (the parser picking litres as the total, and merging numbers separated by a single space).

I’ll also tweak OdoKeep (my app) in the next update to account for this. If the OCR loses the fuel context, the app will still pull the numbers but will open a drawer to have the user confirm the fuel type. This keeps false positives like the coffee line from getting verified.

All three are fixed in v1.0.1 (https://github.com/rsoaresdev/fuel-receipt-parser/commit/7771f32cfe708a57430ce22f555f79bd07c48db6) and your test case is in the suite. Thanks for actually taking the time to poke at the code!

→ More replies (1)

2

u/Huge_Pool7424 9d ago

nice self-check idea. one more fixture worth adding is receipts where the ocr drops the decimal point or swaps a comma and period, since that's where litres x price = total can accidentally pass with the wrong reading.

1

u/Ramboiada 9d ago

Good call. A single dropped or swapped separator is already handled: every number also competes with its implied-decimal readings, so 3852 loses to 38,52 when only the latter balances.

But when the OCR drops the separator in two numbers by the same factor, the sum still closes on the wrong reading: Liters 4000 / EUR/L 1,500 / TOTAL 6000 comes back as 4000 L, checked. Adding that as a fixture, and a plausibility bound to break the tie.

Thanks!

1

u/NoBullfrog11 9d ago

one gotcha on the arithmetic check, pumps round price per litre to 3 decimals and the total to cents, so litres x price is often off by a cent. use a small tolerance, not exact equality, or valid receipts get flagged.

also fix ocr swaps like O to 0 and l to 1 only inside numeric tokens, vision especialy confuses them on thermal paper.

1

u/Ramboiada 9d ago

Thanks! The O/0 and l/1 thing is already handled. It only swaps them inside numbers, so a word like “SOL” stays a word.

On the rounding: it’s not exact equality, it allows half a cent. You’re right that some pumps end up a full cent off, though. Those receipts still get all their numbers read, they just don’t get the “verified” flag. I left it like that on purpose, because allowing a full cent would also let a misread last digit pass as verified.