From 92dd867a48a8bc6930f151109eae60523e799f39 Mon Sep 17 00:00:00 2001 From: Rafael Soares <107703258+rsoaresdev@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:19:26 +0000 Subject: [PATCH] fix: read unmarked pence, $ after @, LTR rows and fuel amounts beside a car wash - `11.482 G @ $3.299`: a currency mark between @ and the price no longer loses the price. - `40.00L @ 169.9`: pence or cents with no mark are read as 1.699 when that closes the sum. - `30.00 LTR 155.9 P/LTR`: the quantity is no longer swallowed as a date. - `PPL 155.9`: a pence mark heading the price is read as the price per litre. - `FUEL 77.98` beside `CAR WASH 8.00`: the fuel's own amount checks the fuel when the total does not. --- CHANGELOG.md | 10 ++++++ src/extract.ts | 24 +++++++++++--- src/numbers.ts | 37 ++++++++++++++++----- src/patterns.ts | 3 +- test/world.test.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 142 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c1735..9e07887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this package are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions follow [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Fixed + +- `11.482 G @ $3.299`, with a currency mark between the `@` and the price, is read as the price per gallon. +- A price in pence or cents printed with no mark after `@` (`40.00L @ 169.9`, in Australia, Canada and the UK) is read as 1.699 when that closes the sum, and kept as printed when it does not. +- `30.00 LTR 155.9 P/LTR` keeps its quantity. The `00 LTR 155` in it was being read as a date. +- `PPL 155.9` and `P/LTR 155.9`, with the pence mark heading the price, are read as the price per litre. +- On a receipt whose total includes a car wash or a snack, the amount labelled as the fuel's (`FUEL 77.99`, or a fuel name) checks the fuel when the total does not. + ## [1.1.0] - 2026-10-31 ### Added diff --git a/src/extract.ts b/src/extract.ts index c2f0375..cbc02e5 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -70,6 +70,18 @@ const LABEL_REACH = 64; * The label nearest a number: what precedes it, else the column heading above * it, else the start of its line. */ +/** + * The amount for the fuel alone, on a receipt that also sells a car wash or a + * snack: `FUEL 77.99`, `UNLEADED 77.99` above `TOTAL 85.99`. + */ +function namesFuelAmount(label: string): boolean { + return ( + /(? 0) { diff --git a/src/numbers.ts b/src/numbers.ts index d5a8d8c..e0f24e7 100644 --- a/src/numbers.ts +++ b/src/numbers.ts @@ -127,12 +127,20 @@ const SUBUNIT_RATE = new RegExp( `^\\s*(?:p|c|ct|cts|¢)(?:\\s*\\/\\s*(${LITRE}|${GALLON})|(pl))?(?![a-z0-9])`, "i", ); +/** The same mark heading its value: `PPL 155.9`, `P/LTR 155.9`. */ +const SUBUNIT_BEFORE = new RegExp( + `(? Math.round(value * 1e4) / 1e6) - : values, - implied, + : unmarkedHundredths + ? [values[0], Math.round(values[0] * 1e4) / 1e6] + : values, + implied: unmarkedHundredths ? 1 : implied, lineIndex, box: boxForRange(line, from, to), before: before.toLowerCase(), after: after.toLowerCase(), currency: lineCurrency, - unit: hundredths - ? rateKind(hundredths[1] ?? "l") - : unitFor(before, after), + unit, repaired, partOfDateTime: overlapsAny(stamps, from, to), glued: /[a-zà-ÿ]$/i.test(before) || /^[a-zà-ÿ]/i.test(after), diff --git a/src/patterns.ts b/src/patterns.ts index a895e1e..6684372 100644 --- a/src/patterns.ts +++ b/src/patterns.ts @@ -25,7 +25,8 @@ export const DATE_PATTERNS: readonly RegExp[] = [ // 01-08-2026, 01/08/26, 1.8.2026 /\b(\d{1,2})[-/.](\d{1,2})[-/.](\d{2,4})\b/g, // 01 AGO 2026, 1 Aug 26, 01-SEP-2026 - /\b(\d{1,2})\.?[\s-]([a-zà-ÿ]{3,12})\.?[\s-](\d{2,4})\b/gi, + // (never the decimals of a number: `30.00 LTR 155.9` is no date) + /(? { ); expect(r.unitPrice?.value).toBe(1.499); }); + + it("reads pence or cents printed with no mark after @", () => { + for (const [country, mark] of [ + ["AU", "$"], + ["CA", "$"], + ["GB", "£"], + ]) { + const r = read(["Ampol", "E10", "40.00L @ 169.9", `TOTAL ${mark}67.96`], { + country, + }); + expect(summary(r)).toEqual({ + quantity: 40, + unitPrice: 1.699, + total: 67.96, + checked: true, + }); + } + }); + + it("keeps an unmarked price as printed when hundredths do not close", () => { + const r = read(["40.00L @ 169.9", "TOTAL £50.00"], { country: "GB" }); + expect(r.unitPrice?.value).toBe(169.9); + expect(r.arithmeticChecked).toBe(false); + }); + + it("reads LTR and P/LTR on one line", () => { + const r = read( + ["BP", "UNLEADED", "30.00 LTR 155.9 P/LTR", "TOTAL £46.77"], + { + country: "GB", + }, + ); + expect(summary(r)).toEqual({ + quantity: 30, + unitPrice: 1.559, + total: 46.77, + checked: true, + }); + expect(r.date).toBeNull(); + }); + + it("reads a PPL heading before the price", () => { + const r = read( + ["BP", "UNLEADED", "LITRES 30.00", "PPL 155.9", "TOTAL £46.77"], + { + country: "GB", + }, + ); + expect(summary(r)).toEqual({ + quantity: 30, + unitPrice: 1.559, + total: 46.77, + checked: true, + }); + }); }); describe("a US pump", () => { + it("reads a $ between @ and the price", () => { + const r = read(["Shell", "UNLEADED", "11.482 G @ $3.299", "TOTAL $37.88"], { + country: "US", + }); + expect(summary(r)).toEqual({ + quantity: 11.482, + unitPrice: 3.299, + total: 37.88, + checked: true, + }); + }); + + it("checks the fuel amount when the total includes a car wash or a snack", () => { + for (const extra of ["CAR WASH 8.00", "SNACKS 8.00"]) { + const r = read( + ["Shell", "23.640 G @ 3.299", "FUEL 77.99", extra, "TOTAL $85.99"], + { country: "US" }, + ); + expect(summary(r)).toEqual({ + quantity: 23.64, + unitPrice: 3.299, + total: 77.99, + checked: true, + }); + } + }); + it("joins the tenth of a cent printed small", () => { for (const price of ["PRICE/GAL $3.45 9", "PRICE/GAL $3.45⁹"]) { const r = read(["Shell", "GALLONS 12.345", price, "FUEL TOTAL $42.70"], {