first commit
This commit is contained in:
commit
e630f2a3ec
80 changed files with 3889 additions and 0 deletions
105
lib/services/receipt_parser.dart
Normal file
105
lib/services/receipt_parser.dart
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/// Best-effort values pulled out of OCR'd receipt text. Any field can be
|
||||
/// null if it couldn't be found, and the confirm screen lets the user fill
|
||||
/// in or correct whatever the parser got wrong.
|
||||
class ParsedReceipt {
|
||||
final double? gallons;
|
||||
final double? pricePerGallon;
|
||||
final double? totalCost;
|
||||
final String rawText;
|
||||
|
||||
ParsedReceipt({
|
||||
this.gallons,
|
||||
this.pricePerGallon,
|
||||
this.totalCost,
|
||||
required this.rawText,
|
||||
});
|
||||
}
|
||||
|
||||
/// Parses free-form OCR text from a fuel receipt looking for the number of
|
||||
/// gallons pumped, the price per gallon, and the total amount charged.
|
||||
///
|
||||
/// Fuel receipts vary a lot between stations, so this uses a handful of
|
||||
/// label-based regexes rather than assuming a fixed layout, and falls back
|
||||
/// to deriving a missing value from the other two when exactly one is
|
||||
/// missing (total = gallons * price, etc).
|
||||
class ReceiptParser {
|
||||
static final _gallonsPatterns = [
|
||||
RegExp(r'GALLONS?\s*[:\-]?\s*(\d+\.\d{2,3})', caseSensitive: false),
|
||||
RegExp(r'\bGAL\b\s*[:\-]?\s*(\d+\.\d{2,3})', caseSensitive: false),
|
||||
RegExp(r'(\d+\.\d{2,3})\s*GAL(?:LONS)?\b', caseSensitive: false),
|
||||
];
|
||||
|
||||
static final _pricePerGallonPatterns = [
|
||||
RegExp(r'PRICE\s*/?\s*GAL(?:LON)?\s*[:\-]?\s*\$?\s*(\d+\.\d{2,3})',
|
||||
caseSensitive: false),
|
||||
RegExp(r'\bPPG\b\s*[:\-]?\s*\$?\s*(\d+\.\d{2,3})', caseSensitive: false),
|
||||
RegExp(r'PER\s*GAL(?:LON)?\s*[:\-]?\s*\$?\s*(\d+\.\d{2,3})',
|
||||
caseSensitive: false),
|
||||
RegExp(r'\$\s*/\s*GAL\s*[:\-]?\s*\$?\s*(\d+\.\d{2,3})',
|
||||
caseSensitive: false),
|
||||
];
|
||||
|
||||
static final _totalPatterns = [
|
||||
RegExp(r'FUEL\s*TOTAL\s*[:\-]?\s*\$?\s*(\d+\.\d{2})', caseSensitive: false),
|
||||
RegExp(r'SALE\s*TOTAL\s*[:\-]?\s*\$?\s*(\d+\.\d{2})', caseSensitive: false),
|
||||
RegExp(r'AMOUNT\s*DUE\s*[:\-]?\s*\$?\s*(\d+\.\d{2})', caseSensitive: false),
|
||||
RegExp(r'(?<!SUB)\bTOTAL\b\s*[:\-]?\s*\$?\s*(\d+\.\d{2})',
|
||||
caseSensitive: false),
|
||||
];
|
||||
|
||||
static ParsedReceipt parse(String text) {
|
||||
final normalized = text.replaceAll(',', '');
|
||||
|
||||
final gallons = _firstMatch(_gallonsPatterns, normalized);
|
||||
final pricePerGallon = _firstMatch(_pricePerGallonPatterns, normalized);
|
||||
var totalCost = _firstMatch(_totalPatterns, normalized);
|
||||
|
||||
final derivedTotal = _deriveMissingValue(
|
||||
gallons: gallons,
|
||||
pricePerGallon: pricePerGallon,
|
||||
totalCost: totalCost,
|
||||
);
|
||||
|
||||
return ParsedReceipt(
|
||||
gallons: derivedTotal.gallons,
|
||||
pricePerGallon: derivedTotal.pricePerGallon,
|
||||
totalCost: derivedTotal.totalCost,
|
||||
rawText: text,
|
||||
);
|
||||
}
|
||||
|
||||
static double? _firstMatch(List<RegExp> patterns, String text) {
|
||||
for (final pattern in patterns) {
|
||||
final match = pattern.firstMatch(text);
|
||||
if (match != null) {
|
||||
return double.tryParse(match.group(1)!);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static ({double? gallons, double? pricePerGallon, double? totalCost})
|
||||
_deriveMissingValue({
|
||||
required double? gallons,
|
||||
required double? pricePerGallon,
|
||||
required double? totalCost,
|
||||
}) {
|
||||
final missingCount =
|
||||
[gallons, pricePerGallon, totalCost].where((v) => v == null).length;
|
||||
|
||||
// Only safe to derive when exactly one of the three is missing.
|
||||
if (missingCount != 1) {
|
||||
return (gallons: gallons, pricePerGallon: pricePerGallon, totalCost: totalCost);
|
||||
}
|
||||
|
||||
if (totalCost == null && gallons != null && pricePerGallon != null) {
|
||||
totalCost = double.parse((gallons * pricePerGallon).toStringAsFixed(2));
|
||||
} else if (gallons == null && totalCost != null && pricePerGallon != null && pricePerGallon > 0) {
|
||||
gallons = double.parse((totalCost / pricePerGallon).toStringAsFixed(3));
|
||||
} else if (pricePerGallon == null && totalCost != null && gallons != null && gallons > 0) {
|
||||
pricePerGallon = double.parse((totalCost / gallons).toStringAsFixed(3));
|
||||
}
|
||||
|
||||
return (gallons: gallons, pricePerGallon: pricePerGallon, totalCost: totalCost);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue