KiCad PCB EDA Suite
Loading...
Searching...
No Matches
text_eval_parser.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 along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21#include <fmt/format.h>
22#include <array>
23#include <cctype>
24
25namespace calc_parser
26{
27thread_local ERROR_COLLECTOR* g_errorCollector = nullptr;
28
30{
31private:
32 static constexpr int epochYear = 1970;
33 static constexpr std::array<int, 12> daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
34 static constexpr std::array<const char*, 12> monthNames = {
35 "January", "February", "March", "April", "May", "June",
36 "July", "August", "September", "October", "November", "December"
37 };
38 static constexpr std::array<const char*, 12> monthAbbrev = {
39 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
40 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
41 };
42 static constexpr std::array<const char*, 7> weekdayNames = {
43 "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
44 };
45
46 static auto isLeapYear( int aYear ) -> bool
47 {
48 return ( aYear % 4 == 0 && aYear % 100 != 0 ) || ( aYear % 400 == 0 );
49 }
50
51 static auto daysInYear( int aYear ) -> int
52 {
53 return isLeapYear( aYear ) ? 366 : 365;
54 }
55
56 static auto daysInMonthForYear( int aMonth, int aYear ) -> int
57 {
58 if( aMonth == 2 && isLeapYear( aYear ) )
59 return 29;
60
61 return daysInMonth[aMonth - 1];
62 }
63
64public:
65 static auto DaysToYmd( int aDaysSinceEpoch ) -> std::tuple<int, int, int>
66 {
67 int year = epochYear;
68 int remainingDays = aDaysSinceEpoch;
69
70 if( remainingDays >= 0 )
71 {
72 while( remainingDays >= daysInYear( year ) )
73 {
74 remainingDays -= daysInYear( year );
75 year++;
76 }
77 }
78 else
79 {
80 while( remainingDays < 0 )
81 {
82 year--;
83 remainingDays += daysInYear( year );
84 }
85 }
86
87 int month = 1;
88 while( month <= 12 && remainingDays >= daysInMonthForYear( month, year ) )
89 {
90 remainingDays -= daysInMonthForYear( month, year );
91 month++;
92 }
93
94 int day = remainingDays + 1;
95 return {year, month, day};
96 }
97
98 static auto YmdToDays( int aYear, int aMonth, int aDay ) -> int
99 {
100 int totalDays = 0;
101
102 if( aYear >= epochYear )
103 {
104 for( int y = epochYear; y < aYear; ++y )
105 totalDays += daysInYear( y );
106 }
107 else
108 {
109 for( int y = aYear; y < epochYear; ++y )
110 totalDays -= daysInYear( y );
111 }
112
113 for( int m = 1; m < aMonth; ++m )
114 totalDays += daysInMonthForYear( m, aYear );
115
116 totalDays += aDay - 1;
117 return totalDays;
118 }
119
120 static auto ParseDate( const std::string& aDateStr ) -> std::optional<int>
121 {
122 std::istringstream iss( aDateStr );
123 std::string token;
124 std::vector<int> parts;
125
126 char separator = 0;
127 bool isCjkFormat = false;
128
129 // Check for CJK date formats first (Chinese, Korean, or mixed)
130 bool hasChineseYear = aDateStr.find( "年" ) != std::string::npos;
131 bool hasChineseMonth = aDateStr.find( "月" ) != std::string::npos;
132 bool hasChineseDay = aDateStr.find( "日" ) != std::string::npos;
133 bool hasKoreanYear = aDateStr.find( "년" ) != std::string::npos;
134 bool hasKoreanMonth = aDateStr.find( "월" ) != std::string::npos;
135 bool hasKoreanDay = aDateStr.find( "일" ) != std::string::npos;
136
137 // Check if we have any CJK date format (pure or mixed)
138 if( (hasChineseYear || hasKoreanYear) &&
139 (hasChineseMonth || hasKoreanMonth) &&
140 (hasChineseDay || hasKoreanDay) )
141 {
142 // CJK format: Support pure Chinese, pure Korean, or mixed formats
143 isCjkFormat = true;
144
145 size_t yearPos, monthPos, dayPos;
146
147 // Find year position and marker
148 if( hasChineseYear )
149 yearPos = aDateStr.find( "年" );
150 else
151 yearPos = aDateStr.find( "년" );
152
153 // Find month position and marker
154 if( hasChineseMonth )
155 monthPos = aDateStr.find( "月" );
156 else
157 monthPos = aDateStr.find( "월" );
158
159 // Find day position and marker
160 if( hasChineseDay )
161 dayPos = aDateStr.find( "日" );
162 else
163 dayPos = aDateStr.find( "일" );
164
165 try
166 {
167 int year = std::stoi( aDateStr.substr( 0, yearPos ) );
168 int month = std::stoi( aDateStr.substr( yearPos + 3, monthPos - yearPos - 3 ) ); // 3 bytes for CJK year marker
169 int day = std::stoi( aDateStr.substr( monthPos + 3, dayPos - monthPos - 3 ) ); // 3 bytes for CJK month marker
170
171 parts = { year, month, day };
172 }
173 catch( ... )
174 {
175 return std::nullopt;
176 }
177 }
178 else if( aDateStr.find( '-' ) != std::string::npos )
179 separator = '-';
180 else if( aDateStr.find( '/' ) != std::string::npos )
181 separator = '/';
182 else if( aDateStr.find( '.' ) != std::string::npos )
183 separator = '.';
184
185 if( separator )
186 {
187 while( std::getline( iss, token, separator ) )
188 {
189 try
190 {
191 parts.push_back( std::stoi( token ) );
192 }
193 catch( ... )
194 {
195 return std::nullopt;
196 }
197 }
198 }
199 else if( !isCjkFormat && aDateStr.length() == 8 )
200 {
201 try
202 {
203 int dateNum = std::stoi( aDateStr );
204 int year = dateNum / 10000;
205 int month = ( dateNum / 100 ) % 100;
206 int day = dateNum % 100;
207 return YmdToDays( year, month, day );
208 }
209 catch( ... )
210 {
211 return std::nullopt;
212 }
213 }
214 else if( !isCjkFormat )
215 {
216 return std::nullopt;
217 }
218
219 if( parts.empty() || parts.size() > 3 )
220 return std::nullopt;
221
222 int year, month, day;
223
224 if( parts.size() == 1 )
225 {
226 year = parts[0];
227 month = 1;
228 day = 1;
229 }
230 else if( parts.size() == 2 )
231 {
232 year = parts[0];
233 month = parts[1];
234 day = 1;
235 }
236 else
237 {
238 if( isCjkFormat )
239 {
240 // CJK formats are always in YYYY年MM月DD日 or YYYY년 MM월 DD일 order
241 year = parts[0];
242 month = parts[1];
243 day = parts[2];
244 }
245 else if( separator == '/' && parts[0] <= 12 && parts[1] <= 31 )
246 {
247 month = parts[0];
248 day = parts[1];
249 year = parts[2];
250 }
251 else if( separator == '/' && parts[1] <= 12 )
252 {
253 day = parts[0];
254 month = parts[1];
255 year = parts[2];
256 }
257 else
258 {
259 year = parts[0];
260 month = parts[1];
261 day = parts[2];
262 }
263 }
264
265 if( month < 1 || month > 12 )
266 return std::nullopt;
267 if( day < 1 || day > daysInMonthForYear( month, year ) )
268 return std::nullopt;
269
270 return YmdToDays( year, month, day );
271 }
272
273 static auto FormatDate( int aDaysSinceEpoch, const std::string& aFormat ) -> std::string
274 {
275 auto [year, month, day] = DaysToYmd( aDaysSinceEpoch );
276
277 if( aFormat == "ISO" || aFormat == "iso" )
278 return fmt::format( "{:04d}-{:02d}-{:02d}", year, month, day );
279 else if( aFormat == "US" || aFormat == "us" )
280 return fmt::format( "{:02d}/{:02d}/{:04d}", month, day, year );
281 else if( aFormat == "EU" || aFormat == "european" )
282 return fmt::format( "{:02d}/{:02d}/{:04d}", day, month, year );
283 else if( aFormat == "long" )
284 return fmt::format( "{} {}, {}", monthNames[month-1], day, year );
285 else if( aFormat == "short" )
286 return fmt::format( "{} {}, {}", monthAbbrev[month-1], day, year );
287 else if( aFormat == "Chinese" || aFormat == "chinese" || aFormat == "CN" || aFormat == "cn" || aFormat == "中文" )
288 return fmt::format( "{}年{:02d}月{:02d}日", year, month, day );
289 else if( aFormat == "Japanese" || aFormat == "japanese" || aFormat == "JP" || aFormat == "jp" || aFormat == "日本語" )
290 return fmt::format( "{}年{:02d}月{:02d}日", year, month, day );
291 else if( aFormat == "Korean" || aFormat == "korean" || aFormat == "KR" || aFormat == "kr" || aFormat == "한국어" )
292 return fmt::format( "{}년 {:02d}월 {:02d}일", year, month, day );
293 else
294 return fmt::format( "{:04d}-{:02d}-{:02d}", year, month, day );
295 }
296
297 static auto GetWeekdayName( int aDaysSinceEpoch ) -> std::string
298 {
299 int weekday = ( ( aDaysSinceEpoch + 3 ) % 7 ); // +3 because epoch was Thursday (Monday = 0)
300
301 if( weekday < 0 )
302 weekday += 7;
303
304 return std::string{ weekdayNames[weekday] };
305 }
306
307 static auto GetCurrentDays() -> int
308 {
309 auto now = std::chrono::system_clock::now();
310 auto timeT = std::chrono::system_clock::to_time_t( now );
311 return static_cast<int>( timeT / ( 24 * 3600 ) );
312 }
313
314 static auto GetCurrentTimestamp() -> double
315 {
316 auto now = std::chrono::system_clock::now();
317 auto timeT = std::chrono::system_clock::to_time_t( now );
318 return static_cast<double>( timeT );
319 }
320};
321
322EVAL_VISITOR::EVAL_VISITOR( VariableCallback aVariableCallback, ERROR_COLLECTOR& aErrorCollector ) :
323 m_variableCallback( std::move( aVariableCallback ) ),
324 m_errors( aErrorCollector ),
325 m_gen( m_rd() )
326{}
327
328auto EVAL_VISITOR::operator()( const NODE& aNode ) const -> Result<Value>
329 {
330 switch( aNode.type )
331 {
332 case NodeType::Number:
333 return MakeValue<Value>( std::get<double>( aNode.data ) );
334
335 case NodeType::String:
336 return MakeValue<Value>( std::get<std::string>( aNode.data ) );
337
338 case NodeType::Var:
339 {
340 const auto& varName = std::get<std::string>( aNode.data );
341
342 // Use callback to resolve variable
344 return m_variableCallback( varName );
345
346 return MakeError<Value>( fmt::format( "No variable resolver configured for: {}", varName ) );
347 }
348
349 case NodeType::BinOp:
350 {
351 const auto& binop = std::get<BIN_OP_DATA>( aNode.data );
352 auto leftResult = binop.left->Accept( *this );
353 if( !leftResult )
354 return leftResult;
355
356 auto rightResult = binop.right ?
357 binop.right->Accept( *this ) : MakeValue<Value>( 0.0 );
358 if( !rightResult )
359 return rightResult;
360
361 // Special handling for string concatenation with +
362 if( binop.op == '+' )
363 {
364 const auto& leftVal = leftResult.GetValue();
365 const auto& rightVal = rightResult.GetValue();
366
367 // If either operand is a string, concatenate
368 if( std::holds_alternative<std::string>( leftVal ) ||
369 std::holds_alternative<std::string>( rightVal ) )
370 {
371 return MakeValue<Value>( VALUE_UTILS::ConcatStrings( leftVal, rightVal ) );
372 }
373 }
374
375 // Otherwise, perform arithmetic
376 return VALUE_UTILS::ArithmeticOp( leftResult.GetValue(), rightResult.GetValue(), binop.op );
377 }
378
380 {
381 const auto& func = std::get<FUNC_DATA>( aNode.data );
382 return evaluateFunction( func );
383 }
384
385 default:
386 return MakeError<Value>( "Cannot evaluate this node type" );
387 }
388}
389
391{
392 const auto& name = aFunc.name;
393 const auto& args = aFunc.args;
394
395 // Zero-argument functions
396 if( args.empty() )
397 {
398 if( name == "today" )
399 return MakeValue<Value>( static_cast<double>( DATE_UTILS::GetCurrentDays() ) );
400 else if( name == "now" )
402 else if( name == "random" )
403 {
404 std::uniform_real_distribution<double> dis( 0.0, 1.0 );
405 return MakeValue<Value>( dis( m_gen ) );
406 }
407 }
408
409 // Evaluate arguments to mixed types
410 std::vector<Value> argValues;
411 argValues.reserve( args.size() );
412
413 for( const auto& arg : args )
414 {
415 auto result = arg->Accept( *this );
416 if( !result )
417 return result;
418
419 argValues.push_back( result.GetValue() );
420 }
421
422 const auto argc = argValues.size();
423
424 // String formatting functions (return strings!)
425 if( name == "format" && argc >= 1 )
426 {
427 auto numResult = VALUE_UTILS::ToDouble( argValues[0] );
428 if( !numResult )
429 return MakeError<Value>( numResult.GetError() );
430
431 const auto value = numResult.GetValue();
432 int decimals = 2;
433 if( argc > 1 )
434 {
435 auto decResult = VALUE_UTILS::ToDouble( argValues[1] );
436 if( decResult )
437 decimals = static_cast<int>( decResult.GetValue() );
438 }
439
440 return MakeValue<Value>( fmt::format( "{:.{}f}", value, decimals ) );
441 }
442 else if( name == "currency" && argc >= 1 )
443 {
444 auto numResult = VALUE_UTILS::ToDouble( argValues[0] );
445 if( !numResult )
446 return MakeError<Value>( numResult.GetError() );
447
448 const auto amount = numResult.GetValue();
449 const auto symbol = argc > 1 ? VALUE_UTILS::ToString( argValues[1] ) : "$";
450
451 return MakeValue<Value>( fmt::format( "{}{:.2f}", symbol, amount ) );
452 }
453 else if( name == "fixed" && argc >= 1 )
454 {
455 auto numResult = VALUE_UTILS::ToDouble( argValues[0] );
456 if( !numResult )
457 return MakeError<Value>( numResult.GetError() );
458
459 const auto value = numResult.GetValue();
460 int decimals = 2;
461 if( argc > 1 )
462 {
463 auto decResult = VALUE_UTILS::ToDouble( argValues[1] );
464 if( decResult )
465 decimals = static_cast<int>( decResult.GetValue() );
466 }
467
468 return MakeValue<Value>( fmt::format( "{:.{}f}", value, decimals ) );
469 }
470
471 // Date formatting functions (return strings!)
472 else if( name == "dateformat" && argc >= 1 )
473 {
474 auto dateResult = VALUE_UTILS::ToDouble( argValues[0] );
475 if( !dateResult )
476 return MakeError<Value>( dateResult.GetError() );
477
478 const auto days = static_cast<int>( dateResult.GetValue() );
479 const auto format = argc > 1 ? VALUE_UTILS::ToString( argValues[1] ) : "ISO";
480
481 return MakeValue<Value>( DATE_UTILS::FormatDate( days, format ) );
482 }
483 else if( name == "datestring" && argc == 1 )
484 {
485 auto dateStr = VALUE_UTILS::ToString( argValues[0] );
486 auto daysResult = DATE_UTILS::ParseDate( dateStr );
487
488 if( !daysResult )
489 return MakeError<Value>( "Invalid date format: " + dateStr );
490
491 return MakeValue<Value>( static_cast<double>( daysResult.value() ) );
492 }
493 else if( name == "weekdayname" && argc == 1 )
494 {
495 auto dateResult = VALUE_UTILS::ToDouble( argValues[0] );
496 if( !dateResult )
497 return MakeError<Value>( dateResult.GetError() );
498
499 const auto days = static_cast<int>( dateResult.GetValue() );
501 }
502
503 // String functions (return strings!)
504 else if( name == "upper" && argc == 1 )
505 {
506 auto str = VALUE_UTILS::ToString( argValues[0] );
507 std::transform( str.begin(), str.end(), str.begin(), ::toupper );
508 return MakeValue<Value>( str );
509 }
510 else if( name == "lower" && argc == 1 )
511 {
512 auto str = VALUE_UTILS::ToString( argValues[0] );
513 std::transform( str.begin(), str.end(), str.begin(), ::tolower );
514 return MakeValue<Value>( str );
515 }
516 else if( name == "concat" && argc >= 2 )
517 {
518 std::string result;
519 for( const auto& val : argValues )
521
522 return MakeValue<Value>( result );
523 }
524
525 // Conditional functions (handle mixed types)
526 if( name == "if" && argc == 3 )
527 {
528 // Convert only the condition to a number
529 auto conditionResult = VALUE_UTILS::ToDouble( argValues[0] );
530 if( !conditionResult )
531 return MakeError<Value>( conditionResult.GetError() );
532
533 const auto condition = conditionResult.GetValue() != 0.0;
534 return MakeValue<Value>( condition ? argValues[1] : argValues[2] );
535 }
536
537 // Mathematical functions (return numbers) - convert args to doubles first
538 std::vector<double> numArgs;
539 for( const auto& val : argValues )
540 {
541 auto numResult = VALUE_UTILS::ToDouble( val );
542 if( !numResult )
543 return MakeError<Value>( numResult.GetError() );
544
545 numArgs.push_back( numResult.GetValue() );
546 }
547
548 // Mathematical function implementations
549 if( name == "abs" && argc == 1 )
550 return MakeValue<Value>( std::abs( numArgs[0] ) );
551 else if( name == "sum" && argc >= 1 )
552 return MakeValue<Value>( std::accumulate( numArgs.begin(), numArgs.end(), 0.0 ) );
553 else if( name == "round" && argc >= 1 )
554 {
555 const auto value = numArgs[0];
556 const auto precision = argc > 1 ? static_cast<int>( numArgs[1] ) : 0;
557 const auto multiplier = std::pow( 10.0, precision );
558 return MakeValue<Value>( std::round( value * multiplier ) / multiplier );
559 }
560 else if( name == "sqrt" && argc == 1 )
561 {
562 if( numArgs[0] < 0 )
563 return MakeError<Value>( "Square root of negative number" );
564
565 return MakeValue<Value>( std::sqrt( numArgs[0] ) );
566 }
567 else if( name == "pow" && argc == 2 )
568 return MakeValue<Value>( std::pow( numArgs[0], numArgs[1] ) );
569 else if( name == "floor" && argc == 1 )
570 return MakeValue<Value>( std::floor( numArgs[0] ) );
571 else if( name == "ceil" && argc == 1 )
572 return MakeValue<Value>( std::ceil( numArgs[0] ) );
573 else if( name == "min" && argc >= 1 )
574 return MakeValue<Value>( *std::min_element( numArgs.begin(), numArgs.end() ) );
575 else if( name == "max" && argc >= 1 )
576 return MakeValue<Value>( *std::max_element( numArgs.begin(), numArgs.end() ) );
577 else if( name == "avg" && argc >= 1 )
578 {
579 const auto sum = std::accumulate( numArgs.begin(), numArgs.end(), 0.0 );
580 return MakeValue<Value>( sum / static_cast<double>( argc ) );
581 }
582
583 return MakeError<Value>( fmt::format( "Unknown function: {} with {} arguments", name, argc ) );
584}
585
586auto DOC_PROCESSOR::Process( const DOC& aDoc, VariableCallback aVariableCallback )
587 -> std::pair<std::string, bool>
588 {
589 std::string result;
590 auto localErrors = ERROR_COLLECTOR{};
591 EVAL_VISITOR evaluator{ std::move( aVariableCallback ), localErrors };
592 bool hadErrors = aDoc.HasErrors();
593
594 for( const auto& node : aDoc.GetNodes() )
595 {
596 switch( node->type )
597 {
598 case NodeType::Text:
599 result += std::get<std::string>( node->data );
600 break;
601
602 case NodeType::Calc:
603 {
604 const auto& calcData = std::get<BIN_OP_DATA>( node->data );
605 auto evalResult = calcData.left->Accept( evaluator );
606
607 if( evalResult )
608 result += VALUE_UTILS::ToString( evalResult.GetValue() );
609 else
610 {
611 // Don't add error formatting to result - errors go to error vector only
612 // The higher level will return original input unchanged if there are errors
613 hadErrors = true;
614 }
615 break;
616 }
617
618 default:
619 result += "[Unknown node type]";
620 hadErrors = true;
621 break;
622 }
623 }
624
625 return { std::move( result ), hadErrors || localErrors.HasErrors() };
626}
627
628auto DOC_PROCESSOR::ProcessWithDetails( const DOC& aDoc, VariableCallback aVariableCallback )
629 -> std::tuple<std::string, std::vector<std::string>, bool>
630{
631 auto [result, hadErrors] = Process( aDoc, std::move( aVariableCallback ) );
632 auto allErrors = aDoc.GetErrors();
633
634 return { std::move( result ), std::move( allErrors ), hadErrors };
635}
636
637} // namespace calc_parser
const char * name
static auto GetCurrentDays() -> int
static constexpr std::array< const char *, 12 > monthNames
static auto GetWeekdayName(int aDaysSinceEpoch) -> std::string
static auto FormatDate(int aDaysSinceEpoch, const std::string &aFormat) -> std::string
static constexpr std::array< const char *, 12 > monthAbbrev
static auto YmdToDays(int aYear, int aMonth, int aDay) -> int
static auto DaysToYmd(int aDaysSinceEpoch) -> std::tuple< int, int, int >
static auto ParseDate(const std::string &aDateStr) -> std::optional< int >
static auto daysInYear(int aYear) -> int
static constexpr int epochYear
static constexpr std::array< int, 12 > daysInMonth
static auto isLeapYear(int aYear) -> bool
static constexpr std::array< const char *, 7 > weekdayNames
static auto daysInMonthForYear(int aMonth, int aYear) -> int
static auto GetCurrentTimestamp() -> double
static auto ProcessWithDetails(const DOC &aDoc, VariableCallback aVariableCallback) -> std::tuple< std::string, std::vector< std::string >, bool >
Process document with detailed error reporting.
EVAL_VISITOR::VariableCallback VariableCallback
static auto Process(const DOC &aDoc, VariableCallback aVariableCallback) -> std::pair< std::string, bool >
Process document using callback for variable resolution.
auto operator()(const NODE &aNode) const -> Result< Value >
VariableCallback m_variableCallback
std::function< Result< Value >(const std::string &aVariableName)> VariableCallback
EVAL_VISITOR(VariableCallback aVariableCallback, ERROR_COLLECTOR &aErrorCollector)
Construct evaluator with variable callback function.
auto evaluateFunction(const FUNC_DATA &aFunc) const -> Result< Value >
static auto ArithmeticOp(const Value &aLeft, const Value &aRight, char aOp) -> Result< Value >
static auto ToString(const Value &aVal) -> std::string
static auto ToDouble(const Value &aVal) -> Result< double >
static auto ConcatStrings(const Value &aLeft, const Value &aRight) -> Value
thread_local ERROR_COLLECTOR * g_errorCollector
auto MakeValue(T aVal) -> Result< T >
auto MakeError(std::string aMsg) -> Result< T >
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
wxString result
Test unit parsing edge cases and error handling.