FOSSredder 0.5.0
Loading...
Searching...
No Matches
DefaultTransactionParser.cpp
Go to the documentation of this file.
1
5
9
10#include <algorithm>
11#include <regex>
12#include <string>
13
17
21namespace helpers = core::application::importing::internal;
22namespace amount_parser = core::application::importing::transaction;
23
24namespace {
25
26bool tightContinuation(const OcrLine& prev, const OcrLine& next) {
27 const bool closeY = (next.minY - prev.maxY) <= 18;
28 const bool leftAligned = std::abs(next.minX - prev.minX) <= 14;
29 return closeY && leftAligned;
30}
31
32bool hasSingleTokenBeforeColon(const std::string& s, size_t colonPos) {
33 if (colonPos == std::string::npos || colonPos == 0) return false;
34 const auto left = trim(s.substr(0, colonPos));
35 if (left.empty()) return false;
36 return splitWhitespace(left).size() == 1;
37}
38
39bool looksLikeLabelStart(const std::string& line) {
40 const auto pos = line.find(':');
41 if (pos == std::string::npos || pos == 0 || pos > 40) return false;
42 return hasSingleTokenBeforeColon(line, pos);
43}
44
45std::optional<std::string> parseValutaToken(const std::string& line) {
46 static const std::regex reJoined(R"((\d{2}\.\d{2}))");
47 static const std::regex reSplit(R"((\d{2})\.)");
48 std::smatch m;
49
50 if (std::regex_search(line, m, reJoined)) return m.str(1);
51
52 {
53 std::smatch md;
54 if (std::regex_search(line, md, reSplit)) {
55 const auto day = md.str(1);
56 const auto rest = md.suffix().str();
57 static const std::regex reMonth(R"(\b(\d{2})\b)");
58 std::smatch mm;
59 if (std::regex_search(rest, mm, reMonth)) {
60 return day + "." + mm.str(1);
61 }
62 }
63 }
64
65 return std::nullopt;
66}
67
68}
69
70int textRightEdge(const OcrLine& l) {
71 if (!l.wordSpans.empty()) return l.wordSpans.back().second;
72 return l.maxX;
73}
74
78
79 if (!block.main.valuta.empty()) {
80 if (auto v = parseValutaToken(block.main.valuta.line.text)) tx.valuta = *v;
81 }
82
83 bool sourceDebit = false;
84 bool sourceCredit = false;
85
86 auto tokenIndicatesNegative = [&](const std::string &txt)->bool{
87 std::string s = txt;
88 try { s = core::utils::trim(s); } catch (...) { }
89 if (s.empty()) return false;
90 if (s.front() == '(' && s.back() == ')') return true;
91 if (s.front() == '-') return true;
92 if (s.back() == '-') return true;
93 return false;
94 };
95
96 auto centerXOf = [&](const OcrLine& l)->int{
97 if (!l.wordSpans.empty()) return (l.wordSpans.front().first + l.wordSpans.back().second) / 2;
98 return (l.minX + l.maxX) / 2;
99 };
100
101 std::optional<double> creditVal;
102 std::optional<double> debitVal;
103 bool creditExplicitNeg = false;
104 bool debitExplicitNeg = false;
105
106 if (!block.main.credit.empty()) {
107 creditExplicitNeg = tokenIndicatesNegative(block.main.credit.line.text);
108 if (auto v = helpers::findAndParseAmountInLine(block.main.credit.line, -1, debugOut)) {
109 creditVal = v;
110 if (debugOut) debugOut->push_back(std::string("initial.credit.helperUsed\t") + block.main.credit.line.text + std::string(" -> ") + std::to_string(*creditVal));
111 } else {
112 creditVal = amount_parser::parseAmountString(block.main.credit.line.text);
113 if (debugOut) debugOut->push_back(std::string("initial.credit.parse\t") + block.main.credit.line.text + std::string(" -> ") + (creditVal ? std::to_string(*creditVal) : std::string("(none)")));
114 }
115 }
116 if (!block.main.debit.empty()) {
117 debitExplicitNeg = tokenIndicatesNegative(block.main.debit.line.text);
118 if (auto v = helpers::findAndParseAmountInLine(block.main.debit.line, -1, debugOut)) {
119 debitVal = v;
120 if (debugOut) debugOut->push_back(std::string("initial.debit.helperUsed\t") + block.main.debit.line.text + std::string(" -> ") + std::to_string(*debitVal));
121 } else {
122 debitVal = amount_parser::parseAmountString(block.main.debit.line.text);
123 if (debugOut) debugOut->push_back(std::string("initial.debit.parse\t") + block.main.debit.line.text + std::string(" -> ") + (debitVal ? std::to_string(*debitVal) : std::string("(none)")));
124 }
125 }
126
127 try {
128 const double suspiciousThreshold = 1.0;
129 bool needFallback = (!creditVal && !debitVal);
130 if (!needFallback) {
131 if (creditVal && std::abs(*creditVal) < suspiciousThreshold) needFallback = true;
132 if (debitVal && std::abs(*debitVal) < suspiciousThreshold) needFallback = true;
133 }
134 if (needFallback) {
135 if (debugOut) debugOut->push_back(std::string("recovery.scan.start\tcreditVal=") + (creditVal?"1":"0") + std::string(" debitVal=") + (debitVal?"1":"0"));
136 if (!block.main.credit.empty()) {
137 if (auto v = helpers::findAndParseAmountInLine(block.main.credit.line, -1, debugOut)) { creditVal = v; if (debugOut) debugOut->push_back(std::string("recovery.credit.helper\t") + block.main.credit.line.text + std::string(" -> ") + std::to_string(*v)); }
138 }
139 if (!creditVal && !block.main.debit.empty()) {
140 if (auto v = helpers::findAndParseAmountInLine(block.main.debit.line, -1, debugOut)) { debitVal = v; if (debugOut) debugOut->push_back(std::string("recovery.debit.helper\t") + block.main.debit.line.text + std::string(" -> ") + std::to_string(*v)); }
141 }
142 if (!creditVal && !debitVal) {
143 if (!block.main.left.empty()) {
144 if (auto v = helpers::findAndParseAmountInLine(block.main.left.line, -1, debugOut)) {
145 creditVal = v;
146 if (debugOut) debugOut->push_back(std::string("recovery.left.helper\t") + block.main.left.line.text + std::string(" -> ") + std::to_string(*v));
147 }
148 }
149 for (const auto& dl : block.detailLines) {
150 if (creditVal || debitVal) break;
151 if (auto v = helpers::findAndParseAmountInLine(dl, -1, debugOut)) { creditVal = v; if (debugOut) debugOut->push_back(std::string("recovery.detail.helper\t") + dl.text + std::string(" -> ") + std::to_string(*v)); break; }
152 }
153 }
154 }
155 } catch (...) { }
156
157 if (creditVal && debitVal) {
158 if (creditExplicitNeg && !debitExplicitNeg) {
159 tx.amount = *creditVal;
160 if (debugOut) debugOut->push_back(std::string("resolve.both\tchoose.creditNeg->") + std::to_string(tx.amount));
161 } else if (debitExplicitNeg && !creditExplicitNeg) {
162 tx.amount = *debitVal;
163 if (debugOut) debugOut->push_back(std::string("resolve.both\tchoose.debitNeg->") + std::to_string(tx.amount));
164 } else {
165 if (std::abs(*debitVal) >= std::abs(*creditVal)) tx.amount = *debitVal; else tx.amount = *creditVal;
166 if (debugOut) debugOut->push_back(std::string("resolve.both\tchoose.larger->") + std::to_string(tx.amount));
167 }
168 } else if (creditVal) {
169 tx.amount = *creditVal;
170 if (debugOut) debugOut->push_back(std::string("resolve.credit->") + std::to_string(tx.amount));
171 } else if (debitVal) {
172 bool treatAsCredit = false;
173 if (!block.main.credit.empty()) {
174 try {
175 int cxDebit = centerXOf(block.main.debit.line);
176 int cxCredit = centerXOf(block.main.credit.line);
177 if (cxDebit > cxCredit) treatAsCredit = true;
178 } catch (...) { }
179 }
180 if (tokenIndicatesNegative(block.main.debit.line.text)) {
181 tx.amount = *debitVal;
182 if (debugOut) debugOut->push_back(std::string("resolve.debitExpNeg->") + std::to_string(tx.amount));
183 } else if (treatAsCredit) {
184 tx.amount = *debitVal;
185 if (debugOut) debugOut->push_back(std::string("resolve.debit.treatAsCredit->") + std::to_string(tx.amount));
186 } else {
187 tx.amount = -std::abs(*debitVal);
188 if (debugOut) debugOut->push_back(std::string("resolve.debit.defaultNegative->") + std::to_string(tx.amount));
189 }
190 }
191
192 if (tx.amount == 0.0 && !block.main.debit.empty() && !block.main.credit.empty()) {
193 auto tryCombineParse = [&](const std::string& a, const std::string& b)->std::optional<double> {
194 try {
195 if (a.empty() || b.empty()) return std::nullopt;
196 if (auto p = amount_parser::parseAmountString(a + b)) return p;
197 if (auto p2 = amount_parser::parseAmountString(a + " " + b)) return p2;
198 } catch (...) { }
199 return std::nullopt;
200 };
201 if (auto p = tryCombineParse(block.main.debit.line.text, block.main.credit.line.text)) {
202 bool creditRightOfDebit = false;
203 try { creditRightOfDebit = centerXOf(block.main.credit.line) > centerXOf(block.main.debit.line); } catch (...) { }
204 if (creditRightOfDebit) tx.amount = *p; else tx.amount = -std::abs(*p);
205 if (debugOut) debugOut->push_back(std::string("combine.debit+credit->") + std::to_string(tx.amount));
206 } else if (auto p = tryCombineParse(block.main.credit.line.text, block.main.debit.line.text)) {
207 tx.amount = *p;
208 if (debugOut) debugOut->push_back(std::string("combine.credit+debit->") + std::to_string(tx.amount));
209 }
210 }
211
212 try {
213 auto hasNegMarker = [&](const std::string &t)->bool{
214 if (t.empty()) return false;
215 for (char c : t) if (c == '-' || c == '(' || c == ')') return true;
216 return false;
217 };
218 if (tx.amount > 0.0 && (hasNegMarker(block.main.debit.line.text) || hasNegMarker(block.main.credit.line.text))) {
219 tx.amount = -std::abs(tx.amount);
220 if (debugOut) debugOut->push_back(std::string("enforce.negMarker->") + std::to_string(tx.amount));
221 }
222 } catch (...) { }
223
224 try {
225 if (sourceDebit && sourceCredit) {
226 } else if (sourceDebit) {
227 if (tx.amount > 0.0) tx.amount = -std::abs(tx.amount);
228 } else if (sourceCredit) {
229 if (tx.amount < 0.0) tx.amount = std::abs(tx.amount);
230 }
231 } catch (...) { }
232
233 std::vector<OcrLine> lines;
234 if (!block.main.left.empty()) lines.push_back(block.main.left.line);
235 for (const auto& l : block.detailLines) {
236 if (!trim(l.text).empty()) lines.push_back(l);
237 }
238 if (lines.empty()) return tx;
239
240 int rightEdge = 0;
241 for (const auto& l : lines) rightEdge = std::max(rightEdge, textRightEdge(l));
242
243 std::vector<std::string> nameLines;
244 nameLines.push_back(collapseWhitespace(lines.front().text));
245
246 for (size_t i = 1; i < lines.size(); ++i) {
247 const auto& prev = lines[i - 1];
248 const auto& cur = lines[i];
249
250 const int prevRight = textRightEdge(prev);
251 const int gap = std::max(0, rightEdge - prevRight);
252 const int w = std::max(1, rightEdge - lines.front().minX);
253
254 const int allowAbs = std::max(24, w / 40);
255 const int allowRel = w / 25;
256 const int allow = std::min(std::max(1, allowAbs), std::max(1, allowRel));
257
258 if (gap > allow) break;
259 if (!tightContinuation(prev, cur)) break;
260
261 nameLines.push_back(collapseWhitespace(cur.text));
262 }
263
264 std::string nm;
265 for (const auto& l : nameLines) nm += l;
266 tx.name = collapseWhitespace(std::move(nm));
267 if (tx.name.size() > 120) tx.name.resize(120);
268
269 std::string meta;
270 bool lastWasLabel = false;
271 for (size_t i = 0; i < lines.size(); ++i) {
272 std::string t = collapseWhitespace(lines[i].text);
273 t = trim(std::move(t));
274 if (t.empty()) continue;
275 const bool isLabel = looksLikeLabelStart(t);
276
277 if (i == nameLines.size()) {
278 meta.push_back('\n');
279 } else {
280 if (!meta.empty()) {
281 if (isLabel || lastWasLabel) meta.push_back('\n');
282 }
283 }
284
285 meta += t;
286 lastWasLabel = isLabel;
287 }
288 tx.metadata = trim(std::move(meta));
289
290 return tx;
291}
292
293}
std::vector< std::string > splitWhitespace(const std::string &s)
Splits a string on ASCII whitespace.
std::string collapseWhitespace(std::string s)
Collapses consecutive whitespace to single spaces and trims the result.
Declares the default transaction parser used by the import pipeline.
Declares low-level OCR helper routines for statement parsing.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
Declares private string helpers shared across core implementation files.
std::vector< std::string > splitWhitespace(const std::string &s)
Splits a string on ASCII whitespace.
std::string collapseWhitespace(std::string s)
Collapses consecutive whitespace to single spaces and trims the result.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
Represents the parsed fields of a single transaction.
static DefaultTransactionParser parseTransaction(const TransactionBlock &block, std::vector< std::string > *debugOut=nullptr)
Parses a transaction block into a structured transaction model.
Represents one OCR line with its bounding box and recognized text.
Groups the main row and detail rows of one parsed transaction block.