FOSSredder 0.5.0
Loading...
Searching...
No Matches
PlotAnalysis.cpp
Go to the documentation of this file.
1
5
6#include "PlotAnalysis.h"
14
15#include <algorithm>
16#include <cmath>
17#include <map>
18#include <sstream>
19#include <unordered_map>
20
21#include <nlohmann/json.hpp>
22
24
25namespace {
26
27using ContractTypeIndex = std::unordered_map<std::string, std::string>;
28using ContractPropertyIndex = std::unordered_map<std::string, std::vector<std::string>>;
29using PropertyNameIndex = std::unordered_map<std::string, std::string>;
30
31struct PlotConfig {
32 std::string plotType = std::string(core::application::analysis::keys::plotTypes::kPie);
33 std::string plotMeasure = std::string(core::application::analysis::keys::plotMeasures::kTotalAmount);
34 std::vector<std::string> propertyAnalysisFilter;
35 std::vector<std::string> contractTypeAnalysisFilter;
36};
37
38std::string normalizeValue(const std::string& value);
39std::string normalizePlotMeasure(const std::string& value);
40
41PlotConfig parsePlotConfig(const core::domain::Analysis& analysis)
42{
43 PlotConfig config;
44 if (analysis.configJson().empty()) {
45 return config;
46 }
47
48 try {
49 const auto json = nlohmann::json::parse(analysis.configJson());
50 if (!json.is_object()) {
51 return config;
52 }
53
54 const auto plotTypeIt = json.find(core::application::analysis::keys::kPlotTypeKey);
55 if (plotTypeIt != json.end() && plotTypeIt->is_string()) {
56 config.plotType = normalizeValue(plotTypeIt->get<std::string>());
57 }
58
59 const auto plotMeasureIt = json.find(core::application::analysis::keys::kPlotMeasureKey);
60 if (plotMeasureIt != json.end() && plotMeasureIt->is_string()) {
61 config.plotMeasure = normalizePlotMeasure(plotMeasureIt->get<std::string>());
62 }
63
64 const auto propertiesIt = json.find(core::application::analysis::keys::kPropertiesKey);
65 if (propertiesIt != json.end() && propertiesIt->is_array()) {
66 for (const auto& entry : *propertiesIt) {
67 if (entry.is_string()) {
68 config.propertyAnalysisFilter.push_back(entry.get<std::string>());
69 }
70 }
71 }
72
73 const auto contractTypesIt = json.find(core::application::analysis::keys::kContractTypesKey);
74 if (contractTypesIt != json.end() && contractTypesIt->is_array()) {
75 for (const auto& entry : *contractTypesIt) {
76 if (entry.is_string()) {
77 config.contractTypeAnalysisFilter.push_back(entry.get<std::string>());
78 }
79 }
80 }
81 } catch (...) {}
82
83 return config;
84}
85
86std::string normalizeValue(const std::string& value)
87{
88 std::string normalized = core::utils::trim(value);
89 if (normalized.empty()) {
90 return {};
91 }
92
93 for (auto& ch : normalized) {
94 ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
95 }
96
97 return normalized;
98}
99
100std::vector<std::string> normalizeList(const std::vector<std::string>& values)
101{
102 std::vector<std::string> normalizedValues;
103 normalizedValues.reserve(values.size());
104 for (const auto& value : values) {
105 const auto normalized = normalizeValue(value);
106 if (!normalized.empty()) {
107 normalizedValues.push_back(normalized);
108 }
109 }
110 return normalizedValues;
111}
112
113std::string normalizePlotMeasure(const std::string& value)
114{
115 const auto normalized = normalizeValue(value);
118 }
119 if (normalized == "averageamount" || normalized == "average amount") {
121 }
122 if (normalized == "totalamount" || normalized == "total amount") {
124 }
125 return normalized;
126}
127
128ContractTypeIndex buildContractTypeIndex(const core::domain::catalog::WorkspaceCatalog& state, bool normalize)
129{
130 ContractTypeIndex index;
131 index.reserve(state.contracts().size());
132 for (const auto& contract : state.contracts()) {
133 if (!contract) {
134 continue;
135 }
136 index.emplace(contract->id(), normalize ? normalizeValue(contract->type()) : contract->type());
137 }
138 return index;
139}
140
141ContractPropertyIndex buildContractPropertyIndex(const core::domain::catalog::WorkspaceCatalog& state)
142{
143 ContractPropertyIndex index;
144 index.reserve(state.contracts().size());
145 for (const auto& contract : state.contracts()) {
146 if (!contract) {
147 continue;
148 }
149 index.emplace(contract->id(), contract->propertyIds());
150 }
151 return index;
152}
153
154PropertyNameIndex buildPropertyNameIndex(const core::domain::catalog::WorkspaceCatalog& state)
155{
156 PropertyNameIndex index;
157 index.reserve(state.properties().size());
158 for (const auto& property : state.properties()) {
159 if (!property) {
160 continue;
161 }
162 index.emplace(property->id(), property->name().empty() ? property->id() : property->name());
163 }
164 return index;
165}
166
167std::string propertyLabelForId(const PropertyNameIndex& propertyNameById,
168 const std::string& propertyId)
169{
170 const auto it = propertyNameById.find(propertyId);
171 if (it == propertyNameById.end() || it->second.empty()) {
172 return propertyId;
173 }
174 return it->second;
175}
176
177std::vector<std::string> effectivePropertyIds(const core::domain::Transaction& transaction,
178 const ContractPropertyIndex& propertyIdsByContractId)
179{
180 std::vector<std::string> ids = transaction.propertyIds();
181 if (transaction.contractId().empty()) {
182 return ids;
183 }
184
185 const auto it = propertyIdsByContractId.find(transaction.contractId());
186 if (it == propertyIdsByContractId.end()) {
187 return ids;
188 }
189
190 for (const auto& propertyId : it->second) {
191 if (std::find(ids.begin(), ids.end(), propertyId) == ids.end()) {
192 ids.push_back(propertyId);
193 }
194 }
195 return ids;
196}
197
198bool matchesPropertyAnalysisFilter(const core::domain::Transaction& transaction,
199 const std::vector<std::string>& propertyAnalysisFilter,
200 const ContractPropertyIndex& propertyIdsByContractId)
201{
202 if (propertyAnalysisFilter.empty()) {
203 return true;
204 }
205
206 const auto propertyIds = effectivePropertyIds(transaction, propertyIdsByContractId);
207 for (const auto& propertyId : propertyIds) {
208 if (std::find(propertyAnalysisFilter.begin(), propertyAnalysisFilter.end(), propertyId) != propertyAnalysisFilter.end()) {
209 return true;
210 }
211 }
212
213 return false;
214}
215
216bool matchesContractTypeAnalysisFilter(const core::domain::Transaction& transaction,
217 const std::vector<std::string>& normalizedContractTypeAnalysisFilter,
218 const ContractTypeIndex& normalizedContractTypeById)
219{
220 if (normalizedContractTypeAnalysisFilter.empty()) {
221 return true;
222 }
223
224 if (transaction.contractId().empty()) {
225 return std::find(normalizedContractTypeAnalysisFilter.begin(),
226 normalizedContractTypeAnalysisFilter.end(),
227 std::string(core::application::analysis::filterKeys::kUnassigned)) != normalizedContractTypeAnalysisFilter.end();
228 }
229
230 const auto it = normalizedContractTypeById.find(transaction.contractId());
231 if (it == normalizedContractTypeById.end()) {
232 return false;
233 }
234
235 return std::find(normalizedContractTypeAnalysisFilter.begin(),
236 normalizedContractTypeAnalysisFilter.end(),
237 it->second) != normalizedContractTypeAnalysisFilter.end();
238}
239
240std::vector<std::shared_ptr<core::domain::Transaction>> collectMatchedTransactions(const core::domain::catalog::WorkspaceCatalog& state,
241 const AnalysisFilter& filter,
242 const std::vector<std::string>& propertyAnalysisFilter,
243 const std::vector<std::string>& normalizedContractTypeAnalysisFilter,
244 const ContractTypeIndex& normalizedContractTypeById,
245 const ContractPropertyIndex& propertyIdsByContractId)
246{
247 std::vector<std::shared_ptr<core::domain::Transaction>> matched;
248 matched.reserve(state.transactions().size());
249 for (const auto& transaction : state.transactions()) {
250 if (!transaction) {
251 continue;
252 }
253 if (!filter.empty() && !filter.matches(transaction, state)) {
254 continue;
255 }
256 if (!matchesPropertyAnalysisFilter(*transaction, propertyAnalysisFilter, propertyIdsByContractId)) {
257 continue;
258 }
259 if (!matchesContractTypeAnalysisFilter(*transaction, normalizedContractTypeAnalysisFilter, normalizedContractTypeById)) {
260 continue;
261 }
262 matched.push_back(transaction);
263 }
264
265 return matched;
266}
267
268std::string extractYearMonth(const std::string& bookingDate)
269{
270 const std::string value = core::utils::trim(bookingDate);
271 if (value.empty()) {
272 return {};
273 }
274
275 if (value.size() >= 7
276 && std::isdigit(static_cast<unsigned char>(value[0]))
277 && std::isdigit(static_cast<unsigned char>(value[1]))
278 && std::isdigit(static_cast<unsigned char>(value[2]))
279 && std::isdigit(static_cast<unsigned char>(value[3]))) {
280 const std::string year = value.substr(0, 4);
281 if (value[4] == '-') {
282 return year + "-" + value.substr(5, 2);
283 }
284
285 if (value.size() >= 6) {
286 return year + "-" + value.substr(4, 2);
287 }
288 }
289
290 if (value.find('.') != std::string::npos) {
291 std::vector<std::string> parts;
292 std::istringstream stream(value);
293 std::string token;
294 while (std::getline(stream, token, '.')) {
295 parts.push_back(core::utils::trim(token));
296 }
297
298 if (parts.size() == 3) {
299 std::string month = parts[1];
300 if (parts[2].size() == 4 && (month.size() == 1 || month.size() == 2)) {
301 if (month.size() == 1) {
302 month.insert(month.begin(), '0');
303 }
304 return parts[2] + "-" + month;
305 }
306 }
307 }
308
309 return value;
310}
311
312core::ports::analysis::AnalysisResult buildPieResult(
313 const std::vector<std::shared_ptr<core::domain::Transaction>>& matched,
314 const ContractTypeIndex& normalizedContractTypeById,
315 const std::string& plotMeasure)
316{
317 core::ports::analysis::AnalysisResult result;
318 std::map<std::string, double> aggregatedAmounts;
319 std::map<std::string, int> transactionCounts;
320
321 for (const auto& transaction : matched) {
322 std::string key = std::string(core::application::analysis::filterKeys::kUnassigned);
323 if (!transaction->contractId().empty()) {
324 const auto it = normalizedContractTypeById.find(transaction->contractId());
325 if (it != normalizedContractTypeById.end() && !it->second.empty()) {
326 key = it->second;
327 } else {
328 key = transaction->contractId();
329 }
330 }
331
332 aggregatedAmounts[key] += transaction->amount();
333 transactionCounts[key]++;
334 }
335
336 double totalAmount = 0.0;
337 std::size_t totalCount = 0;
338 for (const auto& [key, amount] : aggregatedAmounts) {
339 totalAmount += std::fabs(amount);
340 totalCount += transactionCounts[key];
341 }
342
343 for (const auto& [key, amount] : aggregatedAmounts) {
344 double value = 0.0;
346 value = static_cast<double>(transactionCounts[key]);
348 value = transactionCounts[key] > 0 ? std::fabs(amount) / transactionCounts[key] : 0.0;
349 } else {
350 value = std::fabs(amount);
351 }
352
353 result.table.push_back({key, std::to_string(value)});
354 }
355
357 result.metrics[std::string(core::application::analysis::keys::metricKeys::kRowCount)] = static_cast<double>(totalCount);
358 return result;
359}
360
361core::ports::analysis::AnalysisResult buildHistogramResult(
362 const std::vector<std::shared_ptr<core::domain::Transaction>>& matched,
363 const core::domain::catalog::WorkspaceCatalog& state,
364 const ContractPropertyIndex& propertyIdsByContractId)
365{
366 core::ports::analysis::AnalysisResult result;
367 if (matched.empty()) {
368 return result;
369 }
370
371 const auto contractTypeByIdRaw = buildContractTypeIndex(state, false);
372 const auto propertyNameById = buildPropertyNameIndex(state);
373 std::map<std::string, double> monthTotal;
374 std::map<std::string, std::map<std::string, double>> monthByContract;
375 std::map<std::string, std::map<std::string, double>> monthByProperty;
376
377 for (const auto& transaction : matched) {
378 const std::string month = extractYearMonth(transaction->bookingDate());
379 const double amount = std::fabs(transaction->amount());
380 monthTotal[month] += amount;
381
382 std::string contractType = std::string(core::application::analysis::filterKeys::kUnassigned);
383 if (!transaction->contractId().empty()) {
384 const auto it = contractTypeByIdRaw.find(transaction->contractId());
385 if (it != contractTypeByIdRaw.end() && !it->second.empty()) {
386 contractType = it->second;
387 } else {
388 contractType = transaction->contractId();
389 }
390 }
391 monthByContract[month][contractType] += amount;
392
393 const auto propertyIds = effectivePropertyIds(*transaction, propertyIdsByContractId);
394 if (!propertyIds.empty()) {
395 for (const auto& propertyId : propertyIds) {
396 monthByProperty[month][propertyLabelForId(propertyNameById, propertyId)] += amount;
397 }
398 } else {
399 monthByProperty[month][std::string(core::application::analysis::keys::labels::kNoProperty)] += amount;
400 }
401 }
402
403 std::vector<std::string> months;
404 months.reserve(monthTotal.size());
405 for (const auto& [month, amount] : monthTotal) {
406 (void)amount;
407 months.push_back(month);
408 }
409 std::sort(months.begin(), months.end());
410
411 double totalAmount = 0.0;
412 for (const auto& month : months) {
413 nlohmann::json summary;
416 summary[core::application::analysis::keys::resultFields::kByContract] = nlohmann::json::object();
417 for (const auto& [label, amount] : monthByContract[month]) {
419 }
420 summary[core::application::analysis::keys::resultFields::kByProperty] = nlohmann::json::object();
421 for (const auto& [label, amount] : monthByProperty[month]) {
423 }
424
425 result.table.push_back({month, summary.dump()});
426 totalAmount += monthTotal[month];
427 }
428
430 result.metrics[std::string(core::application::analysis::keys::metricKeys::kRowCount)] = static_cast<double>(months.size());
431 result.metrics[std::string(core::application::analysis::keys::metricKeys::kMatchedTransactions)] = static_cast<double>(matched.size());
432 return result;
433}
434
435} // namespace
436
437core::ports::analysis::AnalysisResult
440 const AnalysisFilter& filter)
441{
442 const PlotConfig config = parsePlotConfig(analysis);
443 const ContractTypeIndex normalizedContractTypeById = buildContractTypeIndex(state, true);
444 const ContractPropertyIndex propertyIdsByContractId = buildContractPropertyIndex(state);
445 const auto normalizedPropertyFilter = normalizeList(config.propertyAnalysisFilter);
446 const auto normalizedContractTypeFilter = normalizeList(config.contractTypeAnalysisFilter);
447 const auto matched = collectMatchedTransactions(state,
448 filter,
449 normalizedPropertyFilter,
450 normalizedContractTypeFilter,
451 normalizedContractTypeById,
452 propertyIdsByContractId);
453
456 out = buildHistogramResult(matched, state, propertyIdsByContractId);
457 } else {
458 out = buildPieResult(matched, normalizedContractTypeById, config.plotMeasure);
459 }
460
461 return out;
462}
463
464} // namespace core::application::analysis
Declares internal analysis filter predicates and collection helpers.
Defines stable analysis type, configuration, filter, and result keys.
Domain model for contracts.
Declares plot-oriented analysis aggregation helpers.
nlohmann::json json
Domain model for properties.
Declares private string helpers shared across core implementation files.
Domain model for transactions.
Domain aggregate that stores catalog entities for one workspace.
Aggregates transaction predicates parsed from one analysis filter specification.
const std::string & configJson() const noexcept
Definition Analysis.cpp:196
const std::vector< std::string > & propertyIds() const noexcept
const std::string & bookingDate() const noexcept
const double & amount() const noexcept
const std::string & contractId() const noexcept
const PropertyList & properties() const noexcept
Returns all properties in the catalog.
const TransactionList & transactions() const noexcept
Returns all transactions in the catalog.
const ContractList & contracts() const noexcept
Returns all contracts in the catalog.
constexpr std::string_view kUnassigned
constexpr std::string_view kNoProperty
constexpr std::string_view kMatchedTransactions
constexpr std::string_view kPlotTypeKey
constexpr std::string_view kPropertiesKey
constexpr std::string_view kPlotMeasureKey
constexpr std::string_view kContractTypesKey
core::ports::analysis::AnalysisResult computePlotAnalysis(const core::domain::Analysis &analysis, const core::domain::catalog::WorkspaceCatalog &state, const AnalysisFilter &filter)
Computes plot metrics and table rows for one analysis definition.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
std::vector< std::vector< std::string > > table
std::map< std::string, double > metrics