KiCad PCB EDA Suite
Loading...
Searching...
No Matches
database_autofill.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <algorithm>
23#include <tuple>
24
25#include <wx/wxcrt.h>
26
27
28namespace DB_AUTOFILL
29{
30
31static constexpr size_t MAX_REMEMBERED = 8;
32
34{
36 SUBSTRING = 10,
37 AFFIX = 30,
39 EDGE_WORD = 80,
40 EXACT = 100,
41 REMEMBERED = 1000
42};
43
44
46static std::vector<wxString> SplitWords( const wxString& aName )
47{
48 std::vector<wxString> words;
49 wxString word;
50
51 auto flush =
52 [&]()
53 {
54 if( !word.IsEmpty() )
55 words.push_back( word.Lower() );
56
57 word.clear();
58 };
59
60 for( size_t ii = 0; ii < aName.length(); ++ii )
61 {
62 wxUniChar ch = aName[ii];
63
64 if( !wxIsalnum( ch ) )
65 {
66 flush();
67 continue;
68 }
69
70 if( !word.IsEmpty() )
71 {
72 wxUniChar prev = word.Last();
73 bool nextIsLower = ii + 1 < aName.length() && wxIslower( aName[ii + 1] );
74
75 // The last capital of an acronym starts the next word: "PCBFootprint" is PCB, Footprint
76 if( ( wxIslower( prev ) && wxIsupper( ch ) ) || ( !wxIsdigit( prev ) != !wxIsdigit( ch ) )
77 || ( wxIsupper( prev ) && wxIsupper( ch ) && nextIsLower ) )
78 {
79 flush();
80 }
81 }
82
83 word += ch;
84 }
85
86 flush();
87 return words;
88}
89
90
91static bool sameWord( const wxString& aA, const wxString& aB )
92{
93 return aA == aB || aA + wxS( "s" ) == aB || aB + wxS( "s" ) == aA || aA + wxS( "es" ) == aB
94 || aB + wxS( "es" ) == aA;
95}
96
97
98static bool wordsMatchAt( const std::vector<wxString>& aWords, const std::vector<wxString>& aTerm, size_t aPos )
99{
100 for( size_t ii = 0; ii < aTerm.size(); ++ii )
101 {
102 if( !sameWord( aWords[aPos + ii], aTerm[ii] ) )
103 return false;
104 }
105
106 return true;
107}
108
109
110static wxString joined( const std::vector<wxString>& aWords )
111{
112 wxString out;
113
114 for( const wxString& word : aWords )
115 out += word;
116
117 return out;
118}
119
120
122static int MatchScore( const wxString& aName, const wxString& aTerm )
123{
124 std::vector<wxString> words = SplitWords( aName );
125 std::vector<wxString> term = SplitWords( aTerm );
126
127 if( words.empty() || term.empty() )
128 return NO_MATCH;
129
130 if( term.size() <= words.size() )
131 {
132 size_t last = words.size() - term.size();
133
134 if( last == 0 && wordsMatchAt( words, term, 0 ) )
135 return EXACT;
136
137 if( wordsMatchAt( words, term, 0 ) || wordsMatchAt( words, term, last ) )
138 return EDGE_WORD;
139
140 for( size_t pos = 1; pos < last; ++pos )
141 {
142 if( wordsMatchAt( words, term, pos ) )
143 return INNER_WORD;
144 }
145 }
146
147 wxString name = joined( words );
148 wxString needle = joined( term );
149
150 if( name.StartsWith( needle ) || name.EndsWith( needle ) )
151 return AFFIX;
152
153 // Short terms such as "id" or "fp" turn up inside too many unrelated words
154 if( needle.length() >= 4 && name.Contains( needle ) )
155 return SUBSTRING;
156
157 return NO_MATCH;
158}
159
160
161static int roleScore( const ROLE& aRole, const std::string& aColumn )
162{
163 if( aRole.excluded.contains( aColumn ) )
164 return NO_MATCH;
165
166 wxString column = wxString::FromUTF8( aColumn );
167
168 for( size_t ii = 0; ii < aRole.remembered.size(); ++ii )
169 {
170 if( column.IsSameAs( wxString::FromUTF8( aRole.remembered[ii] ), false ) )
171 return REMEMBERED - static_cast<int>( ii );
172 }
173
174 int best = NO_MATCH;
175
176 for( const wxString& term : aRole.terms )
177 best = std::max( best, MatchScore( column, term ) );
178
179 return best;
180}
181
182
183std::vector<std::string> AssignColumns( const std::vector<std::string>& aColumns, const std::vector<ROLE>& aRoles )
184{
185 // Score, role, column; ties go to the earlier role and then the earlier column
186 std::vector<std::tuple<int, size_t, size_t>> candidates;
187
188 for( size_t role = 0; role < aRoles.size(); ++role )
189 {
190 for( size_t col = 0; col < aColumns.size(); ++col )
191 {
192 if( int score = roleScore( aRoles[role], aColumns[col] ); score > NO_MATCH )
193 candidates.emplace_back( -score, role, col );
194 }
195 }
196
197 std::sort( candidates.begin(), candidates.end() );
198
199 std::vector<std::string> chosen( aRoles.size() );
200 std::vector<bool> roleDone( aRoles.size(), false );
201 std::vector<bool> columnUsed( aColumns.size(), false );
202
203 for( const auto& [negScore, role, col] : candidates )
204 {
205 if( roleDone[role] || columnUsed[col] )
206 continue;
207
208 chosen[role] = aColumns[col];
209 roleDone[role] = true;
210 columnUsed[col] = true;
211 }
212
213 return chosen;
214}
215
216
217std::string MatchTable( const std::vector<std::string>& aTables, const std::string& aLibraryName )
218{
219 wxString library = wxString::FromUTF8( aLibraryName );
220 std::string best;
221 int bestScore = INNER_WORD - 1;
222
223 for( const std::string& table : aTables )
224 {
225 wxString name = wxString::FromUTF8( table );
226
227 // Either may carry extra words: library "Resistors" for table "tbl_resistor", or the reverse
228 int score = std::max( MatchScore( name, library ), MatchScore( library, name ) );
229
230 if( score > bestScore )
231 {
232 best = table;
233 bestScore = score;
234 }
235 }
236
237 return best;
238}
239
240
241std::map<std::string, size_t> RepeatCounts( const std::vector<std::map<std::string, std::any>>& aRows )
242{
243 std::map<std::string, std::set<std::string>> seen;
244 std::map<std::string, size_t> repeats;
245
246 for( const std::map<std::string, std::any>& row : aRows )
247 {
248 for( const auto& [column, value] : row )
249 {
250 // The library files rows by key text, so empty and NULL keys collide like any other repeat
251 const std::string* text = std::any_cast<std::string>( &value );
252
253 repeats.try_emplace( column, 0 );
254
255 if( !seen[column].insert( text ? *text : std::string() ).second )
256 repeats[column]++;
257 }
258 }
259
260 return repeats;
261}
262
263
264void RememberChoice( std::vector<std::string>& aHistory, const std::string& aColumn )
265{
266 if( aColumn.empty() )
267 return;
268
269 wxString column = wxString::FromUTF8( aColumn );
270
271 std::erase_if( aHistory,
272 [&]( const std::string& aEntry )
273 {
274 return column.IsSameAs( wxString::FromUTF8( aEntry ), false );
275 } );
276
277 aHistory.insert( aHistory.begin(), aColumn );
278
279 if( aHistory.size() > MAX_REMEMBERED )
280 aHistory.resize( MAX_REMEMBERED );
281}
282
283} // namespace DB_AUTOFILL
const char * name
Guesses the table and column mappings of a new database library from their names.
static wxString joined(const std::vector< wxString > &aWords)
std::vector< std::string > AssignColumns(const std::vector< std::string > &aColumns, const std::vector< ROLE > &aRoles)
Chooses a column for each role, best scores first, so no column serves two roles.
static std::vector< wxString > SplitWords(const wxString &aName)
Splits an identifier into lower case words: "PCBFootprint_ref2" gives pcb, footprint,...
static int roleScore(const ROLE &aRole, const std::string &aColumn)
static bool wordsMatchAt(const std::vector< wxString > &aWords, const std::vector< wxString > &aTerm, size_t aPos)
static bool sameWord(const wxString &aA, const wxString &aB)
void RememberChoice(std::vector< std::string > &aHistory, const std::string &aColumn)
Moves aColumn to the front of a role's history, dropping repeats and the oldest entries.
std::string MatchTable(const std::vector< std::string > &aTables, const std::string &aLibraryName)
@ SUBSTRING
The term is buried inside a word: "myfootprintref" for "footprint".
@ AFFIX
The name starts or ends with the term mid-word: "Identical", "partid".
@ REMEMBERED
A column the user chose before; earlier choices score lower.
@ EDGE_WORD
The term is a whole word at either end: "PartID", "symbol_ref".
@ INNER_WORD
The term is a whole word inside the name: "Main ID Code".
@ EXACT
The name is the term: "ID", "Symbols".
static int MatchScore(const wxString &aName, const wxString &aTerm)
std::map< std::string, size_t > RepeatCounts(const std::vector< std::map< std::string, std::any > > &aRows)
Counts, per column, the rows whose value already appeared in an earlier row, with NULL counted as emp...
static constexpr size_t MAX_REMEMBERED
One mapping to fill, such as the key column or the Value field.
std::vector< std::string > remembered
Columns the user chose before, most recent first.
std::set< std::string > excluded
Columns this role must not take, even when remembered.
std::vector< wxString > terms
Words to look for, in English and translated.
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)