FOSSredder 0.5.0
Loading...
Searching...
No Matches
DraftMatchingPolicy.cpp
Go to the documentation of this file.
1
5
6#include "core/pch.h"
11
13
15
16#include <algorithm>
17#include <cctype>
18#include <cmath>
19#include <limits>
20#include <regex>
21#include <sstream>
22#include <utility>
23
25
26namespace {
27
28template <typename TEntity>
29std::vector<core::domain::Alias> aliasUsages(const TEntity& entity)
30{
31 std::vector<core::domain::Alias> out;
32 out.reserve(entity.aliases().size());
33 for (const auto& alias : entity.aliases()) {
34 if (alias.value().empty()) continue;
35 out.push_back(alias);
36 }
37 return out;
38}
39
40bool containsId(const std::vector<std::string>& ids, const std::string& id)
41{
42 return std::find(ids.begin(), ids.end(), id) != ids.end();
43}
44
45bool appendUnique(std::vector<std::string>& values, const std::string& value)
46{
47 if (value.empty() || containsId(values, value)) return false;
48 values.push_back(value);
49 return true;
50}
51
52} // namespace
53
54std::string trim(std::string value)
55{
56 return core::utils::trim(std::move(value));
57}
58
59bool matchesDraftText(const std::string& left, const std::string& right)
60{
61 return normalizeText(left) == normalizeText(right);
62}
63
64std::vector<std::string> splitLines(const std::string& text)
65{
66 std::vector<std::string> lines;
67 std::string current;
68 for (char c : text) {
69 if (c == '\r') continue;
70 if (c == '\n') {
71 const auto line = trim(current);
72 if (!line.empty()) lines.push_back(line);
73 current.clear();
74 continue;
75 }
76 current.push_back(c);
77 }
78 const auto tail = trim(current);
79 if (!tail.empty()) lines.push_back(tail);
80 return lines;
81}
82
83std::string joinNonEmptyLines(const std::vector<std::string>& values, const std::string& separator)
84{
85 std::ostringstream out;
86 bool first = true;
87 for (const auto& value : values) {
88 const auto trimmedValue = trim(value);
89 if (trimmedValue.empty()) continue;
90 if (!first) out << separator;
91 first = false;
92 out << trimmedValue;
93 }
94 return out.str();
95}
96
97bool lineLooksMetadataBoundary(const std::string& line)
98{
99 const auto value = trim(line);
100 if (value.empty()) return true;
101 if (value.find(':') != std::string::npos) return true;
102
103 static const std::regex amountPattern("(eur|usd|chf|gbp)\\s*[-+]?\\d|\\b\\d+(?:[\\.,]\\d{2})\\b", std::regex::icase);
104 return std::regex_search(value, amountPattern);
105}
106
107bool lineLooksActorStopSignal(const std::string& line)
108{
109 const auto value = trim(line);
110 if (value.empty()) return true;
111 if (lineLooksMetadataBoundary(value)) return true;
112
113 static const std::regex ibanPattern("^[A-Z]{2}\\d{2}[A-Z0-9]{10,32}$", std::regex::icase);
114 static const std::regex bicPattern("^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$", std::regex::icase);
115 static const std::regex dateOnlyPattern("^\\d{1,2}[\\./-]\\d{1,2}[\\./-]\\d{2,4}$", std::regex::icase);
116 static const std::regex bankOrReferencePattern("(end-to-end|kundenreferenz|verwendungszweck|vertragskont|iban|bic|dauerauftrag|mandat|referenz|zweck)", std::regex::icase);
117 return std::regex_match(value, ibanPattern)
118 || std::regex_match(value, bicPattern)
119 || std::regex_match(value, dateOnlyPattern)
120 || std::regex_search(value, bankOrReferencePattern);
121}
122
123bool lineLooksActorContinuation(const std::string& line)
124{
125 const auto value = trim(line);
126 if (value.empty()) return false;
127 if (lineLooksActorStopSignal(value)) return false;
128
129 int digitCount = 0;
130 int alphaCount = 0;
131 for (const unsigned char ch : value) {
132 if (std::isdigit(ch)) ++digitCount;
133 if (std::isalpha(ch)) ++alphaCount;
134 }
135
136 if (alphaCount == 0) return false;
137 if (digitCount == 0) return true;
138 if (digitCount <= 2 && alphaCount >= digitCount * 2) return true;
139
140 static const std::regex compactReferencePattern("^[A-Z]{1,6}(?:[ -]?[A-Z0-9]{2,8}){1,5}$", std::regex::icase);
141 return value.size() <= 36 && std::regex_match(value, compactReferencePattern);
142}
143
144std::string firstMeaningfulLine(const std::string& text)
145{
146 for (const auto& line : splitLines(text)) {
147 if (!line.empty()) return line;
148 }
149 return {};
150}
151
152std::string leadingPhrase(const std::string& text, std::size_t maxTokens)
153{
154 std::istringstream in(text);
155 std::ostringstream out;
156 std::string token;
157 std::size_t count = 0;
158 while (in >> token) {
159 if (count++ > 0) out << ' ';
160 out << token;
161 if (count >= maxTokens) break;
162 }
163 return trim(out.str());
164}
165
166std::string normalizeText(std::string text)
167{
168 for (auto& ch : text) {
169 const unsigned char byte = static_cast<unsigned char>(ch);
170 if (byte < 128 && std::isalpha(byte)) {
171 ch = static_cast<char>(std::tolower(byte));
172 } else if (byte < 128 && std::isalnum(byte)) {
173 ch = static_cast<char>(byte);
174 } else {
175 ch = ' ';
176 }
177 }
178 return trim(text);
179}
180
181std::vector<std::string> tokens(const std::string& text)
182{
183 std::vector<std::string> out;
184 std::istringstream in(normalizeText(text));
185 for (std::string token; in >> token;) out.push_back(token);
186 return out;
187}
188
189std::string leadingText(const std::string& text, std::size_t maxTokens)
190{
191 const auto parts = tokens(text);
192 std::ostringstream out;
193 for (std::size_t i = 0; i < parts.size() && i < maxTokens; ++i) {
194 if (i > 0) out << ' ';
195 out << parts[i];
196 }
197 return trim(out.str());
198}
199
200bool tokenIsMatchable(const std::string& token)
201{
202 if (token.size() >= 3) return true;
203 return std::all_of(token.begin(), token.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
204}
205
206int tokenOverlapScore(const std::vector<std::string>& sourceTokens, const std::vector<std::string>& targetTokens)
207{
208 if (sourceTokens.empty() || targetTokens.empty()) return 0;
209 int matches = 0;
210 for (const auto& token : targetTokens) {
211 if (!tokenIsMatchable(token)) continue;
212 if (std::find(sourceTokens.begin(), sourceTokens.end(), token) != sourceTokens.end()) ++matches;
213 }
214 return matches;
215}
216
217double aliasHitWeight(int hitCount)
218{
219 return std::min(0.7, std::log1p(std::max(1, hitCount)) / 5.0);
220}
221
222double aliasRecencyWeight(const std::string& lastUsedAt)
223{
224 (void)lastUsedAt;
225 return 0.0;
226}
227
228std::string metadataSignalText(const std::string& metadata)
229{
230 std::ostringstream out;
231 bool first = true;
232 const auto lines = splitLines(metadata);
233 for (std::size_t i = 0; i < lines.size(); ++i) {
234 const auto& line = lines[i];
235 const auto colon = line.find(':');
236 if (colon != std::string::npos) {
237 const auto key = trim(line.substr(0, colon));
238 const auto value = trim(line.substr(colon + 1));
239 if (!key.empty()) {
240 if (!first) out << ' ';
241 first = false;
242 out << key;
243 }
244 if (!value.empty()) {
245 if (!first) out << ' ';
246 first = false;
247 out << value;
248 } else if (i + 1 < lines.size() && !trim(lines[i + 1]).empty()) {
249 if (!first) out << ' ';
250 first = false;
251 out << trim(lines[i + 1]);
252 ++i;
253 }
254 continue;
255 }
256
257 if (!first) out << ' ';
258 first = false;
259 out << line;
260 }
261 return trim(out.str());
262}
263
264std::vector<std::string> referenceAliasesFromMetadata(const std::string& metadata)
265{
266 std::vector<std::string> out;
267 const std::regex refKeyword("(kund(?:en)?ref|kundenreferenz|referenz|verweis|vertragsnr|vertragsnummer|nr|nummer|ref)", std::regex::icase);
268 const std::regex digits("\\b\\d{3,}\\b");
269
270 for (const auto& line : splitLines(metadata)) {
271 if (line.empty()) continue;
272 if (std::regex_search(line, refKeyword)) appendUnique(out, line);
273
274 for (std::sregex_iterator it(line.begin(), line.end(), digits), end; it != end; ++it) {
275 appendUnique(out, it->str());
276 }
277 }
278
279 return out;
280}
281
283{
284 std::vector<std::string> types;
285 std::vector<std::string> normalizedSeen;
286 types.reserve(state.contracts().size());
287 normalizedSeen.reserve(state.contracts().size());
288 for (const auto& contract : state.contracts()) {
289 if (!contract) continue;
290 const auto type = trim(contract->type());
291 const auto normalizedType = normalizeText(type);
292 if (normalizedType.empty()) continue;
293 if (std::find(normalizedSeen.begin(), normalizedSeen.end(), normalizedType) != normalizedSeen.end()) continue;
294 normalizedSeen.push_back(normalizedType);
295 types.push_back(type);
296 }
297 return types;
298}
299
301 const std::string& signalText,
302 const std::vector<std::string>& lines)
303{
304 const auto contractTypes = knownContractTypes(state);
305 if (contractTypes.empty()) return {};
306
307 for (const auto& line : lines) {
308 const auto value = trim(line);
309 const auto separatorPos = value.find('-');
310 if (value.empty() || separatorPos == std::string::npos) continue;
311
312 const auto left = trim(value.substr(0, separatorPos));
313 if (left.empty()) continue;
314
315 const auto normalizedLeft = normalizeText(left);
316 for (const auto& type : contractTypes) {
317 if (normalizeText(type) == normalizedLeft) return type;
318 }
319 }
320
321 const auto fullText = normalizeText(signalText);
322 const auto headText = normalizeText(joinNonEmptyLines(lines, " "));
323
324 std::string bestType;
325 int bestScore = std::numeric_limits<int>::min();
326 for (const auto& type : contractTypes) {
327 const auto normalizedType = normalizeText(type);
328 if (normalizedType.empty()) continue;
329
330 int score = 0;
331 const auto fullPos = fullText.find(normalizedType);
332 if (fullPos != std::string::npos) {
333 score += 500;
334 score += std::max(0, 120 - static_cast<int>(fullPos));
335 }
336
337 const auto headPos = headText.find(normalizedType);
338 if (headPos != std::string::npos) {
339 score += 220;
340 score += std::max(0, 80 - static_cast<int>(headPos));
341 }
342
343 if (score <= 0) continue;
344 score += static_cast<int>(normalizedType.size());
345 if (score > bestScore) {
346 bestScore = score;
347 bestType = type;
348 }
349 }
350
351 return bestType;
352}
353
354std::string extractActorText(const std::vector<std::string>& lines, const std::string& typeText)
355{
356 std::vector<std::string> actorLines;
357 actorLines.reserve(lines.size());
358
359 const auto normalizedType = normalizeText(typeText);
360 for (std::size_t index = 0; index < lines.size(); ++index) {
361 const auto& line = lines[index];
362 const auto value = trim(line);
363 if (value.empty()) continue;
364
365 if (actorLines.empty()) {
366 actorLines.push_back(value);
367 continue;
368 }
369
370 if (lineLooksActorStopSignal(value)) break;
371 if (!lineLooksActorContinuation(value)) break;
372 actorLines.push_back(value);
373 if (index >= 1) break;
374 }
375
376 if (actorLines.empty()) return {};
377
378 auto actorText = joinNonEmptyLines(actorLines, " ");
379 if (!normalizedType.empty() && normalizeText(actorText) == normalizedType && actorLines.size() > 1) {
380 actorText = joinNonEmptyLines({actorLines.front(), actorLines[1]}, " ");
381 }
382 return trim(actorText);
383}
384
385} // namespace core::domain::policies::matching
Declares draft matching policy helpers for import-time bank statement inference.
Declares private string helpers shared across core implementation files.
const ContractList & contracts() const noexcept
Returns all contracts in the catalog.
Collects common core includes and aliases for precompiled-header builds.
bool lineLooksActorStopSignal(const std::string &line)
Checks whether a line should stop actor extraction.
std::vector< std::string > splitLines(const std::string &text)
Splits text into non-empty normalized lines.
std::vector< std::string > referenceAliasesFromMetadata(const std::string &metadata)
Extracts reference-like aliases from metadata.
bool matchesDraftText(const std::string &left, const std::string &right)
Compares two pieces of draft text after normalization.
bool lineLooksMetadataBoundary(const std::string &line)
Checks whether a line looks like a metadata boundary.
bool tokenIsMatchable(const std::string &token)
Checks whether a token is useful for matching.
std::string extractActorText(const std::vector< std::string > &lines, const std::string &typeText)
Extracts an actor hint from draft text.
std::string trim(std::string value)
Trims whitespace from raw text.
bool lineLooksActorContinuation(const std::string &line)
Checks whether a line can continue actor extraction.
std::string metadataSignalText(const std::string &metadata)
Normalizes metadata into signal text.
int tokenOverlapScore(const std::vector< std::string > &sourceTokens, const std::vector< std::string > &targetTokens)
Scores token overlap between two token lists.
double aliasHitWeight(int hitCount)
Computes a score weight from alias hit count.
double aliasRecencyWeight(const std::string &lastUsedAt)
Computes a score weight from alias recency.
std::string leadingPhrase(const std::string &text, std::size_t maxTokens)
Extracts a leading phrase from text.
std::string extractTypeText(const core::domain::catalog::WorkspaceCatalog &state, const std::string &signalText, const std::vector< std::string > &lines)
Extracts a contract type hint from draft text.
std::string joinNonEmptyLines(const std::vector< std::string > &values, const std::string &separator)
Joins non-empty lines with a separator.
std::string leadingText(const std::string &text, std::size_t maxTokens)
Returns a leading slice of the normalized text.
std::string firstMeaningfulLine(const std::string &text)
Returns the first meaningful line of text.
std::vector< std::string > knownContractTypes(const core::domain::catalog::WorkspaceCatalog &state)
Collects known contract types from the workspace state.
std::vector< std::string > tokens(const std::string &text)
Splits normalized text into tokens.
std::string normalizeText(std::string text)
Normalizes draft text for matching.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.