Line data Source code
1 : // Copyright 2025 The terCAD team. All rights reserved.
2 : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
3 :
4 : import 'package:app_finance/_classes/structure/bill_app_data.dart';
5 : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
6 :
7 : class BudgetPrediction {
8 : final Map<String, Map<String, int>> _descriptionPatterns = {};
9 : final Map<String, Map<String, int>> _accountCategories = {};
10 : final Map<String, Map<String, int>> _amountRanges = {};
11 :
12 0 : BudgetPrediction();
13 :
14 0 : void add(BillAppData item) {
15 0 : final category = item.category;
16 0 : _learnDescriptionPatterns(item.title, category);
17 0 : _learnAccountCategories(item.account, category);
18 0 : _learnAmountRanges(item.details, item.currency, category);
19 : }
20 :
21 0 : void _learnDescriptionPatterns(String? description, String category) {
22 0 : if (description == null || description.isEmpty) return;
23 :
24 0 : final words = description.toLowerCase().split(RegExp(r'\W+'));
25 0 : _descriptionPatterns.putIfAbsent(category, () => {});
26 :
27 0 : for (final word in words) {
28 0 : if (word.length > 2) {
29 0 : _descriptionPatterns[category]![word] = (_descriptionPatterns[category]![word] ?? 0) + 1;
30 : }
31 : }
32 : }
33 :
34 0 : void _learnAccountCategories(String? account, String category) {
35 : if (account == null) return;
36 :
37 0 : _accountCategories.putIfAbsent(category, () => {});
38 0 : _accountCategories[category]![account] = (_accountCategories[category]![account] ?? 0) + 1;
39 : }
40 :
41 0 : void _learnAmountRanges(double? amount, Currency? currency, String category) {
42 : if (amount == null) return;
43 :
44 0 : final range = _getAmountRange(amount, currency);
45 0 : _amountRanges.putIfAbsent(category, () => {});
46 0 : _amountRanges[category]![range] = (_amountRanges[category]![range] ?? 0) + 1;
47 : }
48 :
49 0 : String _getAmountRange(double amount, Currency? currency) {
50 0 : return amount.toStringAsFixed(6).length.toString() + (currency?.code ?? '');
51 : }
52 :
53 0 : List<String> predict(BillAppData item) {
54 0 : final scores = <String, double>{};
55 0 : _scoreByDescription(item.title, scores);
56 0 : _scoreByAccount(item.account, scores);
57 0 : _scoreByAmount(item.details, item.currency, scores);
58 :
59 0 : if (scores.isEmpty) return [];
60 :
61 0 : final sortedEntries = scores.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
62 :
63 0 : return sortedEntries.take(3).map((e) => e.key).toList();
64 : }
65 :
66 0 : void _scoreByDescription(String? description, Map<String, double> scores) {
67 0 : if (description == null || description.isEmpty) return;
68 :
69 0 : final words = description.toLowerCase().split(RegExp(r'\W+'));
70 :
71 0 : for (final category in _descriptionPatterns.keys) {
72 : double score = 0;
73 0 : int totalWords = _descriptionPatterns[category]!.values.fold(0, (a, b) => a + b);
74 :
75 0 : for (final word in words) {
76 0 : if (word.length > 2 && _descriptionPatterns[category]!.containsKey(word)) {
77 0 : score += _descriptionPatterns[category]![word]! / totalWords;
78 : }
79 : }
80 :
81 0 : scores[category] = (scores[category] ?? 0) + score * 0.5; // 50% weight
82 : }
83 : }
84 :
85 0 : void _scoreByAccount(String? account, Map<String, double> scores) {
86 : if (account == null) return;
87 :
88 0 : for (final category in _accountCategories.keys) {
89 0 : if (_accountCategories[category]!.containsKey(account)) {
90 0 : final occurrences = _accountCategories[category]![account]!;
91 0 : final totalForCategory = _accountCategories[category]!.values.fold(0, (a, b) => a + b);
92 0 : scores[category] = (scores[category] ?? 0) + (occurrences / totalForCategory) * 0.3; // 30% weight
93 : }
94 : }
95 : }
96 :
97 0 : void _scoreByAmount(double? amount, Currency? currency, Map<String, double> scores) {
98 : if (amount == null) return;
99 :
100 0 : final range = _getAmountRange(amount, currency);
101 :
102 0 : for (final category in _amountRanges.keys) {
103 0 : if (_amountRanges[category]!.containsKey(range)) {
104 0 : final occurrences = _amountRanges[category]![range]!;
105 0 : final totalForCategory = _amountRanges[category]!.values.fold(0, (a, b) => a + b);
106 0 : scores[category] = (scores[category] ?? 0) + (occurrences / totalForCategory) * 0.2; // 20% weight
107 : }
108 : }
109 : }
110 : }
|