Browse Source
ADDED: Text expression evaluation
ADDED: Text expression evaluation
Arbitrary text strings now support full evaluation with a rich functional language Fixes https://gitlab.com/kicad/code/kicad/-/issues/6643master
26 changed files with 7228 additions and 4 deletions
-
10common/CMakeLists.txt
-
9common/eda_text.cpp
-
19common/gr_text.cpp
-
19common/plotters/plotter.cpp
-
200common/text_eval/text_eval.lemon
-
636common/text_eval/text_eval_parser.cpp
-
2068common/text_eval/text_eval_wrapper.cpp
-
3eeschema/sch_field.cpp
-
3eeschema/sch_label.cpp
-
3eeschema/sch_text.cpp
-
3eeschema/sch_textbox.cpp
-
2include/eda_text.h
-
454include/text_eval/text_eval_parser.h
-
120include/text_eval/text_eval_types.h
-
267include/text_eval/text_eval_units.h
-
529include/text_eval/text_eval_wrapper.h
-
3pcbnew/pcb_text.cpp
-
3pcbnew/pcb_textbox.cpp
-
6qa/tests/common/CMakeLists.txt
-
115qa/tests/common/text_eval/README.md
-
697qa/tests/common/text_eval/test_text_eval_numeric_compat.cpp
-
589qa/tests/common/text_eval/test_text_eval_parser.cpp
-
508qa/tests/common/text_eval/test_text_eval_parser_core.cpp
-
471qa/tests/common/text_eval/test_text_eval_parser_datetime.cpp
-
441qa/tests/common/text_eval/test_text_eval_parser_integration.cpp
-
54qa/tests/common/text_eval/test_text_eval_render.cpp
@ -0,0 +1,200 @@ |
|||
%include { |
|||
#include <text_eval/text_eval_parser.h> |
|||
using namespace calc_parser; |
|||
} |
|||
|
|||
%token_type {calc_parser::TOKEN_TYPE} |
|||
%token_prefix KI_EVAL_ |
|||
%default_type {calc_parser::NODE*} |
|||
%extra_argument {calc_parser::DOC** pDocument} |
|||
|
|||
%type document {calc_parser::DOC*} |
|||
%type content_list {calc_parser::DOC*} |
|||
%type content_item {calc_parser::NODE*} |
|||
%type calculation {calc_parser::NODE*} |
|||
%type expression {calc_parser::NODE*} |
|||
%type term {calc_parser::NODE*} |
|||
%type factor {calc_parser::NODE*} |
|||
%type variable {calc_parser::NODE*} |
|||
%type function_call {calc_parser::NODE*} |
|||
%type arg_list {std::vector<std::unique_ptr<NODE>>*} |
|||
|
|||
%destructor document { delete $$; } |
|||
%destructor content_list { delete $$; } |
|||
%destructor content_item { delete $$; } |
|||
%destructor calculation { delete $$; } |
|||
%destructor expression { delete $$; } |
|||
%destructor term { delete $$; } |
|||
%destructor factor { delete $$; } |
|||
%destructor variable { delete $$; } |
|||
%destructor function_call { delete $$; } |
|||
%destructor arg_list { delete $$; } |
|||
|
|||
%left LT GT LE GE EQ NE. |
|||
%left PLUS MINUS. |
|||
%left MULTIPLY DIVIDE MODULO. |
|||
%right UMINUS. |
|||
%right POWER. |
|||
%token COMMA. |
|||
|
|||
%start_symbol document |
|||
|
|||
// Main document structure |
|||
document(D) ::= content_list(L). { |
|||
D = L; |
|||
*pDocument = D; // Store the result in the extra argument |
|||
} |
|||
|
|||
content_list(L) ::= . { |
|||
L = new DOC(); |
|||
} |
|||
|
|||
content_list(L) ::= content_list(L) content_item(I). { |
|||
L->AddNodeRaw(I); |
|||
} |
|||
|
|||
content_item(I) ::= TEXT(T). { |
|||
I = NODE::CreateTextRaw(GetTokenString(T)); |
|||
} |
|||
|
|||
content_item(I) ::= calculation(C). { |
|||
I = C; |
|||
} |
|||
|
|||
content_item(I) ::= variable(V). { |
|||
I = NODE::CreateCalcRaw(V); |
|||
} |
|||
|
|||
calculation(C) ::= AT_OPEN expression(E) CLOSE_BRACE. { |
|||
C = NODE::CreateCalcRaw(E); |
|||
} |
|||
|
|||
// Mathematical expressions with proper precedence |
|||
expression(E) ::= expression(L) LT expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, '<', R); |
|||
} |
|||
|
|||
expression(E) ::= expression(L) GT expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, '>', R); |
|||
} |
|||
|
|||
expression(E) ::= expression(L) LE expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, 1, R); // Using 1 for <= |
|||
} |
|||
|
|||
expression(E) ::= expression(L) GE expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, 2, R); // Using 2 for >= |
|||
} |
|||
|
|||
expression(E) ::= expression(L) EQ expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, 3, R); // Using 3 for == |
|||
} |
|||
|
|||
expression(E) ::= expression(L) NE expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, 4, R); // Using 4 for != |
|||
} |
|||
|
|||
expression(E) ::= expression(L) PLUS expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, '+', R); |
|||
} |
|||
|
|||
expression(E) ::= expression(L) MINUS expression(R). { |
|||
E = NODE::CreateBinOpRaw(L, '-', R); |
|||
} |
|||
|
|||
expression(E) ::= term(T). { |
|||
E = T; |
|||
} |
|||
|
|||
term(T) ::= term(L) MULTIPLY term(R). { |
|||
T = NODE::CreateBinOpRaw(L, '*', R); |
|||
} |
|||
|
|||
term(T) ::= term(L) DIVIDE term(R). { |
|||
T = NODE::CreateBinOpRaw(L, '/', R); |
|||
} |
|||
|
|||
term(T) ::= term(L) MODULO term(R). { |
|||
T = NODE::CreateBinOpRaw(L, '%', R); |
|||
} |
|||
|
|||
term(T) ::= factor(F). { |
|||
T = F; |
|||
} |
|||
|
|||
factor(F) ::= factor(L) POWER factor(R). { |
|||
F = NODE::CreateBinOpRaw(L, '^', R); |
|||
} |
|||
|
|||
factor(F) ::= MINUS factor(R). [UMINUS] { |
|||
F = NODE::CreateBinOpRaw(NODE::CreateNumberRaw(0.0), '-', R); |
|||
} |
|||
|
|||
factor(F) ::= PLUS factor(R). [UMINUS] { |
|||
F = R; |
|||
} |
|||
|
|||
factor(F) ::= LPAREN expression(E) RPAREN. { |
|||
F = E; |
|||
} |
|||
|
|||
factor(F) ::= NUMBER(N). { |
|||
try |
|||
{ |
|||
F = NODE::CreateNumberRaw( N.isString ? std::stod( GetTokenString( N ) ) : GetTokenDouble( N ) ); |
|||
} |
|||
catch (const std::exception&) |
|||
{ |
|||
if (g_errorCollector) |
|||
{ |
|||
g_errorCollector->AddError( std::format( "Invalid number format: {}", GetTokenString( N ) ) ); |
|||
} |
|||
F = NODE::CreateNumberRaw( 0.0 ); |
|||
} |
|||
} |
|||
|
|||
factor(F) ::= STRING(S). { |
|||
F = NODE::CreateStringRaw( GetTokenString( S ) ); |
|||
} |
|||
|
|||
factor(F) ::= variable(V). { |
|||
F = V; |
|||
} |
|||
|
|||
factor(F) ::= function_call(FC). { |
|||
F = FC; |
|||
} |
|||
|
|||
function_call(FC) ::= IDENTIFIER(I) LPAREN RPAREN. { |
|||
auto empty_args = new std::vector<std::unique_ptr<NODE>>(); |
|||
FC = NODE::CreateFunctionRaw(GetTokenString(I), empty_args); |
|||
} |
|||
|
|||
function_call(FC) ::= IDENTIFIER(I) LPAREN arg_list(AL) RPAREN. { |
|||
FC = NODE::CreateFunctionRaw(GetTokenString(I), AL); |
|||
} |
|||
|
|||
arg_list(AL) ::= expression(E). { |
|||
AL = new std::vector<std::unique_ptr<NODE>>(); |
|||
AL->emplace_back(std::unique_ptr<NODE>(E)); |
|||
} |
|||
|
|||
arg_list(AL) ::= arg_list(AL) COMMA expression(E). { |
|||
AL->emplace_back(std::unique_ptr<NODE>(E)); |
|||
} |
|||
|
|||
variable(V) ::= DOLLAR_OPEN IDENTIFIER(I) CLOSE_BRACE. { |
|||
V = NODE::CreateVarRaw(GetTokenString(I)); |
|||
} |
|||
|
|||
%syntax_error { |
|||
if (g_errorCollector) { |
|||
g_errorCollector->AddSyntaxError(); |
|||
} |
|||
} |
|||
|
|||
%parse_failure { |
|||
if (g_errorCollector) { |
|||
g_errorCollector->AddParseFailure(); |
|||
} |
|||
} |
@ -0,0 +1,636 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.txt for contributors. |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify it |
|||
* under the terms of the GNU General Public License as published by the |
|||
* Free Software Foundation, either version 3 of the License, or (at your |
|||
* option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, but |
|||
* WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|||
* General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License along |
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
*/ |
|||
|
|||
#include <text_eval/text_eval_parser.h>
|
|||
#include <array>
|
|||
#include <cctype>
|
|||
|
|||
namespace calc_parser |
|||
{ |
|||
thread_local ERROR_COLLECTOR* g_errorCollector = nullptr; |
|||
|
|||
class DATE_UTILS |
|||
{ |
|||
private: |
|||
static constexpr int epochYear = 1970; |
|||
static constexpr std::array<int, 12> daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; |
|||
static constexpr std::array<const char*, 12> monthNames = { |
|||
"January", "February", "March", "April", "May", "June", |
|||
"July", "August", "September", "October", "November", "December" |
|||
}; |
|||
static constexpr std::array<const char*, 12> monthAbbrev = { |
|||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", |
|||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" |
|||
}; |
|||
static constexpr std::array<const char*, 7> weekdayNames = { |
|||
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" |
|||
}; |
|||
|
|||
static auto isLeapYear( int aYear ) -> bool |
|||
{ |
|||
return ( aYear % 4 == 0 && aYear % 100 != 0 ) || ( aYear % 400 == 0 ); |
|||
} |
|||
|
|||
static auto daysInYear( int aYear ) -> int |
|||
{ |
|||
return isLeapYear( aYear ) ? 366 : 365; |
|||
} |
|||
|
|||
static auto daysInMonthForYear( int aMonth, int aYear ) -> int |
|||
{ |
|||
if( aMonth == 2 && isLeapYear( aYear ) ) |
|||
return 29; |
|||
|
|||
return daysInMonth[aMonth - 1]; |
|||
} |
|||
|
|||
public: |
|||
static auto DaysToYmd( int aDaysSinceEpoch ) -> std::tuple<int, int, int> |
|||
{ |
|||
int year = epochYear; |
|||
int remainingDays = aDaysSinceEpoch; |
|||
|
|||
if( remainingDays >= 0 ) |
|||
{ |
|||
while( remainingDays >= daysInYear( year ) ) |
|||
{ |
|||
remainingDays -= daysInYear( year ); |
|||
year++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
while( remainingDays < 0 ) |
|||
{ |
|||
year--; |
|||
remainingDays += daysInYear( year ); |
|||
} |
|||
} |
|||
|
|||
int month = 1; |
|||
while( month <= 12 && remainingDays >= daysInMonthForYear( month, year ) ) |
|||
{ |
|||
remainingDays -= daysInMonthForYear( month, year ); |
|||
month++; |
|||
} |
|||
|
|||
int day = remainingDays + 1; |
|||
return {year, month, day}; |
|||
} |
|||
|
|||
static auto YmdToDays( int aYear, int aMonth, int aDay ) -> int |
|||
{ |
|||
int totalDays = 0; |
|||
|
|||
if( aYear >= epochYear ) |
|||
{ |
|||
for( int y = epochYear; y < aYear; ++y ) |
|||
totalDays += daysInYear( y ); |
|||
} |
|||
else |
|||
{ |
|||
for( int y = aYear; y < epochYear; ++y ) |
|||
totalDays -= daysInYear( y ); |
|||
} |
|||
|
|||
for( int m = 1; m < aMonth; ++m ) |
|||
totalDays += daysInMonthForYear( m, aYear ); |
|||
|
|||
totalDays += aDay - 1; |
|||
return totalDays; |
|||
} |
|||
|
|||
static auto ParseDate( const std::string& aDateStr ) -> std::optional<int> |
|||
{ |
|||
std::istringstream iss( aDateStr ); |
|||
std::string token; |
|||
std::vector<int> parts; |
|||
|
|||
char separator = 0; |
|||
bool isCjkFormat = false; |
|||
|
|||
// Check for CJK date formats first (Chinese, Korean, or mixed)
|
|||
bool hasChineseYear = aDateStr.find( "年" ) != std::string::npos; |
|||
bool hasChineseMonth = aDateStr.find( "月" ) != std::string::npos; |
|||
bool hasChineseDay = aDateStr.find( "日" ) != std::string::npos; |
|||
bool hasKoreanYear = aDateStr.find( "년" ) != std::string::npos; |
|||
bool hasKoreanMonth = aDateStr.find( "월" ) != std::string::npos; |
|||
bool hasKoreanDay = aDateStr.find( "일" ) != std::string::npos; |
|||
|
|||
// Check if we have any CJK date format (pure or mixed)
|
|||
if( (hasChineseYear || hasKoreanYear) && |
|||
(hasChineseMonth || hasKoreanMonth) && |
|||
(hasChineseDay || hasKoreanDay) ) |
|||
{ |
|||
// CJK format: Support pure Chinese, pure Korean, or mixed formats
|
|||
isCjkFormat = true; |
|||
|
|||
size_t yearPos, monthPos, dayPos; |
|||
|
|||
// Find year position and marker
|
|||
if( hasChineseYear ) |
|||
yearPos = aDateStr.find( "年" ); |
|||
else |
|||
yearPos = aDateStr.find( "년" ); |
|||
|
|||
// Find month position and marker
|
|||
if( hasChineseMonth ) |
|||
monthPos = aDateStr.find( "月" ); |
|||
else |
|||
monthPos = aDateStr.find( "월" ); |
|||
|
|||
// Find day position and marker
|
|||
if( hasChineseDay ) |
|||
dayPos = aDateStr.find( "日" ); |
|||
else |
|||
dayPos = aDateStr.find( "일" ); |
|||
|
|||
try |
|||
{ |
|||
int year = std::stoi( aDateStr.substr( 0, yearPos ) ); |
|||
int month = std::stoi( aDateStr.substr( yearPos + 3, monthPos - yearPos - 3 ) ); // 3 bytes for CJK year marker
|
|||
int day = std::stoi( aDateStr.substr( monthPos + 3, dayPos - monthPos - 3 ) ); // 3 bytes for CJK month marker
|
|||
|
|||
parts = { year, month, day }; |
|||
} |
|||
catch( ... ) |
|||
{ |
|||
return std::nullopt; |
|||
} |
|||
} |
|||
else if( aDateStr.find( '-' ) != std::string::npos ) |
|||
separator = '-'; |
|||
else if( aDateStr.find( '/' ) != std::string::npos ) |
|||
separator = '/'; |
|||
else if( aDateStr.find( '.' ) != std::string::npos ) |
|||
separator = '.'; |
|||
|
|||
if( separator ) |
|||
{ |
|||
while( std::getline( iss, token, separator ) ) |
|||
{ |
|||
try |
|||
{ |
|||
parts.push_back( std::stoi( token ) ); |
|||
} |
|||
catch( ... ) |
|||
{ |
|||
return std::nullopt; |
|||
} |
|||
} |
|||
} |
|||
else if( !isCjkFormat && aDateStr.length() == 8 ) |
|||
{ |
|||
try |
|||
{ |
|||
int dateNum = std::stoi( aDateStr ); |
|||
int year = dateNum / 10000; |
|||
int month = ( dateNum / 100 ) % 100; |
|||
int day = dateNum % 100; |
|||
return YmdToDays( year, month, day ); |
|||
} |
|||
catch( ... ) |
|||
{ |
|||
return std::nullopt; |
|||
} |
|||
} |
|||
else if( !isCjkFormat ) |
|||
{ |
|||
return std::nullopt; |
|||
} |
|||
|
|||
if( parts.empty() || parts.size() > 3 ) |
|||
return std::nullopt; |
|||
|
|||
int year, month, day; |
|||
|
|||
if( parts.size() == 1 ) |
|||
{ |
|||
year = parts[0]; |
|||
month = 1; |
|||
day = 1; |
|||
} |
|||
else if( parts.size() == 2 ) |
|||
{ |
|||
year = parts[0]; |
|||
month = parts[1]; |
|||
day = 1; |
|||
} |
|||
else |
|||
{ |
|||
if( isCjkFormat ) |
|||
{ |
|||
// CJK formats are always in YYYY年MM月DD日 or YYYY년 MM월 DD일 order
|
|||
year = parts[0]; |
|||
month = parts[1]; |
|||
day = parts[2]; |
|||
} |
|||
else if( separator == '/' && parts[0] <= 12 && parts[1] <= 31 ) |
|||
{ |
|||
month = parts[0]; |
|||
day = parts[1]; |
|||
year = parts[2]; |
|||
} |
|||
else if( separator == '/' && parts[1] <= 12 ) |
|||
{ |
|||
day = parts[0]; |
|||
month = parts[1]; |
|||
year = parts[2]; |
|||
} |
|||
else |
|||
{ |
|||
year = parts[0]; |
|||
month = parts[1]; |
|||
day = parts[2]; |
|||
} |
|||
} |
|||
|
|||
if( month < 1 || month > 12 ) |
|||
return std::nullopt; |
|||
if( day < 1 || day > daysInMonthForYear( month, year ) ) |
|||
return std::nullopt; |
|||
|
|||
return YmdToDays( year, month, day ); |
|||
} |
|||
|
|||
static auto FormatDate( int aDaysSinceEpoch, const std::string& aFormat ) -> std::string |
|||
{ |
|||
auto [year, month, day] = DaysToYmd( aDaysSinceEpoch ); |
|||
|
|||
if( aFormat == "ISO" || aFormat == "iso" ) |
|||
return std::format( "{:04d}-{:02d}-{:02d}", year, month, day ); |
|||
else if( aFormat == "US" || aFormat == "us" ) |
|||
return std::format( "{:02d}/{:02d}/{:04d}", month, day, year ); |
|||
else if( aFormat == "EU" || aFormat == "european" ) |
|||
return std::format( "{:02d}/{:02d}/{:04d}", day, month, year ); |
|||
else if( aFormat == "long" ) |
|||
return std::format( "{} {}, {}", monthNames[month-1], day, year ); |
|||
else if( aFormat == "short" ) |
|||
return std::format( "{} {}, {}", monthAbbrev[month-1], day, year ); |
|||
else if( aFormat == "Chinese" || aFormat == "chinese" || aFormat == "CN" || aFormat == "cn" || aFormat == "中文" ) |
|||
return std::format( "{}年{:02d}月{:02d}日", year, month, day ); |
|||
else if( aFormat == "Japanese" || aFormat == "japanese" || aFormat == "JP" || aFormat == "jp" || aFormat == "日本語" ) |
|||
return std::format( "{}年{:02d}月{:02d}日", year, month, day ); |
|||
else if( aFormat == "Korean" || aFormat == "korean" || aFormat == "KR" || aFormat == "kr" || aFormat == "한국어" ) |
|||
return std::format( "{}년 {:02d}월 {:02d}일", year, month, day ); |
|||
else |
|||
return std::format( "{:04d}-{:02d}-{:02d}", year, month, day ); |
|||
} |
|||
|
|||
static auto GetWeekdayName( int aDaysSinceEpoch ) -> std::string |
|||
{ |
|||
int weekday = ( ( aDaysSinceEpoch + 3 ) % 7 ); // +3 because epoch was Thursday (Monday = 0)
|
|||
|
|||
if( weekday < 0 ) |
|||
weekday += 7; |
|||
|
|||
return std::string{ weekdayNames[weekday] }; |
|||
} |
|||
|
|||
static auto GetCurrentDays() -> int |
|||
{ |
|||
auto now = std::chrono::system_clock::now(); |
|||
auto timeT = std::chrono::system_clock::to_time_t( now ); |
|||
return static_cast<int>( timeT / ( 24 * 3600 ) ); |
|||
} |
|||
|
|||
static auto GetCurrentTimestamp() -> double |
|||
{ |
|||
auto now = std::chrono::system_clock::now(); |
|||
auto timeT = std::chrono::system_clock::to_time_t( now ); |
|||
return static_cast<double>( timeT ); |
|||
} |
|||
}; |
|||
|
|||
EVAL_VISITOR::EVAL_VISITOR( VariableCallback aVariableCallback, ERROR_COLLECTOR& aErrorCollector ) : |
|||
m_variableCallback( std::move( aVariableCallback ) ), |
|||
m_errors( aErrorCollector ), |
|||
m_gen( m_rd() ) |
|||
{} |
|||
|
|||
auto EVAL_VISITOR::operator()( const NODE& aNode ) const -> Result<Value> |
|||
{ |
|||
switch( aNode.type ) |
|||
{ |
|||
case NodeType::Number: |
|||
return MakeValue<Value>( std::get<double>( aNode.data ) ); |
|||
|
|||
case NodeType::String: |
|||
return MakeValue<Value>( std::get<std::string>( aNode.data ) ); |
|||
|
|||
case NodeType::Var: |
|||
{ |
|||
const auto& varName = std::get<std::string>( aNode.data ); |
|||
|
|||
// Use callback to resolve variable
|
|||
if( m_variableCallback ) |
|||
return m_variableCallback( varName ); |
|||
|
|||
return MakeError<Value>( std::format( "No variable resolver configured for: {}", varName ) ); |
|||
} |
|||
|
|||
case NodeType::BinOp: |
|||
{ |
|||
const auto& binop = std::get<BIN_OP_DATA>( aNode.data ); |
|||
auto leftResult = binop.left->Accept( *this ); |
|||
if( !leftResult ) |
|||
return leftResult; |
|||
|
|||
auto rightResult = binop.right ? |
|||
binop.right->Accept( *this ) : MakeValue<Value>( 0.0 ); |
|||
if( !rightResult ) |
|||
return rightResult; |
|||
|
|||
// Special handling for string concatenation with +
|
|||
if( binop.op == '+' ) |
|||
{ |
|||
const auto& leftVal = leftResult.GetValue(); |
|||
const auto& rightVal = rightResult.GetValue(); |
|||
|
|||
// If either operand is a string, concatenate
|
|||
if( std::holds_alternative<std::string>( leftVal ) || |
|||
std::holds_alternative<std::string>( rightVal ) ) |
|||
{ |
|||
return MakeValue<Value>( VALUE_UTILS::ConcatStrings( leftVal, rightVal ) ); |
|||
} |
|||
} |
|||
|
|||
// Otherwise, perform arithmetic
|
|||
return VALUE_UTILS::ArithmeticOp( leftResult.GetValue(), rightResult.GetValue(), binop.op ); |
|||
} |
|||
|
|||
case NodeType::Function: |
|||
{ |
|||
const auto& func = std::get<FUNC_DATA>( aNode.data ); |
|||
return evaluateFunction( func ); |
|||
} |
|||
|
|||
default: |
|||
return MakeError<Value>( "Cannot evaluate this node type" ); |
|||
} |
|||
} |
|||
|
|||
auto EVAL_VISITOR::evaluateFunction( const FUNC_DATA& aFunc ) const -> Result<Value> |
|||
{ |
|||
const auto& name = aFunc.name; |
|||
const auto& args = aFunc.args; |
|||
|
|||
// Zero-argument functions
|
|||
if( args.empty() ) |
|||
{ |
|||
if( name == "today" ) |
|||
return MakeValue<Value>( static_cast<double>( DATE_UTILS::GetCurrentDays() ) ); |
|||
else if( name == "now" ) |
|||
return MakeValue<Value>( DATE_UTILS::GetCurrentTimestamp() ); |
|||
else if( name == "random" ) |
|||
{ |
|||
std::uniform_real_distribution<double> dis( 0.0, 1.0 ); |
|||
return MakeValue<Value>( dis( m_gen ) ); |
|||
} |
|||
} |
|||
|
|||
// Evaluate arguments to mixed types
|
|||
std::vector<Value> argValues; |
|||
argValues.reserve( args.size() ); |
|||
|
|||
for( const auto& arg : args ) |
|||
{ |
|||
auto result = arg->Accept( *this ); |
|||
if( !result ) |
|||
return result; |
|||
|
|||
argValues.push_back( result.GetValue() ); |
|||
} |
|||
|
|||
const auto argc = argValues.size(); |
|||
|
|||
// String formatting functions (return strings!)
|
|||
if( name == "format" && argc >= 1 ) |
|||
{ |
|||
auto numResult = VALUE_UTILS::ToDouble( argValues[0] ); |
|||
if( !numResult ) |
|||
return MakeError<Value>( numResult.GetError() ); |
|||
|
|||
const auto value = numResult.GetValue(); |
|||
int decimals = 2; |
|||
if( argc > 1 ) |
|||
{ |
|||
auto decResult = VALUE_UTILS::ToDouble( argValues[1] ); |
|||
if( decResult ) |
|||
decimals = static_cast<int>( decResult.GetValue() ); |
|||
} |
|||
|
|||
return MakeValue<Value>( std::format( "{:.{}f}", value, decimals ) ); |
|||
} |
|||
else if( name == "currency" && argc >= 1 ) |
|||
{ |
|||
auto numResult = VALUE_UTILS::ToDouble( argValues[0] ); |
|||
if( !numResult ) |
|||
return MakeError<Value>( numResult.GetError() ); |
|||
|
|||
const auto amount = numResult.GetValue(); |
|||
const auto symbol = argc > 1 ? VALUE_UTILS::ToString( argValues[1] ) : "$"; |
|||
|
|||
return MakeValue<Value>( std::format( "{}{:.2f}", symbol, amount ) ); |
|||
} |
|||
else if( name == "fixed" && argc >= 1 ) |
|||
{ |
|||
auto numResult = VALUE_UTILS::ToDouble( argValues[0] ); |
|||
if( !numResult ) |
|||
return MakeError<Value>( numResult.GetError() ); |
|||
|
|||
const auto value = numResult.GetValue(); |
|||
int decimals = 2; |
|||
if( argc > 1 ) |
|||
{ |
|||
auto decResult = VALUE_UTILS::ToDouble( argValues[1] ); |
|||
if( decResult ) |
|||
decimals = static_cast<int>( decResult.GetValue() ); |
|||
} |
|||
|
|||
return MakeValue<Value>( std::format( "{:.{}f}", value, decimals ) ); |
|||
} |
|||
|
|||
// Date formatting functions (return strings!)
|
|||
else if( name == "dateformat" && argc >= 1 ) |
|||
{ |
|||
auto dateResult = VALUE_UTILS::ToDouble( argValues[0] ); |
|||
if( !dateResult ) |
|||
return MakeError<Value>( dateResult.GetError() ); |
|||
|
|||
const auto days = static_cast<int>( dateResult.GetValue() ); |
|||
const auto format = argc > 1 ? VALUE_UTILS::ToString( argValues[1] ) : "ISO"; |
|||
|
|||
return MakeValue<Value>( DATE_UTILS::FormatDate( days, format ) ); |
|||
} |
|||
else if( name == "datestring" && argc == 1 ) |
|||
{ |
|||
auto dateStr = VALUE_UTILS::ToString( argValues[0] ); |
|||
auto daysResult = DATE_UTILS::ParseDate( dateStr ); |
|||
|
|||
if( !daysResult ) |
|||
return MakeError<Value>( "Invalid date format: " + dateStr ); |
|||
|
|||
return MakeValue<Value>( static_cast<double>( daysResult.value() ) ); |
|||
} |
|||
else if( name == "weekdayname" && argc == 1 ) |
|||
{ |
|||
auto dateResult = VALUE_UTILS::ToDouble( argValues[0] ); |
|||
if( !dateResult ) |
|||
return MakeError<Value>( dateResult.GetError() ); |
|||
|
|||
const auto days = static_cast<int>( dateResult.GetValue() ); |
|||
return MakeValue<Value>( DATE_UTILS::GetWeekdayName( days ) ); |
|||
} |
|||
|
|||
// String functions (return strings!)
|
|||
else if( name == "upper" && argc == 1 ) |
|||
{ |
|||
auto str = VALUE_UTILS::ToString( argValues[0] ); |
|||
std::transform( str.begin(), str.end(), str.begin(), ::toupper ); |
|||
return MakeValue<Value>( str ); |
|||
} |
|||
else if( name == "lower" && argc == 1 ) |
|||
{ |
|||
auto str = VALUE_UTILS::ToString( argValues[0] ); |
|||
std::transform( str.begin(), str.end(), str.begin(), ::tolower ); |
|||
return MakeValue<Value>( str ); |
|||
} |
|||
else if( name == "concat" && argc >= 2 ) |
|||
{ |
|||
std::string result; |
|||
for( const auto& val : argValues ) |
|||
result += VALUE_UTILS::ToString( val ); |
|||
|
|||
return MakeValue<Value>( result ); |
|||
} |
|||
|
|||
// Conditional functions (handle mixed types)
|
|||
if( name == "if" && argc == 3 ) |
|||
{ |
|||
// Convert only the condition to a number
|
|||
auto conditionResult = VALUE_UTILS::ToDouble( argValues[0] ); |
|||
if( !conditionResult ) |
|||
return MakeError<Value>( conditionResult.GetError() ); |
|||
|
|||
const auto condition = conditionResult.GetValue() != 0.0; |
|||
return MakeValue<Value>( condition ? argValues[1] : argValues[2] ); |
|||
} |
|||
|
|||
// Mathematical functions (return numbers) - convert args to doubles first
|
|||
std::vector<double> numArgs; |
|||
for( const auto& val : argValues ) |
|||
{ |
|||
auto numResult = VALUE_UTILS::ToDouble( val ); |
|||
if( !numResult ) |
|||
return MakeError<Value>( numResult.GetError() ); |
|||
|
|||
numArgs.push_back( numResult.GetValue() ); |
|||
} |
|||
|
|||
// Mathematical function implementations
|
|||
if( name == "abs" && argc == 1 ) |
|||
return MakeValue<Value>( std::abs( numArgs[0] ) ); |
|||
else if( name == "sum" && argc >= 1 ) |
|||
return MakeValue<Value>( std::accumulate( numArgs.begin(), numArgs.end(), 0.0 ) ); |
|||
else if( name == "round" && argc >= 1 ) |
|||
{ |
|||
const auto value = numArgs[0]; |
|||
const auto precision = argc > 1 ? static_cast<int>( numArgs[1] ) : 0; |
|||
const auto multiplier = std::pow( 10.0, precision ); |
|||
return MakeValue<Value>( std::round( value * multiplier ) / multiplier ); |
|||
} |
|||
else if( name == "sqrt" && argc == 1 ) |
|||
{ |
|||
if( numArgs[0] < 0 ) |
|||
return MakeError<Value>( "Square root of negative number" ); |
|||
|
|||
return MakeValue<Value>( std::sqrt( numArgs[0] ) ); |
|||
} |
|||
else if( name == "pow" && argc == 2 ) |
|||
return MakeValue<Value>( std::pow( numArgs[0], numArgs[1] ) ); |
|||
else if( name == "floor" && argc == 1 ) |
|||
return MakeValue<Value>( std::floor( numArgs[0] ) ); |
|||
else if( name == "ceil" && argc == 1 ) |
|||
return MakeValue<Value>( std::ceil( numArgs[0] ) ); |
|||
else if( name == "min" && argc >= 1 ) |
|||
return MakeValue<Value>( *std::min_element( numArgs.begin(), numArgs.end() ) ); |
|||
else if( name == "max" && argc >= 1 ) |
|||
return MakeValue<Value>( *std::max_element( numArgs.begin(), numArgs.end() ) ); |
|||
else if( name == "avg" && argc >= 1 ) |
|||
{ |
|||
const auto sum = std::accumulate( numArgs.begin(), numArgs.end(), 0.0 ); |
|||
return MakeValue<Value>( sum / static_cast<double>( argc ) ); |
|||
} |
|||
|
|||
return MakeError<Value>( std::format( "Unknown function: {} with {} arguments", name, argc ) ); |
|||
} |
|||
|
|||
auto DOC_PROCESSOR::Process( const DOC& aDoc, VariableCallback aVariableCallback ) |
|||
-> std::pair<std::string, bool> |
|||
{ |
|||
std::string result; |
|||
auto localErrors = ERROR_COLLECTOR{}; |
|||
EVAL_VISITOR evaluator{ std::move( aVariableCallback ), localErrors }; |
|||
bool hadErrors = aDoc.HasErrors(); |
|||
|
|||
for( const auto& node : aDoc.GetNodes() ) |
|||
{ |
|||
switch( node->type ) |
|||
{ |
|||
case NodeType::Text: |
|||
result += std::get<std::string>( node->data ); |
|||
break; |
|||
|
|||
case NodeType::Calc: |
|||
{ |
|||
const auto& calcData = std::get<BIN_OP_DATA>( node->data ); |
|||
auto evalResult = calcData.left->Accept( evaluator ); |
|||
|
|||
if( evalResult ) |
|||
result += VALUE_UTILS::ToString( evalResult.GetValue() ); |
|||
else |
|||
{ |
|||
// Don't add error formatting to result - errors go to error vector only
|
|||
// The higher level will return original input unchanged if there are errors
|
|||
hadErrors = true; |
|||
} |
|||
break; |
|||
} |
|||
|
|||
default: |
|||
result += "[Unknown node type]"; |
|||
hadErrors = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return { std::move( result ), hadErrors || localErrors.HasErrors() }; |
|||
} |
|||
|
|||
auto DOC_PROCESSOR::ProcessWithDetails( const DOC& aDoc, VariableCallback aVariableCallback ) |
|||
-> std::tuple<std::string, std::vector<std::string>, bool> |
|||
{ |
|||
auto [result, hadErrors] = Process( aDoc, std::move( aVariableCallback ) ); |
|||
auto allErrors = aDoc.GetErrors(); |
|||
|
|||
return { std::move( result ), std::move( allErrors ), hadErrors }; |
|||
} |
|||
|
|||
} // namespace calc_parser
|
2068
common/text_eval/text_eval_wrapper.cpp
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,454 @@ |
|||
/* |
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.txt for contributors. |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify it |
|||
* under the terms of the GNU General Public License as published by the |
|||
* Free Software Foundation, either version 3 of the License, or (at your |
|||
* option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, but |
|||
* WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|||
* General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License along |
|||
* with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
*/ |
|||
#pragma once |
|||
#include <fast_float/fast_float.h> |
|||
#include <kicommon.h> |
|||
#include <text_eval/text_eval_types.h> |
|||
#include <iostream> |
|||
#include <string> |
|||
#include <memory> |
|||
#include <vector> |
|||
#include <variant> |
|||
#include <concepts> |
|||
#include <ranges> |
|||
#include <format> |
|||
#include <optional> |
|||
#include <cassert> |
|||
#include <cmath> |
|||
#include <chrono> |
|||
#include <random> |
|||
#include <numeric> |
|||
#include <algorithm> |
|||
#include <sstream> |
|||
#include <iomanip> |
|||
#include <unordered_map> |
|||
#include <functional> |
|||
#include <cstring> |
|||
|
|||
#ifndef M_PI |
|||
#define M_PI 3.14159265358979323846 |
|||
#endif |
|||
|
|||
namespace calc_parser |
|||
{ |
|||
using Value = std::variant<double, std::string>; |
|||
|
|||
// Simple token type for parser compatibility |
|||
struct TOKEN_TYPE |
|||
{ |
|||
char text[256]; // Fixed size buffer for strings |
|||
double dValue; // Numeric value |
|||
bool isString; // Flag to indicate if this is a string token |
|||
}; |
|||
|
|||
// Helper functions for TOKEN_TYPE |
|||
inline TOKEN_TYPE MakeStringToken(const std::string& str) |
|||
{ |
|||
TOKEN_TYPE token; |
|||
token.dValue = 0.0; |
|||
token.isString = true; |
|||
strncpy(token.text, str.c_str(), sizeof(token.text) - 1); |
|||
token.text[sizeof(token.text) - 1] = '\0'; |
|||
return token; |
|||
} |
|||
|
|||
inline TOKEN_TYPE MakeNumberToken(double val) |
|||
{ |
|||
TOKEN_TYPE token; |
|||
token.dValue = val; |
|||
token.isString = false; |
|||
token.text[0] = '\0'; |
|||
return token; |
|||
} |
|||
|
|||
inline std::string GetTokenString(const TOKEN_TYPE& token) |
|||
{ |
|||
return std::string(token.text); |
|||
} |
|||
|
|||
inline double GetTokenDouble(const TOKEN_TYPE& token) |
|||
{ |
|||
return token.dValue; |
|||
} |
|||
|
|||
// Value utilities for type handling |
|||
class VALUE_UTILS |
|||
{ |
|||
public: |
|||
// Convert Value to double (for arithmetic operations) |
|||
static auto ToDouble( const Value& aVal ) -> Result<double> |
|||
{ |
|||
if( std::holds_alternative<double>( aVal ) ) |
|||
return MakeValue( std::get<double>( aVal ) ); |
|||
|
|||
const auto& str = std::get<std::string>( aVal ); |
|||
try |
|||
{ |
|||
double value; |
|||
auto result = fast_float::from_chars( str.data(), str.data() + str.size(), value ); |
|||
|
|||
if( result.ec != std::errc() || result.ptr != str.data() + str.size() ) |
|||
throw std::invalid_argument( "Invalid number format" ); |
|||
|
|||
return MakeValue( value ); |
|||
} |
|||
catch( ... ) |
|||
{ |
|||
return MakeError<double>( std::format( "Cannot convert '{}' to number", str ) ); |
|||
} |
|||
} |
|||
|
|||
// Convert Value to string (for display/concatenation) |
|||
static auto ToString( const Value& aVal ) -> std::string |
|||
{ |
|||
if( std::holds_alternative<std::string>( aVal ) ) |
|||
return std::get<std::string>( aVal ); |
|||
|
|||
const auto num = std::get<double>( aVal ); |
|||
|
|||
// Smart number formatting with tolerance for floating-point precision |
|||
constexpr double tolerance = 1e-10; |
|||
double rounded = std::round( num ); |
|||
|
|||
// If the number is very close to a whole number, treat it as such |
|||
if( std::abs( num - rounded ) < tolerance && std::abs( rounded ) < 1e15 ) |
|||
return std::format( "{:.0f}", rounded ); |
|||
|
|||
return std::format( "{}", num ); |
|||
} |
|||
|
|||
// Check if Value represents a "truthy" value for conditionals |
|||
static auto IsTruthy( const Value& aVal ) -> bool |
|||
{ |
|||
if( std::holds_alternative<double>( aVal ) ) |
|||
return std::get<double>( aVal ) != 0.0; |
|||
|
|||
return !std::get<std::string>( aVal ).empty(); |
|||
} |
|||
|
|||
// arithmetic operation with type coercion |
|||
static auto ArithmeticOp( const Value& aLeft, const Value& aRight, char aOp ) -> Result<Value> |
|||
{ |
|||
auto leftNum = ToDouble( aLeft ); |
|||
auto rightNum = ToDouble( aRight ); |
|||
|
|||
if( !leftNum ) return MakeError<Value>( leftNum.GetError() ); |
|||
if( !rightNum ) return MakeError<Value>( rightNum.GetError() ); |
|||
|
|||
const auto leftVal = leftNum.GetValue(); |
|||
const auto rightVal = rightNum.GetValue(); |
|||
|
|||
switch( aOp ) |
|||
{ |
|||
case '+': return MakeValue<Value>( leftVal + rightVal ); |
|||
case '-': return MakeValue<Value>( leftVal - rightVal ); |
|||
case '*': return MakeValue<Value>( leftVal * rightVal ); |
|||
case '/': |
|||
if( rightVal == 0.0 ) |
|||
return MakeError<Value>( "Division by zero" ); |
|||
return MakeValue<Value>( leftVal / rightVal ); |
|||
case '%': |
|||
if( rightVal == 0.0 ) |
|||
return MakeError<Value>( "Modulo by zero" ); |
|||
return MakeValue<Value>( std::fmod( leftVal, rightVal ) ); |
|||
case '^': return MakeValue<Value>( std::pow( leftVal, rightVal ) ); |
|||
case '<': return MakeValue<Value>( leftVal < rightVal ? 1.0 : 0.0 ); |
|||
case '>': return MakeValue<Value>( leftVal > rightVal ? 1.0 : 0.0 ); |
|||
case 1: return MakeValue<Value>( leftVal <= rightVal ? 1.0 : 0.0 ); // <= |
|||
case 2: return MakeValue<Value>( leftVal >= rightVal ? 1.0 : 0.0 ); // >= |
|||
case 3: return MakeValue<Value>( leftVal == rightVal ? 1.0 : 0.0 ); // == |
|||
case 4: return MakeValue<Value>( leftVal != rightVal ? 1.0 : 0.0 ); // != |
|||
default: |
|||
return MakeError<Value>( "Unknown operator" ); |
|||
} |
|||
} |
|||
|
|||
// String concatenation (special case of '+' for strings) |
|||
static auto ConcatStrings( const Value& aLeft, const Value& aRight ) -> Value |
|||
{ |
|||
return Value{ ToString( aLeft ) + ToString( aRight ) }; |
|||
} |
|||
}; |
|||
|
|||
class NODE; |
|||
class DOC; |
|||
class PARSE_CONTEXT; |
|||
|
|||
// AST Node types - supporting mixed values |
|||
enum class NodeType { Text, Calc, Var, Number, String, BinOp, Function }; |
|||
|
|||
struct BIN_OP_DATA |
|||
{ |
|||
std::unique_ptr<NODE> left; |
|||
std::unique_ptr<NODE> right; |
|||
char op; |
|||
|
|||
BIN_OP_DATA( std::unique_ptr<NODE> aLeft, char aOperation, std::unique_ptr<NODE> aRight ) : |
|||
left( std::move( aLeft ) ), |
|||
right( std::move( aRight ) ), |
|||
op( aOperation ) |
|||
{} |
|||
}; |
|||
|
|||
struct FUNC_DATA |
|||
{ |
|||
std::string name; |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
|
|||
FUNC_DATA( std::string aName, std::vector<std::unique_ptr<NODE>> aArguments ) : |
|||
name( std::move( aName ) ), |
|||
args( std::move( aArguments ) ) |
|||
{} |
|||
}; |
|||
|
|||
class NODE |
|||
{ |
|||
public: |
|||
NodeType type; |
|||
std::variant<std::string, double, BIN_OP_DATA, FUNC_DATA> data; |
|||
|
|||
// Factory methods for type safety |
|||
static auto CreateText( std::string aText ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::Text; |
|||
node->data = std::move( aText ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateCalc( std::unique_ptr<NODE> aExpr ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::Calc; |
|||
node->data = BIN_OP_DATA( std::move( aExpr ), '=', nullptr ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateVar( std::string aName ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::Var; |
|||
node->data = std::move( aName ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateNumber( double aValue ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::Number; |
|||
node->data = aValue; |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateString( std::string aValue ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::String; |
|||
node->data = std::move( aValue ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateBinOp( std::unique_ptr<NODE> aLeft, char aOp, std::unique_ptr<NODE> aRight ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::BinOp; |
|||
node->data = BIN_OP_DATA( std::move( aLeft ), aOp, std::move( aRight ) ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateFunction( std::string aName, std::vector<std::unique_ptr<NODE>> aArgs ) -> std::unique_ptr<NODE> |
|||
{ |
|||
auto node = std::make_unique<NODE>(); |
|||
node->type = NodeType::Function; |
|||
node->data = FUNC_DATA( std::move( aName ), std::move( aArgs ) ); |
|||
return node; |
|||
} |
|||
|
|||
// Raw pointer factory methods for parser use |
|||
static auto CreateTextRaw( std::string aText ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::Text; |
|||
node->data = std::move( aText ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateCalcRaw( NODE* aExpr ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::Calc; |
|||
node->data = BIN_OP_DATA( std::unique_ptr<NODE>( aExpr ), '=', nullptr ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateVarRaw( std::string aName ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::Var; |
|||
node->data = std::move( aName ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateNumberRaw( double aValue ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::Number; |
|||
node->data = aValue; |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateStringRaw( std::string aValue ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::String; |
|||
node->data = std::move( aValue ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateBinOpRaw( NODE* aLeft, char aOp, NODE* aRight ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::BinOp; |
|||
node->data = BIN_OP_DATA( std::unique_ptr<NODE>( aLeft ), aOp, std::unique_ptr<NODE>( aRight ) ); |
|||
return node; |
|||
} |
|||
|
|||
static auto CreateFunctionRaw( std::string aName, std::vector<std::unique_ptr<NODE>>* aArgs ) -> NODE* |
|||
{ |
|||
auto node = new NODE(); |
|||
node->type = NodeType::Function; |
|||
node->data = FUNC_DATA( std::move( aName ), std::move( *aArgs ) ); |
|||
delete aArgs; |
|||
return node; |
|||
} |
|||
|
|||
// Mixed-type evaluation |
|||
template<typename Visitor> |
|||
auto Accept( Visitor&& aVisitor ) const -> Result<Value> |
|||
{ |
|||
return std::forward<Visitor>( aVisitor )( *this ); |
|||
} |
|||
}; |
|||
|
|||
class DOC |
|||
{ |
|||
public: |
|||
std::vector<std::unique_ptr<NODE>> nodes; |
|||
mutable ERROR_COLLECTOR errors; |
|||
|
|||
auto AddNode( std::unique_ptr<NODE> aNode ) -> void |
|||
{ |
|||
nodes.emplace_back( std::move( aNode ) ); |
|||
} |
|||
|
|||
auto AddNodeRaw( NODE* aNode ) -> void |
|||
{ |
|||
nodes.emplace_back( std::unique_ptr<NODE>( aNode ) ); |
|||
} |
|||
|
|||
auto HasErrors() const -> bool { return errors.HasErrors(); } |
|||
auto GetErrors() const -> const std::vector<std::string>& { return errors.GetErrors(); } |
|||
auto GetErrorSummary() const -> std::string { return errors.GetAllMessages(); } |
|||
|
|||
auto GetNodes() const -> const auto& { return nodes; } |
|||
auto begin() const { return nodes.begin(); } |
|||
auto end() const { return nodes.end(); } |
|||
}; |
|||
|
|||
// Global error collector for parser callbacks |
|||
extern thread_local ERROR_COLLECTOR* g_errorCollector; |
|||
|
|||
class PARSE_CONTEXT |
|||
{ |
|||
public: |
|||
ERROR_COLLECTOR& errors; |
|||
|
|||
explicit PARSE_CONTEXT( ERROR_COLLECTOR& aErrorCollector ) : |
|||
errors( aErrorCollector ) |
|||
{ |
|||
g_errorCollector = &aErrorCollector; |
|||
} |
|||
|
|||
~PARSE_CONTEXT() |
|||
{ |
|||
g_errorCollector = nullptr; |
|||
} |
|||
|
|||
PARSE_CONTEXT( const PARSE_CONTEXT& ) = delete; |
|||
PARSE_CONTEXT& operator=( const PARSE_CONTEXT& ) = delete; |
|||
PARSE_CONTEXT( PARSE_CONTEXT&& ) = delete; |
|||
PARSE_CONTEXT& operator=( PARSE_CONTEXT&& ) = delete; |
|||
}; |
|||
|
|||
// Enhanced evaluation visitor supporting callback-based variable resolution |
|||
class KICOMMON_API EVAL_VISITOR |
|||
{ |
|||
public: |
|||
// Callback function type for variable resolution |
|||
using VariableCallback = std::function<Result<Value>(const std::string& aVariableName)>; |
|||
|
|||
private: |
|||
VariableCallback m_variableCallback; |
|||
[[maybe_unused]] ERROR_COLLECTOR& m_errors; |
|||
mutable std::random_device m_rd; |
|||
mutable std::mt19937 m_gen; |
|||
|
|||
public: |
|||
/** |
|||
* @brief Construct evaluator with variable callback function |
|||
* @param aVariableCallback Function to call when resolving variables |
|||
* @param aErrorCollector Error collector for storing errors |
|||
*/ |
|||
explicit EVAL_VISITOR( VariableCallback aVariableCallback, ERROR_COLLECTOR& aErrorCollector ); |
|||
|
|||
// Visitor methods for evaluating different node types |
|||
auto operator()( const NODE& aNode ) const -> Result<Value>; |
|||
|
|||
private: |
|||
auto evaluateFunction( const FUNC_DATA& aFunc ) const -> Result<Value>; |
|||
}; |
|||
|
|||
// Enhanced document processor supporting callback-based variable resolution |
|||
class KICOMMON_API DOC_PROCESSOR |
|||
{ |
|||
public: |
|||
using VariableCallback = EVAL_VISITOR::VariableCallback; |
|||
|
|||
/** |
|||
* @brief Process document using callback for variable resolution |
|||
* @param aDoc Document to process |
|||
* @param aVariableCallback Function to resolve variables |
|||
* @return Pair of (result_string, had_errors) |
|||
*/ |
|||
static auto Process( const DOC& aDoc, VariableCallback aVariableCallback ) |
|||
-> std::pair<std::string, bool>; |
|||
|
|||
/** |
|||
* @brief Process document with detailed error reporting |
|||
* @param aDoc Document to process |
|||
* @param aVariableCallback Function to resolve variables |
|||
* @return Tuple of (result_string, error_messages, had_errors) |
|||
*/ |
|||
static auto ProcessWithDetails( const DOC& aDoc, VariableCallback aVariableCallback ) |
|||
-> std::tuple<std::string, std::vector<std::string>, bool>; |
|||
}; |
|||
|
|||
|
|||
} // namespace calc_parser |
|||
|
@ -0,0 +1,120 @@ |
|||
/* |
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.txt for contributors. |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify it |
|||
* under the terms of the GNU General Public License as published by the |
|||
* Free Software Foundation, either version 3 of the License, or (at your |
|||
* option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, but |
|||
* WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|||
* General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License along |
|||
* with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
*/ |
|||
|
|||
#pragma once |
|||
|
|||
#include <string> |
|||
#include <variant> |
|||
#include <vector> |
|||
#include <format> |
|||
|
|||
namespace calc_parser |
|||
{ |
|||
using Value = std::variant<double, std::string>; |
|||
|
|||
template<typename T> |
|||
class Result |
|||
{ |
|||
private: |
|||
std::variant<T, std::string> m_data; |
|||
|
|||
public: |
|||
Result( T aValue ) : m_data( std::move( aValue ) ) {} |
|||
Result( std::string aError ) : m_data( std::move( aError ) ) {} |
|||
|
|||
auto HasValue() const -> bool { return std::holds_alternative<T>( m_data ); } |
|||
auto HasError() const -> bool { return std::holds_alternative<std::string>( m_data ); } |
|||
|
|||
auto GetValue() const -> const T& { return std::get<T>( m_data ); } |
|||
auto GetError() const -> const std::string& { return std::get<std::string>( m_data ); } |
|||
|
|||
explicit operator bool() const { return HasValue(); } |
|||
}; |
|||
|
|||
template<typename T> |
|||
auto MakeError( std::string aMsg ) -> Result<T> |
|||
{ |
|||
return Result<T>( std::move( aMsg ) ); |
|||
} |
|||
|
|||
template<typename T> |
|||
auto MakeValue( T aVal ) -> Result<T> |
|||
{ |
|||
return Result<T>( std::move( aVal ) ); |
|||
} |
|||
|
|||
class ERROR_COLLECTOR |
|||
{ |
|||
private: |
|||
std::vector<std::string> m_errors; |
|||
std::vector<std::string> m_warnings; |
|||
|
|||
public: |
|||
auto AddError( std::string aError ) -> void |
|||
{ |
|||
m_errors.emplace_back( std::move( aError ) ); |
|||
} |
|||
|
|||
auto AddWarning( std::string aWarning ) -> void |
|||
{ |
|||
m_warnings.emplace_back( std::move( aWarning ) ); |
|||
} |
|||
|
|||
auto AddSyntaxError( int aLine = -1, int aColumn = -1 ) -> void |
|||
{ |
|||
if( aLine >= 0 && aColumn >= 0 ) |
|||
AddError( std::format( "Syntax error at line {}, column {}", aLine, aColumn ) ); |
|||
else |
|||
AddError( "Syntax error in calculation expression" ); |
|||
} |
|||
|
|||
auto AddParseFailure() -> void |
|||
{ |
|||
AddError( "Parser failed to parse input" ); |
|||
} |
|||
|
|||
auto HasErrors() const -> bool { return !m_errors.empty(); } |
|||
auto HasWarnings() const -> bool { return !m_warnings.empty(); } |
|||
auto GetErrors() const -> const std::vector<std::string>& { return m_errors; } |
|||
auto GetWarnings() const -> const std::vector<std::string>& { return m_warnings; } |
|||
|
|||
auto GetAllMessages() const -> std::string |
|||
{ |
|||
std::string result; |
|||
for( const auto& error : m_errors ) |
|||
result += std::format( "Error: {}\n", error ); |
|||
|
|||
for( const auto& warning : m_warnings ) |
|||
result += std::format( "Warning: {}\n", warning ); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
auto Clear() -> void |
|||
{ |
|||
m_errors.clear(); |
|||
m_warnings.clear(); |
|||
} |
|||
}; |
|||
|
|||
// Forward declarations for parser-related types |
|||
class DOC; |
|||
class PARSE_CONTEXT; |
|||
class DOC_PROCESSOR; |
|||
} |
@ -0,0 +1,267 @@ |
|||
/* |
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.txt for contributors. |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify it |
|||
* under the terms of the GNU General Public License as published by the |
|||
* Free Software Foundation, either version 3 of the License, or (at your |
|||
* option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, but |
|||
* WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|||
* General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License along |
|||
* with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
*/ |
|||
|
|||
#pragma once |
|||
|
|||
#include <kicommon.h> |
|||
#include <magic_enum.hpp> |
|||
#include <string> |
|||
#include <string_view> |
|||
#include <vector> |
|||
#include <array> |
|||
#include <algorithm> |
|||
#include <optional> |
|||
|
|||
namespace text_eval_units { |
|||
|
|||
/** |
|||
* @brief Enumeration of all supported units in the text evaluation system |
|||
* |
|||
* This enum defines all units that can be parsed and converted by the text evaluator. |
|||
* The order matters for parsing - longer unit strings should come first to ensure |
|||
* proper matching (e.g., "ps/mm" before "ps", "thou" before "th"). |
|||
*/ |
|||
enum class Unit { |
|||
// Multi-character composite units (longest first for proper parsing) |
|||
PS_PER_MM, // "ps/mm" - picoseconds per millimeter |
|||
PS_PER_CM, // "ps/cm" - picoseconds per centimeter |
|||
PS_PER_IN, // "ps/in" - picoseconds per inch |
|||
|
|||
// Multi-character simple units |
|||
THOU, // "thou" - thousandths of an inch (same as mil) |
|||
DEG, // "deg" - degrees |
|||
|
|||
// Common single and double character units |
|||
MM, // "mm" - millimeters |
|||
CM, // "cm" - centimeters |
|||
IN, // "in" - inches |
|||
MIL, // "mil" - mils (thousandths of an inch) |
|||
UM, // "um" - micrometers |
|||
PS, // "ps" - picoseconds |
|||
FS, // "fs" - femtoseconds |
|||
|
|||
// Single character units |
|||
INCH_QUOTE, // "\"" - inches (using quote character) |
|||
DEGREE_SYMBOL, // "°" - degrees (using degree symbol) |
|||
|
|||
// Invalid/unknown unit |
|||
INVALID |
|||
}; |
|||
|
|||
/** |
|||
* @brief Unit registry that provides centralized unit string mapping and conversion |
|||
* |
|||
* This class uses magic_enum to provide compile-time unit string mapping and |
|||
* runtime unit parsing/conversion capabilities. All unit-related operations |
|||
* in the text evaluator should use this registry to ensure consistency. |
|||
*/ |
|||
class KICOMMON_API UnitRegistry { |
|||
public: |
|||
/** |
|||
* @brief Unit information structure |
|||
*/ |
|||
struct UnitInfo { |
|||
Unit unit; |
|||
std::string_view unitString; |
|||
std::string_view description; |
|||
double conversionToMM; // Conversion factor to millimeters (base unit) |
|||
}; |
|||
|
|||
private: |
|||
// Static unit information table ordered by parsing priority (longest first) |
|||
static constexpr std::array<UnitInfo, 15> s_unitTable = {{ |
|||
// Multi-character composite units first (longest matches) |
|||
{Unit::PS_PER_MM, "ps/mm", "Picoseconds per millimeter", 1.0}, |
|||
{Unit::PS_PER_CM, "ps/cm", "Picoseconds per centimeter", 1.0}, |
|||
{Unit::PS_PER_IN, "ps/in", "Picoseconds per inch", 1.0}, |
|||
|
|||
// Multi-character simple units |
|||
{Unit::THOU, "thou", "Thousandths of an inch", 25.4 / 1000.0}, |
|||
{Unit::DEG, "deg", "Degrees", 1.0}, |
|||
|
|||
// Common units |
|||
{Unit::MM, "mm", "Millimeters", 1.0}, |
|||
{Unit::CM, "cm", "Centimeters", 10.0}, |
|||
{Unit::IN, "in", "Inches", 25.4}, |
|||
{Unit::MIL, "mil", "Mils (thousandths of an inch)", 25.4 / 1000.0}, |
|||
{Unit::UM, "um", "Micrometers", 1.0 / 1000.0}, |
|||
{Unit::PS, "ps", "Picoseconds", 1.0}, |
|||
{Unit::FS, "fs", "Femtoseconds", 1.0}, |
|||
|
|||
// Single character units (must be last for proper parsing) |
|||
{Unit::INCH_QUOTE, "\"", "Inches (quote notation)", 25.4}, |
|||
{Unit::DEGREE_SYMBOL, "°", "Degrees (symbol)", 1.0}, |
|||
|
|||
// Invalid marker |
|||
{Unit::INVALID, "", "Invalid/unknown unit", 1.0} |
|||
}}; |
|||
|
|||
public: |
|||
/** |
|||
* @brief Parse a unit string and return the corresponding Unit enum |
|||
* @param unitStr The unit string to parse |
|||
* @return The Unit enum value, or Unit::INVALID if not recognized |
|||
*/ |
|||
static constexpr Unit parseUnit(std::string_view unitStr) noexcept { |
|||
if (unitStr.empty()) { |
|||
return Unit::INVALID; |
|||
} |
|||
|
|||
// Search through unit table (ordered by priority) |
|||
for (const auto& info : s_unitTable) { |
|||
if (info.unit != Unit::INVALID && info.unitString == unitStr) { |
|||
return info.unit; |
|||
} |
|||
} |
|||
|
|||
return Unit::INVALID; |
|||
} |
|||
|
|||
/** |
|||
* @brief Get the unit string for a given Unit enum |
|||
* @param unit The Unit enum value |
|||
* @return The unit string, or empty string if invalid |
|||
*/ |
|||
static constexpr std::string_view getUnitString(Unit unit) noexcept { |
|||
for (const auto& info : s_unitTable) { |
|||
if (info.unit == unit) { |
|||
return info.unitString; |
|||
} |
|||
} |
|||
return ""; |
|||
} |
|||
|
|||
/** |
|||
* @brief Get all unit strings in parsing order (longest first) |
|||
* @return Vector of all supported unit strings |
|||
*/ |
|||
static std::vector<std::string> getAllUnitStrings() { |
|||
std::vector<std::string> units; |
|||
units.reserve(s_unitTable.size() - 1); // Exclude INVALID |
|||
|
|||
for (const auto& info : s_unitTable) { |
|||
if (info.unit != Unit::INVALID && !info.unitString.empty()) { |
|||
units.emplace_back(info.unitString); |
|||
} |
|||
} |
|||
|
|||
return units; |
|||
} |
|||
|
|||
/** |
|||
* @brief Get conversion factor from one unit to another |
|||
* @param fromUnit Source unit |
|||
* @param toUnit Target unit |
|||
* @return Conversion factor, or 1.0 if conversion not supported |
|||
*/ |
|||
static constexpr double getConversionFactor(Unit fromUnit, Unit toUnit) noexcept { |
|||
if (fromUnit == toUnit) { |
|||
return 1.0; |
|||
} |
|||
|
|||
// Find conversion factors for both units |
|||
double fromToMM = 1.0; |
|||
double toFromMM = 1.0; |
|||
|
|||
for (const auto& info : s_unitTable) { |
|||
if (info.unit == fromUnit) { |
|||
fromToMM = info.conversionToMM; |
|||
} else if (info.unit == toUnit) { |
|||
toFromMM = 1.0 / info.conversionToMM; |
|||
} |
|||
} |
|||
|
|||
return fromToMM * toFromMM; |
|||
} |
|||
|
|||
/** |
|||
* @brief Convert EDA_UNITS to text evaluator Unit enum |
|||
* @param edaUnits The EDA_UNITS value |
|||
* @return Corresponding Unit enum value |
|||
*/ |
|||
static constexpr Unit fromEdaUnits(EDA_UNITS edaUnits) noexcept { |
|||
switch (edaUnits) { |
|||
case EDA_UNITS::MM: return Unit::MM; |
|||
case EDA_UNITS::CM: return Unit::CM; |
|||
case EDA_UNITS::MILS: return Unit::MIL; |
|||
case EDA_UNITS::INCH: return Unit::IN; |
|||
case EDA_UNITS::DEGREES: return Unit::DEG; |
|||
case EDA_UNITS::FS: return Unit::FS; |
|||
case EDA_UNITS::PS: return Unit::PS; |
|||
case EDA_UNITS::PS_PER_INCH: return Unit::PS_PER_IN; |
|||
case EDA_UNITS::PS_PER_CM: return Unit::PS_PER_CM; |
|||
case EDA_UNITS::PS_PER_MM: return Unit::PS_PER_MM; |
|||
case EDA_UNITS::UM: return Unit::UM; |
|||
default: return Unit::MM; // Default fallback |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @brief Convert a value with units to target units |
|||
* @param value The value to convert |
|||
* @param fromUnit Source unit |
|||
* @param toUnit Target unit |
|||
* @return Converted value |
|||
*/ |
|||
static constexpr double convertValue(double value, Unit fromUnit, Unit toUnit) noexcept { |
|||
return value * getConversionFactor(fromUnit, toUnit); |
|||
} |
|||
|
|||
/** |
|||
* @brief Convert a value with unit string to target EDA_UNITS |
|||
* @param value The value to convert |
|||
* @param unitStr Source unit string |
|||
* @param targetUnits Target EDA_UNITS |
|||
* @return Converted value |
|||
*/ |
|||
static double convertToEdaUnits(double value, std::string_view unitStr, EDA_UNITS targetUnits) { |
|||
Unit fromUnit = parseUnit(unitStr); |
|||
if (fromUnit == Unit::INVALID) { |
|||
return value; // No conversion for invalid units |
|||
} |
|||
|
|||
Unit toUnit = fromEdaUnits(targetUnits); |
|||
return convertValue(value, fromUnit, toUnit); |
|||
} |
|||
|
|||
/** |
|||
* @brief Check if a string is a valid unit |
|||
* @param unitStr The string to check |
|||
* @return True if the string represents a valid unit |
|||
*/ |
|||
static constexpr bool isValidUnit(std::string_view unitStr) noexcept { |
|||
return parseUnit(unitStr) != Unit::INVALID; |
|||
} |
|||
|
|||
/** |
|||
* @brief Get unit information for debugging/display purposes |
|||
* @param unit The unit to get information for |
|||
* @return Optional UnitInfo structure, nullopt if unit is invalid |
|||
*/ |
|||
static std::optional<UnitInfo> getUnitInfo(Unit unit) noexcept { |
|||
for (const auto& info : s_unitTable) { |
|||
if (info.unit == unit) { |
|||
return info; |
|||
} |
|||
} |
|||
return std::nullopt; |
|||
} |
|||
}; |
|||
|
|||
} // namespace text_eval_units |
@ -0,0 +1,529 @@ |
|||
/* |
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.txt for contributors. |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify it |
|||
* under the terms of the GNU General Public License as published by the |
|||
* Free Software Foundation, either version 3 of the License, or (at your |
|||
* option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, but |
|||
* WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|||
* General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License along |
|||
* with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
*/ |
|||
|
|||
#pragma once |
|||
|
|||
#include <kicommon.h> |
|||
#include <wx/string.h> |
|||
#include <unordered_map> |
|||
#include <string> |
|||
#include <memory> |
|||
#include <variant> |
|||
#include <vector> |
|||
#include <functional> |
|||
|
|||
// Include EDA units support |
|||
#include <eda_units.h> |
|||
|
|||
// Include the parser types |
|||
#include <text_eval/text_eval_types.h> |
|||
|
|||
// Forward declaration for NUMERIC_EVALUATOR compatibility |
|||
class NUMERIC_EVALUATOR_COMPAT; |
|||
|
|||
/** |
|||
* @brief High-level wrapper for evaluating mathematical and string expressions in wxString format |
|||
* |
|||
* This class provides a simple interface for evaluating expressions containing @{} syntax |
|||
* within wxString objects. It supports both map-based variable lookup and flexible |
|||
* callback-based variable resolution for dynamic data access. |
|||
* |
|||
* The evaluator can work in two modes: |
|||
* 1. Static variable mode: Variables are stored internally and looked up from memory |
|||
* 2. Callback mode: Variables are resolved dynamically using a user-provided function |
|||
* |
|||
* Example usage: |
|||
* @code |
|||
* // Static variable mode |
|||
* EXPRESSION_EVALUATOR evaluator; |
|||
* evaluator.SetVariable("price", 99.99); |
|||
* evaluator.SetVariable("product", "Widget"); |
|||
* evaluator.SetVariable("qty", 3); |
|||
* |
|||
* wxString input = "Product: @{upper(${product})} - Total: @{currency(${price} * ${qty})}"; |
|||
* wxString result = evaluator.Evaluate(input); |
|||
* // Result: "Product: WIDGET - Total: $299.97" |
|||
* |
|||
* // Callback mode |
|||
* auto callback = [](const std::string& varName) -> calc_parser::Result<calc_parser::Value> { |
|||
* if (varName == "current_time") { |
|||
* return calc_parser::MakeValue<calc_parser::Value>(getCurrentTimestamp()); |
|||
* } |
|||
* return calc_parser::MakeError<calc_parser::Value>("Variable not found: " + varName); |
|||
* }; |
|||
* EXPRESSION_EVALUATOR callbackEvaluator(callback); |
|||
* wxString result2 = callbackEvaluator.Evaluate("Current time: @{${current_time}}"); |
|||
* @endcode |
|||
*/ |
|||
class KICOMMON_API EXPRESSION_EVALUATOR |
|||
{ |
|||
public: |
|||
// Callback function type for dynamic variable resolution |
|||
using VariableCallback = std::function<calc_parser::Result<calc_parser::Value>(const std::string& aVariableName)>; |
|||
|
|||
private: |
|||
std::unordered_map<std::string, calc_parser::Value> m_variables; |
|||
mutable std::unique_ptr<calc_parser::ERROR_COLLECTOR> m_lastErrors; |
|||
bool m_clearVariablesOnEvaluate; |
|||
VariableCallback m_customCallback; |
|||
bool m_useCustomCallback; |
|||
EDA_UNITS m_defaultUnits; // Default units for calculations |
|||
|
|||
public: |
|||
/** |
|||
* @brief Construct a new Expression Evaluator in static variable mode |
|||
* @param aClearVariablesOnEvaluate If true, variables are cleared after each evaluation |
|||
*/ |
|||
explicit EXPRESSION_EVALUATOR( bool aClearVariablesOnEvaluate = false ); |
|||
|
|||
/** |
|||
* @brief Construct with default units support |
|||
* @param aUnits Default units for parsing and evaluating expressions |
|||
* @param aClearVariablesOnEvaluate If true, variables are cleared after each evaluation |
|||
*/ |
|||
explicit EXPRESSION_EVALUATOR( EDA_UNITS aUnits, bool aClearVariablesOnEvaluate = false ); |
|||
|
|||
/** |
|||
* @brief Construct with custom variable resolver callback |
|||
* @param aVariableCallback Custom function for variable resolution |
|||
* @param aClearVariablesOnEvaluate If true, local variables are cleared after evaluation |
|||
*/ |
|||
explicit EXPRESSION_EVALUATOR( VariableCallback aVariableCallback, |
|||
bool aClearVariablesOnEvaluate = false ); |
|||
|
|||
/** |
|||
* @brief Construct with units and custom variable resolver callback |
|||
* @param aUnits Default units for parsing and evaluating expressions |
|||
* @param aVariableCallback Custom function for variable resolution |
|||
* @param aClearVariablesOnEvaluate If true, local variables are cleared after evaluation |
|||
*/ |
|||
explicit EXPRESSION_EVALUATOR( EDA_UNITS aUnits, VariableCallback aVariableCallback, |
|||
bool aClearVariablesOnEvaluate = false ); |
|||
|
|||
/** |
|||
* @brief Destructor |
|||
*/ |
|||
~EXPRESSION_EVALUATOR(); |
|||
|
|||
// Copy and move operations |
|||
EXPRESSION_EVALUATOR( const EXPRESSION_EVALUATOR& aOther ); |
|||
EXPRESSION_EVALUATOR& operator=( const EXPRESSION_EVALUATOR& aOther ); |
|||
EXPRESSION_EVALUATOR( EXPRESSION_EVALUATOR&& aOther ) noexcept; |
|||
EXPRESSION_EVALUATOR& operator=( EXPRESSION_EVALUATOR&& aOther ) noexcept; |
|||
|
|||
/** |
|||
* @brief Set a custom variable resolver callback |
|||
* @param aCallback Function to call for variable resolution |
|||
* |
|||
* When set, this callback takes precedence over stored variables. |
|||
* The callback receives variable names and should return Result<Value>. |
|||
* Set to nullptr or call ClearVariableCallback() to disable callback mode. |
|||
*/ |
|||
void SetVariableCallback( VariableCallback aCallback ); |
|||
|
|||
/** |
|||
* @brief Clear the custom variable resolver callback |
|||
* |
|||
* After calling this, the evaluator will use stored variables only. |
|||
*/ |
|||
void ClearVariableCallback(); |
|||
|
|||
/** |
|||
* @brief Check if a custom variable callback is set |
|||
* @return true if custom callback is active |
|||
*/ |
|||
bool HasVariableCallback() const; |
|||
|
|||
/** |
|||
* @brief Set the default units for expressions |
|||
* @param aUnits The units to use as default (mm, mil, inch, etc.) |
|||
* |
|||
* When expressions contain numeric values with unit suffixes (e.g., "1mm", "25mil"), |
|||
* they will be converted to the default units for calculation. |
|||
*/ |
|||
void SetDefaultUnits( EDA_UNITS aUnits ); |
|||
|
|||
/** |
|||
* @brief Get the current default units |
|||
* @return Current default units |
|||
*/ |
|||
EDA_UNITS GetDefaultUnits() const; |
|||
|
|||
/** |
|||
* @brief Set a numeric variable for use in expressions |
|||
* @param aName Variable name (used as ${name} in expressions) |
|||
* @param aValue Numeric value |
|||
* |
|||
* This has no effect when using callback mode, unless the callback |
|||
* chooses to fall back to stored variables. |
|||
*/ |
|||
void SetVariable( const wxString& aName, double aValue ); |
|||
|
|||
/** |
|||
* @brief Set a string variable for use in expressions |
|||
* @param aName Variable name (used as ${name} in expressions) |
|||
* @param aValue String value |
|||
* |
|||
* This has no effect when using callback mode, unless the callback |
|||
* chooses to fall back to stored variables. |
|||
*/ |
|||
void SetVariable( const wxString& aName, const wxString& aValue ); |
|||
|
|||
/** |
|||
* @brief Set a variable using std::string (convenience overload) |
|||
* @param aName Variable name |
|||
* @param aValue String value |
|||
*/ |
|||
void SetVariable( const std::string& aName, const std::string& aValue ); |
|||
|
|||
/** |
|||
* @brief Remove a variable from the evaluator |
|||
* @param aName Variable name to remove |
|||
* @return true if variable was found and removed, false otherwise |
|||
*/ |
|||
bool RemoveVariable( const wxString& aName ); |
|||
|
|||
/** |
|||
* @brief Clear all stored variables |
|||
* |
|||
* This does not affect callback-based variable resolution. |
|||
*/ |
|||
void ClearVariables(); |
|||
|
|||
/** |
|||
* @brief Check if a variable exists in stored variables |
|||
* @param aName Variable name to check |
|||
* @return true if variable exists in stored variables, false otherwise |
|||
* |
|||
* Note: This only checks stored variables, not callback-resolved variables. |
|||
*/ |
|||
bool HasVariable( const wxString& aName ) const; |
|||
|
|||
/** |
|||
* @brief Get the current value of a stored variable |
|||
* @param aName Variable name |
|||
* @return Variable value as wxString, or empty string if not found |
|||
* |
|||
* Note: This only returns stored variables, not callback-resolved variables. |
|||
*/ |
|||
wxString GetVariable( const wxString& aName ) const; |
|||
|
|||
/** |
|||
* @brief Get all stored variable names currently defined |
|||
* @return Vector of variable names |
|||
* |
|||
* Note: This only returns stored variables, not callback-available variables. |
|||
*/ |
|||
std::vector<wxString> GetVariableNames() const; |
|||
|
|||
/** |
|||
* @brief Set multiple variables at once from a map |
|||
* @param aVariables Map of variable names to numeric values |
|||
*/ |
|||
void SetVariables( const std::unordered_map<wxString, double>& aVariables ); |
|||
|
|||
/** |
|||
* @brief Set multiple string variables at once from a map |
|||
* @param aVariables Map of variable names to string values |
|||
*/ |
|||
void SetVariables( const std::unordered_map<wxString, wxString>& aVariables ); |
|||
|
|||
/** |
|||
* @brief Main evaluation function - processes input string and evaluates all @{} expressions |
|||
* @param aInput Input string potentially containing @{} expressions |
|||
* @return Fully evaluated string with all expressions replaced by their values |
|||
* |
|||
* Variables are resolved using the callback (if set) or stored variables. |
|||
*/ |
|||
wxString Evaluate( const wxString& aInput ); |
|||
|
|||
/** |
|||
* @brief Evaluate with additional temporary variables (doesn't modify stored variables) |
|||
* @param aInput Input string to evaluate |
|||
* @param aTempVariables Temporary numeric variables for this evaluation only |
|||
* @return Evaluated string |
|||
* |
|||
* Temporary variables have lower priority than callback resolution but higher |
|||
* priority than stored variables. |
|||
*/ |
|||
wxString Evaluate( const wxString& aInput, |
|||
const std::unordered_map<wxString, double>& aTempVariables ); |
|||
|
|||
/** |
|||
* @brief Evaluate with mixed temporary variables |
|||
* @param aInput Input string to evaluate |
|||
* @param aTempNumericVars Temporary numeric variables |
|||
* @param aTempStringVars Temporary string variables |
|||
* @return Evaluated string |
|||
* |
|||
* Priority order: callback > temp string vars > temp numeric vars > stored variables |
|||
*/ |
|||
wxString Evaluate( const wxString& aInput, |
|||
const std::unordered_map<wxString, double>& aTempNumericVars, |
|||
const std::unordered_map<wxString, wxString>& aTempStringVars ); |
|||
|
|||
/** |
|||
* @brief Check if the last evaluation had errors |
|||
* @return true if errors occurred during last evaluation |
|||
*/ |
|||
bool HasErrors() const; |
|||
|
|||
/** |
|||
* @brief Get count of errors from the last evaluation |
|||
* @return Number of errors that occurred |
|||
*/ |
|||
size_t GetErrorCount() const; |
|||
|
|||
/** |
|||
* @brief Get detailed error information from the last evaluation |
|||
* @return Error summary as wxString, empty if no errors |
|||
*/ |
|||
wxString GetErrorSummary() const; |
|||
|
|||
/** |
|||
* @brief Get individual error messages from the last evaluation |
|||
* @return Vector of error messages |
|||
*/ |
|||
std::vector<wxString> GetErrors() const; |
|||
|
|||
/** |
|||
* @brief Clear any stored error information |
|||
*/ |
|||
void ClearErrors(); |
|||
|
|||
/** |
|||
* @brief Enable or disable automatic variable clearing after evaluation |
|||
* @param aEnable If true, stored variables are cleared after each Evaluate() call |
|||
* |
|||
* Note: This only affects stored variables, not callback behavior. |
|||
*/ |
|||
void SetClearVariablesOnEvaluate( bool aEnable ); |
|||
|
|||
/** |
|||
* @brief Check if automatic variable clearing is enabled |
|||
* @return true if variables are cleared after each evaluation |
|||
*/ |
|||
bool GetClearVariablesOnEvaluate() const; |
|||
|
|||
/** |
|||
* @brief Test if an expression can be parsed without evaluating it |
|||
* @param aExpression Single expression to test (without @{} wrapper) |
|||
* @return true if expression is syntactically valid |
|||
* |
|||
* This creates a temporary evaluator to test syntax only. |
|||
*/ |
|||
bool TestExpression( const wxString& aExpression ); |
|||
|
|||
/** |
|||
* @brief Count the number of @{} expressions in input string |
|||
* @param aInput Input string to analyze |
|||
* @return Number of @{} expression blocks found |
|||
*/ |
|||
size_t CountExpressions( const wxString& aInput ) const; |
|||
|
|||
/** |
|||
* @brief Extract all @{} expressions from input without evaluating |
|||
* @param aInput Input string to analyze |
|||
* @return Vector of expression strings (content between @{} markers) |
|||
*/ |
|||
std::vector<wxString> ExtractExpressions( const wxString& aInput ) const; |
|||
|
|||
private: |
|||
/** |
|||
* @brief Convert wxString to std::string using UTF-8 encoding |
|||
* @param aWxStr wxString to convert |
|||
* @return Converted std::string |
|||
*/ |
|||
std::string wxStringToStdString( const wxString& aWxStr ) const; |
|||
|
|||
/** |
|||
* @brief Convert std::string to wxString using UTF-8 encoding |
|||
* @param aStdStr std::string to convert |
|||
* @return Converted wxString |
|||
*/ |
|||
wxString stdStringToWxString( const std::string& aStdStr ) const; |
|||
|
|||
/** |
|||
* @brief Create a callback function that combines all variable sources |
|||
* @param aTempNumericVars Temporary numeric variables (optional) |
|||
* @param aTempStringVars Temporary string variables (optional) |
|||
* @return Combined callback for parser |
|||
*/ |
|||
VariableCallback createCombinedCallback( |
|||
const std::unordered_map<wxString, double>* aTempNumericVars = nullptr, |
|||
const std::unordered_map<wxString, wxString>* aTempStringVars = nullptr ) const; |
|||
|
|||
/** |
|||
* @brief Parse and evaluate the input string using the expression parser |
|||
* @param aInput Input string in std::string format |
|||
* @param aVariableCallback Callback function to use for variable resolution |
|||
* @return Pair of (result_string, had_errors) |
|||
*/ |
|||
std::pair<std::string, bool> evaluateWithParser( |
|||
const std::string& aInput, |
|||
VariableCallback aVariableCallback ); |
|||
|
|||
/** |
|||
* @brief Parse and evaluate with partial error recovery - malformed expressions left unchanged |
|||
* @param aInput Input string in std::string format |
|||
* @param aVariableCallback Callback function to use for variable resolution |
|||
* @return Pair of (result_string, had_errors) |
|||
*/ |
|||
std::pair<std::string, bool> evaluateWithPartialErrorRecovery( |
|||
const std::string& aInput, |
|||
VariableCallback aVariableCallback ); |
|||
|
|||
/** |
|||
* @brief Full parser evaluation (original behavior) - fails completely on any error |
|||
* @param aInput Input string in std::string format |
|||
* @param aVariableCallback Callback function to use for variable resolution |
|||
* @return Pair of (result_string, had_errors) |
|||
*/ |
|||
std::pair<std::string, bool> evaluateWithFullParser( |
|||
const std::string& aInput, |
|||
VariableCallback aVariableCallback ); |
|||
|
|||
/** |
|||
* @brief Expand ${variable} patterns that are outside @{} expressions |
|||
* @param aInput Input string to process |
|||
* @param aTempNumericVars Temporary numeric variables |
|||
* @param aTempStringVars Temporary string variables |
|||
* @return String with ${variable} patterns outside expressions expanded |
|||
*/ |
|||
wxString expandVariablesOutsideExpressions( |
|||
const wxString& aInput, |
|||
const std::unordered_map<wxString, double>& aTempNumericVars, |
|||
const std::unordered_map<wxString, wxString>& aTempStringVars ) const; |
|||
}; |
|||
|
|||
/** |
|||
* @brief NUMERIC_EVALUATOR compatible wrapper around EXPRESSION_EVALUATOR |
|||
* |
|||
* This class provides a drop-in replacement for NUMERIC_EVALUATOR that uses |
|||
* the new EXPRESSION_EVALUATOR backend. It maintains the same API to allow |
|||
* seamless migration of existing code. |
|||
* |
|||
* The key difference is that expressions are automatically wrapped in @{...} |
|||
* syntax before evaluation. |
|||
* |
|||
* Example usage: |
|||
* @code |
|||
* // Old NUMERIC_EVALUATOR code: |
|||
* NUMERIC_EVALUATOR eval(EDA_UNITS::MM); |
|||
* eval.Process("1 + 2"); |
|||
* wxString result = eval.Result(); // "3" |
|||
* |
|||
* // New compatible code: |
|||
* NUMERIC_EVALUATOR_COMPAT eval(EDA_UNITS::MM); |
|||
* eval.Process("1 + 2"); |
|||
* wxString result = eval.Result(); // "3" |
|||
* @endcode |
|||
*/ |
|||
class KICOMMON_API NUMERIC_EVALUATOR_COMPAT |
|||
{ |
|||
private: |
|||
EXPRESSION_EVALUATOR m_evaluator; |
|||
wxString m_lastInput; |
|||
wxString m_lastResult; |
|||
bool m_lastValid; |
|||
|
|||
public: |
|||
/** |
|||
* @brief Constructor with default units |
|||
* @param aUnits Default units for the evaluator |
|||
*/ |
|||
explicit NUMERIC_EVALUATOR_COMPAT( EDA_UNITS aUnits ); |
|||
|
|||
/** |
|||
* @brief Destructor |
|||
*/ |
|||
~NUMERIC_EVALUATOR_COMPAT(); |
|||
|
|||
/** |
|||
* @brief Clear parser state but retain variables |
|||
* |
|||
* Resets the parser state for processing a new expression. |
|||
* User-defined variables are retained. |
|||
*/ |
|||
void Clear(); |
|||
|
|||
/** |
|||
* @brief Set default units for evaluation |
|||
* @param aUnits The default units to use |
|||
*/ |
|||
void SetDefaultUnits( EDA_UNITS aUnits ); |
|||
|
|||
/** |
|||
* @brief Handle locale changes (for decimal separator) |
|||
* |
|||
* This is a no-op in the EXPRESSION_EVALUATOR implementation |
|||
* since it handles locale properly internally. |
|||
*/ |
|||
void LocaleChanged(); |
|||
|
|||
/** |
|||
* @brief Check if the last evaluation was successful |
|||
* @return True if last Process() call was successful |
|||
*/ |
|||
bool IsValid() const; |
|||
|
|||
/** |
|||
* @brief Get the result of the last evaluation |
|||
* @return Result string, or empty if invalid |
|||
*/ |
|||
wxString Result() const; |
|||
|
|||
/** |
|||
* @brief Process and evaluate an expression |
|||
* @param aString Expression to evaluate |
|||
* @return True if evaluation was successful |
|||
*/ |
|||
bool Process( const wxString& aString ); |
|||
|
|||
/** |
|||
* @brief Get the original input text |
|||
* @return The last input string passed to Process() |
|||
*/ |
|||
wxString OriginalText() const; |
|||
|
|||
/** |
|||
* @brief Set a variable value |
|||
* @param aString Variable name |
|||
* @param aValue Variable value |
|||
*/ |
|||
void SetVar( const wxString& aString, double aValue ); |
|||
|
|||
/** |
|||
* @brief Get a variable value |
|||
* @param aString Variable name |
|||
* @return Variable value, or 0.0 if not defined |
|||
*/ |
|||
double GetVar( const wxString& aString ); |
|||
|
|||
/** |
|||
* @brief Remove a single variable |
|||
* @param aString Variable name to remove |
|||
*/ |
|||
void RemoveVar( const wxString& aString ); |
|||
|
|||
/** |
|||
* @brief Remove all variables |
|||
*/ |
|||
void ClearVar(); |
|||
}; |
@ -0,0 +1,115 @@ |
|||
# Text Evaluation Parser Tests |
|||
|
|||
This directory contains test suites for the KiCad text evaluation parser functionality. |
|||
|
|||
## Test Files |
|||
|
|||
### `test_text_eval_parser.cpp` |
|||
High-level integration tests using the `EXPRESSION_EVALUATOR` wrapper class. |
|||
|
|||
- **Basic Arithmetic**: Addition, subtraction, multiplication, division, modulo, power operations |
|||
- **Variable Substitution**: Testing variable storage and retrieval in expressions |
|||
- **String Operations**: String concatenation, mixed string/number operations |
|||
- **Mathematical Functions**: `abs`, `sqrt`, `pow`, `floor`, `ceil`, `round`, `min`, `max`, `sum`, `avg` |
|||
- **String Functions**: `upper`, `lower`, `concat` |
|||
- **Formatting Functions**: `format`, `fixed`, `currency` |
|||
- **Date/Time Functions**: `today`, `now`, `dateformat`, `weekdayname` |
|||
- **Conditional Functions**: `if` statements with boolean logic |
|||
- **Random Functions**: `random()` number generation |
|||
- **Error Handling**: Syntax errors, runtime errors, undefined variables |
|||
- **Complex Expressions**: Nested functions, multi-step calculations |
|||
- **Performance Testing**: Large expressions and timing validation |
|||
|
|||
### `test_text_eval_parser_core.cpp` |
|||
Low-level unit tests for the core parser components. Tests the internal API including: |
|||
|
|||
- **ValueUtils**: Type conversion, arithmetic operations, string handling |
|||
- **Node Creation**: AST node factory methods and structure validation |
|||
- **EvaluationVisitor**: Direct AST evaluation with custom variable resolvers |
|||
- **Function Evaluation**: Individual function implementations and error cases |
|||
- **DocumentProcessor**: Document parsing and processing workflows |
|||
- **Error Collection**: Error reporting and message formatting |
|||
- **TokenType Utilities**: Token creation and manipulation |
|||
|
|||
### `test_text_eval_parser_datetime.cpp` |
|||
Specialized tests for date and time functionality: |
|||
|
|||
- **Date Formatting**: Various output formats (ISO, US, EU, Chinese, Japanese, Korean, long, short) |
|||
- **Current Date/Time**: `today()` and `now()` function validation |
|||
- **Date Arithmetic**: Adding/subtracting days, date calculations |
|||
- **Edge Cases**: Leap years, month boundaries, negative dates |
|||
- **Weekday Calculations**: Day-of-week determination and cycling |
|||
- **Performance**: Date operation timing validation |
|||
|
|||
### `test_text_eval_parser_integration.cpp` |
|||
Integration tests simulating real-world KiCad usage scenarios: |
|||
|
|||
- **Real-World Scenarios**: PCB documentation, title blocks, component summaries |
|||
- **Callback Variable Resolution**: Dynamic variable lookup from external sources |
|||
- **Thread Safety**: Multi-evaluator state isolation |
|||
- **Memory Management**: Large expression handling, resource cleanup |
|||
- **Parsing Edge Cases**: Whitespace, special characters, error recovery |
|||
- **Performance Testing**: Realistic workload simulation |
|||
|
|||
## Tested Functions |
|||
|
|||
### Mathematical Functions |
|||
- `abs(x)` - Absolute value |
|||
- `sqrt(x)` - Square root (with negative input validation) |
|||
- `pow(x, y)` - Power/exponentiation |
|||
- `floor(x)` - Round down to integer |
|||
- `ceil(x)` - Round up to integer |
|||
- `round(x, [precision])` - Round to specified decimal places |
|||
- `min(...)` - Minimum of multiple values |
|||
- `max(...)` - Maximum of multiple values |
|||
- `sum(...)` - Sum of multiple values |
|||
- `avg(...)` - Average of multiple values |
|||
|
|||
### String Functions |
|||
- `upper(str)` - Convert to uppercase |
|||
- `lower(str)` - Convert to lowercase |
|||
- `concat(...)` - Concatenate multiple values |
|||
- `format(num, [decimals])` - Format number with specified precision |
|||
- `fixed(num, [decimals])` - Fixed decimal formatting |
|||
- `currency(amount, [symbol])` - Currency formatting |
|||
|
|||
### Date/Time Functions |
|||
- `today()` - Current date as days since epoch |
|||
- `now()` - Current timestamp as seconds since epoch |
|||
- `dateformat(days, [format])` - Format date string |
|||
- Formats: "ISO", "US", "EU", "Chinese", "Japanese", "Korean", "long", "short" |
|||
- `weekdayname(days)` - Get weekday name for date |
|||
|
|||
### Conditional Functions |
|||
- `if(condition, true_value, false_value)` - Conditional evaluation |
|||
|
|||
### Utility Functions |
|||
- `random()` - Random number between 0 and 1 |
|||
|
|||
## Arithmetic Operators |
|||
|
|||
- `+` - Addition (also string concatenation) |
|||
- `-` - Subtraction (also unary minus) |
|||
- `*` - Multiplication |
|||
- `/` - Division (with zero-division error handling) |
|||
- `%` - Modulo (with zero-modulo error handling) |
|||
- `^` - Exponentiation (right-associative) |
|||
|
|||
## Variable Syntax |
|||
|
|||
Variables are referenced using `${variable_name}` syntax and can be: |
|||
- Set statically using `evaluator.SetVariable()` |
|||
- Resolved dynamically using callback functions |
|||
|
|||
## Expression Syntax |
|||
|
|||
Calculations are embedded in text using `@{expression}` syntax: |
|||
- `"Result: @{2 + 3}"` → `"Result: 5"` |
|||
- `"Hello ${name}!"` → `"Hello World!"` (with variable substitution) |
|||
- `"Area: @{${width} * ${height}} mm²"` → `"Area: 100 mm²"` |
|||
|
|||
## Error Handling |
|||
|
|||
The parser collects errors for later diagnostics. However, a string |
|||
with multiple expressions may be partially evaluated. It will return an error for every |
|||
expression that was not fully evaluated. |
@ -0,0 +1,697 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.TXT for contributors. |
|||
* |
|||
* This program is free software; you can redistribute it and/or |
|||
* modify it under the terms of the GNU General Public License |
|||
* as published by the Free Software Foundation; either version 2 |
|||
* of the License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License |
|||
* along with this program; if not, you may find one here: |
|||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|||
* or you may search the http://www.gnu.org website for the version 2 license,
|
|||
* or you may write to the Free Software Foundation, Inc., |
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|||
*/ |
|||
|
|||
/**
|
|||
* @file test_text_eval_numeric_compat.cpp |
|||
* Test suite for text_eval system using examples adapted from numeric_evaluator tests |
|||
*/ |
|||
|
|||
#include <qa_utils/wx_utils/unit_test_utils.h>
|
|||
|
|||
// Code under test
|
|||
#include <text_eval/text_eval_wrapper.h>
|
|||
|
|||
// Make EDA_UNITS printable for Boost.Test
|
|||
std::ostream& operator<<( std::ostream& aStream, EDA_UNITS aUnits ) |
|||
{ |
|||
wxString unitStr = EDA_UNIT_UTILS::GetText( aUnits ); |
|||
return aStream << unitStr.ToStdString(); |
|||
} |
|||
|
|||
/**
|
|||
* Declare the test suite |
|||
*/ |
|||
BOOST_AUTO_TEST_SUITE( TextEvalNumericCompat ) |
|||
|
|||
/**
|
|||
* Struct representing a test case adapted from numeric evaluator |
|||
*/ |
|||
struct TEXT_EVAL_CASE |
|||
{ |
|||
wxString input; // Input expression wrapped in @{} for text_eval
|
|||
wxString exp_result; // Expected result as string
|
|||
bool shouldError; // Whether this case should produce an error
|
|||
}; |
|||
|
|||
/**
|
|||
* Basic functionality test |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( Basic ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
wxString result = evaluator.Evaluate("@{1}"); |
|||
BOOST_CHECK_EQUAL( result, "1" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
} |
|||
|
|||
/**
|
|||
* Variable setting and usage test |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( SetVar ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Set variable and test usage
|
|||
evaluator.SetVariable( "MoL", 42.0 ); |
|||
wxString result = evaluator.Evaluate( "@{1 + ${MoL}}" ); |
|||
BOOST_CHECK_EQUAL( result, "43" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Change variable value
|
|||
evaluator.SetVariable( "MoL", 422.0 ); |
|||
result = evaluator.Evaluate( "@{1 + ${MoL}}" ); |
|||
BOOST_CHECK_EQUAL( result, "423" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Add another variable
|
|||
evaluator.SetVariable( "pi", 3.14 ); |
|||
BOOST_CHECK( evaluator.HasVariable( "pi" ) ); |
|||
|
|||
// Remove one variable
|
|||
BOOST_CHECK( evaluator.RemoveVariable( "pi" ) ); |
|||
BOOST_CHECK( !evaluator.HasVariable( "pi" ) ); |
|||
|
|||
// Other variable should still be there
|
|||
BOOST_CHECK( evaluator.HasVariable( "MoL" ) ); |
|||
|
|||
// Add another variable back
|
|||
evaluator.SetVariable( "piish", 3.1 ); |
|||
|
|||
// Test multiple variables
|
|||
result = evaluator.Evaluate( "@{1 + ${MoL} + ${piish}}" ); |
|||
BOOST_CHECK_EQUAL( result, "426.1" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Clear all variables
|
|||
evaluator.ClearVariables(); |
|||
BOOST_CHECK( !evaluator.HasVariable( "MoL" ) ); |
|||
BOOST_CHECK( !evaluator.HasVariable( "piish" ) ); |
|||
} |
|||
|
|||
/**
|
|||
* A list of valid test cases adapted from numeric evaluator |
|||
* All expressions are wrapped in @{} to use the text_eval system |
|||
*/ |
|||
static const std::vector<TEXT_EVAL_CASE> eval_cases_valid = { |
|||
// Empty case - text_eval handles this differently than numeric evaluator
|
|||
{ "@{}", "@{}", true }, // Empty expressions should error in text_eval
|
|||
|
|||
// Trivial eval
|
|||
{ "@{1}", "1", false }, |
|||
|
|||
// Decimal separators (text_eval may handle differently)
|
|||
{ "@{1.5}", "1.5", false }, |
|||
// Note: comma as decimal separator might not work in text_eval
|
|||
|
|||
// Simple arithmetic
|
|||
{ "@{1+2}", "3", false }, |
|||
{ "@{1 + 2}", "3", false }, |
|||
{ "@{1.5 + 0.2 + 0.1}", "1.8", false }, |
|||
{ "@{3 - 10}", "-7", false }, |
|||
{ "@{1 + 2 + 10 + 1000.05}", "1013.05", false }, |
|||
|
|||
// Operator precedence
|
|||
{ "@{1 + 2 - 4 * 20 / 2}", "-37", false }, |
|||
|
|||
// Parentheses
|
|||
{ "@{(1)}", "1", false }, |
|||
{ "@{-(1 + (2 - 4)) * 20.8 / 2}", "10.4", false }, |
|||
|
|||
// Unary operators
|
|||
{ "@{+2 - 1}", "1", false }, |
|||
}; |
|||
|
|||
/**
|
|||
* A list of invalid test cases adapted from numeric evaluator |
|||
*/ |
|||
static const std::vector<TEXT_EVAL_CASE> eval_cases_invalid = { |
|||
// Trailing operator
|
|||
{ "@{1+}", "", true }, |
|||
|
|||
// Leading operator (except unary)
|
|||
{ "@{*2 + 1}", "", true }, |
|||
|
|||
// Division by zero
|
|||
{ "@{1 / 0}", "", true }, |
|||
|
|||
// Unknown variables should preserve the original expression
|
|||
{ "@{1 + ${unknown}}", "@{1 + ${unknown}}", true }, |
|||
|
|||
// Mismatched parentheses
|
|||
{ "@{(1 + 2}", "", true }, |
|||
{ "@{1 + 2)}", "", true }, |
|||
|
|||
// Invalid syntax
|
|||
{ "@{1 $ 2}", "", true }, |
|||
}; |
|||
|
|||
/**
|
|||
* Run through valid test cases |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ValidResults ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
for( const auto& testCase : eval_cases_valid ) |
|||
{ |
|||
BOOST_TEST_CONTEXT( testCase.input + " -> " + testCase.exp_result ) |
|||
{ |
|||
wxString result = evaluator.Evaluate( testCase.input ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.exp_result ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Run through invalid test cases |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( InvalidResults ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
for( const auto& testCase : eval_cases_invalid ) |
|||
{ |
|||
BOOST_TEST_CONTEXT( testCase.input ) |
|||
{ |
|||
wxString result = evaluator.Evaluate( testCase.input ); |
|||
|
|||
// All these cases should produce errors
|
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
|
|||
// For undefined variables, result should be the original expression
|
|||
if( testCase.input.Contains( "${unknown}" ) ) |
|||
{ |
|||
BOOST_CHECK_EQUAL( result, testCase.input ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test variable usage with more complex expressions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( VariableExpressions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Set up variables similar to numeric evaluator tests
|
|||
evaluator.SetVariable( "x", 10.0 ); |
|||
evaluator.SetVariable( "y", 5.0 ); |
|||
|
|||
struct VarTestCase { |
|||
wxString input; |
|||
wxString expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<VarTestCase> varCases = { |
|||
{ "@{${x}}", "10", false }, |
|||
{ "@{${y}}", "5", false }, |
|||
{ "@{${x} + ${y}}", "15", false }, |
|||
{ "@{${x} * ${y}}", "50", false }, |
|||
{ "@{${x} - ${y}}", "5", false }, |
|||
{ "@{${x} / ${y}}", "2", false }, |
|||
{ "@{(${x} + ${y}) * 2}", "30", false }, |
|||
|
|||
// Undefined variable should preserve expression
|
|||
{ "@{${undefined}}", "@{${undefined}}", true }, |
|||
|
|||
// Mixed defined and undefined
|
|||
{ "@{${x} + ${undefined}}", "@{${x} + ${undefined}}", true }, |
|||
}; |
|||
|
|||
for( const auto& testCase : varCases ) |
|||
{ |
|||
BOOST_TEST_CONTEXT( testCase.input + " -> " + testCase.expected ) |
|||
{ |
|||
wxString result = evaluator.Evaluate( testCase.input ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.input ); // Original expression preserved
|
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test mathematical functions available in text_eval |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( MathFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct MathTestCase { |
|||
wxString input; |
|||
wxString expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<MathTestCase> mathCases = { |
|||
// Basic math functions that are confirmed to work
|
|||
{ "@{abs(-5)}", "5", false }, |
|||
{ "@{min(3, 7)}", "3", false }, |
|||
{ "@{max(3, 7)}", "7", false }, |
|||
{ "@{sqrt(16)}", "4", false }, |
|||
{ "@{ceil(3.2)}", "4", false }, |
|||
{ "@{floor(3.8)}", "3", false }, |
|||
{ "@{round(3.6)}", "4", false }, |
|||
{ "@{pow(2, 3)}", "8", false }, |
|||
|
|||
// Sum and average functions
|
|||
{ "@{sum(1, 2, 3)}", "6", false }, |
|||
{ "@{avg(2, 4, 6)}", "4", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : mathCases ) |
|||
{ |
|||
BOOST_TEST_CONTEXT( testCase.input + " -> " + testCase.expected ) |
|||
{ |
|||
wxString result = evaluator.Evaluate( testCase.input ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test unit support functionality |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( UnitSupport ) |
|||
{ |
|||
// Test basic unit constructor
|
|||
EXPRESSION_EVALUATOR evaluator_mm( EDA_UNITS::MM ); |
|||
EXPRESSION_EVALUATOR evaluator_inch( EDA_UNITS::INCH ); |
|||
EXPRESSION_EVALUATOR evaluator_mil( EDA_UNITS::MILS ); |
|||
|
|||
// Test unit setting and getting
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.GetDefaultUnits(), EDA_UNITS::MM ); |
|||
BOOST_CHECK_EQUAL( evaluator_inch.GetDefaultUnits(), EDA_UNITS::INCH ); |
|||
BOOST_CHECK_EQUAL( evaluator_mil.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
|
|||
// Test unit change
|
|||
evaluator_mm.SetDefaultUnits( EDA_UNITS::INCH ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.GetDefaultUnits(), EDA_UNITS::INCH ); |
|||
|
|||
// Test basic expressions work with unit-aware evaluator
|
|||
wxString result = evaluator_mm.Evaluate( "@{1 + 2}" ); |
|||
BOOST_CHECK_EQUAL( result, "3" ); |
|||
BOOST_CHECK( !evaluator_mm.HasErrors() ); |
|||
|
|||
// Test unit constructor with variable callback
|
|||
auto callback = [](const std::string& varName) -> calc_parser::Result<calc_parser::Value> { |
|||
if (varName == "width") { |
|||
return calc_parser::MakeValue<calc_parser::Value>(10.0); |
|||
} |
|||
return calc_parser::MakeError<calc_parser::Value>("Variable not found: " + varName); |
|||
}; |
|||
|
|||
EXPRESSION_EVALUATOR evaluator_callback( EDA_UNITS::MM, callback, false ); |
|||
BOOST_CHECK_EQUAL( evaluator_callback.GetDefaultUnits(), EDA_UNITS::MM ); |
|||
BOOST_CHECK( evaluator_callback.HasVariableCallback() ); |
|||
|
|||
result = evaluator_callback.Evaluate( "@{${width} * 2}" ); |
|||
BOOST_CHECK_EQUAL( result, "20" ); |
|||
BOOST_CHECK( !evaluator_callback.HasErrors() ); |
|||
} |
|||
|
|||
/**
|
|||
* Test unit conversion infrastructure readiness |
|||
* Tests the unit support foundation without exposing internal functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( UnitInfrastructureReadiness ) |
|||
{ |
|||
// Test that different unit types can be set and retrieved
|
|||
EXPRESSION_EVALUATOR evaluator_mm( EDA_UNITS::MM ); |
|||
EXPRESSION_EVALUATOR evaluator_inch( EDA_UNITS::INCH ); |
|||
EXPRESSION_EVALUATOR evaluator_mil( EDA_UNITS::MILS ); |
|||
EXPRESSION_EVALUATOR evaluator_cm( EDA_UNITS::CM ); |
|||
EXPRESSION_EVALUATOR evaluator_um( EDA_UNITS::UM ); |
|||
|
|||
// Verify unit storage works for all supported units
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.GetDefaultUnits(), EDA_UNITS::MM ); |
|||
BOOST_CHECK_EQUAL( evaluator_inch.GetDefaultUnits(), EDA_UNITS::INCH ); |
|||
BOOST_CHECK_EQUAL( evaluator_mil.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
BOOST_CHECK_EQUAL( evaluator_cm.GetDefaultUnits(), EDA_UNITS::CM ); |
|||
BOOST_CHECK_EQUAL( evaluator_um.GetDefaultUnits(), EDA_UNITS::UM ); |
|||
|
|||
// Test unit changes
|
|||
evaluator_mm.SetDefaultUnits( EDA_UNITS::INCH ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.GetDefaultUnits(), EDA_UNITS::INCH ); |
|||
|
|||
evaluator_inch.SetDefaultUnits( EDA_UNITS::MILS ); |
|||
BOOST_CHECK_EQUAL( evaluator_inch.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
|
|||
// Verify expressions still work with all unit types
|
|||
wxString result; |
|||
|
|||
result = evaluator_mm.Evaluate( "@{5 * 2}" ); |
|||
BOOST_CHECK_EQUAL( result, "10" ); |
|||
|
|||
result = evaluator_inch.Evaluate( "@{3.5 + 1.5}" ); |
|||
BOOST_CHECK_EQUAL( result, "5" ); |
|||
|
|||
result = evaluator_mil.Evaluate( "@{100 / 4}" ); |
|||
BOOST_CHECK_EQUAL( result, "25" ); |
|||
|
|||
// Test complex expressions work with unit-aware evaluators
|
|||
result = evaluator_cm.Evaluate( "@{(10 + 5) * 2 - 1}" ); |
|||
BOOST_CHECK_EQUAL( result, "29" ); |
|||
|
|||
// Test variable support with units
|
|||
evaluator_um.SetVariable( "length", 25.4 ); |
|||
result = evaluator_um.Evaluate( "@{${length} * 2}" ); |
|||
BOOST_CHECK_EQUAL( result, "50.8" ); |
|||
|
|||
// Test that unit-aware evaluator preserves its unit setting across operations
|
|||
EXPRESSION_EVALUATOR persistent_eval( EDA_UNITS::MILS ); |
|||
BOOST_CHECK_EQUAL( persistent_eval.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
|
|||
persistent_eval.Evaluate( "@{1 + 1}" ); |
|||
BOOST_CHECK_EQUAL( persistent_eval.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
|
|||
persistent_eval.SetVariable( "test", 42 ); |
|||
BOOST_CHECK_EQUAL( persistent_eval.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
|
|||
persistent_eval.Evaluate( "@{${test} + 8}" ); |
|||
BOOST_CHECK_EQUAL( persistent_eval.GetDefaultUnits(), EDA_UNITS::MILS ); |
|||
BOOST_CHECK_EQUAL( persistent_eval.Evaluate( "@{${test} + 8}" ), "50" ); |
|||
} |
|||
|
|||
/**
|
|||
* Test mixed unit arithmetic expectations using known conversion factors |
|||
* Documents expected behavior for when unit parsing is integrated |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( UnitMixingExpectations ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator_mm( EDA_UNITS::MM ); |
|||
|
|||
// Verify basic functionality works before discussing unit mixing
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.GetDefaultUnits(), EDA_UNITS::MM ); |
|||
|
|||
wxString result = evaluator_mm.Evaluate( "@{2 + 3}" ); |
|||
BOOST_CHECK_EQUAL( result, "5" ); |
|||
|
|||
// Test complex arithmetic
|
|||
result = evaluator_mm.Evaluate( "@{(1 + 2) * 3}" ); |
|||
BOOST_CHECK_EQUAL( result, "9" ); |
|||
|
|||
// Test with decimals (important for unit conversions)
|
|||
result = evaluator_mm.Evaluate( "@{25.4 + 12.7}" ); |
|||
// Use close comparison for floating point
|
|||
double numeric_result = wxAtof( result ); |
|||
BOOST_CHECK_CLOSE( numeric_result, 38.1, 0.01 ); |
|||
|
|||
// Test with variables that could represent converted values
|
|||
evaluator_mm.SetVariable( "inch_in_mm", 25.4 ); // 1 inch = 25.4 mm
|
|||
evaluator_mm.SetVariable( "mil_in_mm", 0.0254 ); // 1 mil = 0.0254 mm
|
|||
|
|||
result = evaluator_mm.Evaluate( "@{${inch_in_mm} + ${mil_in_mm}}" ); |
|||
BOOST_CHECK_EQUAL( result, "25.4254" ); |
|||
|
|||
// Simulate what "1mm + 1in" should become when units are parsed
|
|||
evaluator_mm.SetVariable( "mm_part", 1.0 ); |
|||
evaluator_mm.SetVariable( "in_part", 25.4 ); // 1in converted to mm
|
|||
result = evaluator_mm.Evaluate( "@{${mm_part} + ${in_part}}" ); |
|||
BOOST_CHECK_EQUAL( result, "26.4" ); |
|||
|
|||
// Simulate what "1in + 1000mil" should become
|
|||
evaluator_mm.SetVariable( "one_inch", 25.4 ); |
|||
evaluator_mm.SetVariable( "thousand_mils", 25.4 ); // 1000 mils = 1 inch = 25.4 mm
|
|||
result = evaluator_mm.Evaluate( "@{${one_inch} + ${thousand_mils}}" ); |
|||
BOOST_CHECK_EQUAL( result, "50.8" ); |
|||
|
|||
// Test expressions that will be possible once unit parsing is integrated:
|
|||
// These would parse "1mm", "1in", "1mil" etc. and convert to default units
|
|||
|
|||
|
|||
// Basic unit expressions
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1mm}" ), "1" ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1in}" ), "25.4" ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1000mil}" ), "25.4" ); |
|||
|
|||
// Mixed unit arithmetic
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1mm + 1in}" ), "26.4" ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1in + 1000mil}" ), "50.8" ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{10mm + 0.5in + 500mil}" ), "35.4" ); |
|||
|
|||
// Unit expressions with whitespace
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1 mm}" ), "1" ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{1 in}" ), "25.4" ); |
|||
|
|||
// Complex mixed unit expressions with variables
|
|||
evaluator_mm.SetVariable( "width", 10 ); // 10mm
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{${width}mm + 1in}" ), "35.4" ); |
|||
// These two should both work the same
|
|||
BOOST_CHECK_EQUAL( evaluator_mm.Evaluate( "@{${width} * 1mm + 1in}" ), "35.4" ); |
|||
|
|||
// Different evaluator units should convert appropriately
|
|||
EXPRESSION_EVALUATOR evaluator_inch( EDA_UNITS::INCH ); |
|||
BOOST_CHECK_EQUAL( evaluator_inch.Evaluate( "@{1in}" ), "1" ); |
|||
BOOST_CHECK_EQUAL( evaluator_inch.Evaluate( "@{25.4mm}" ), "1" ); |
|||
} |
|||
|
|||
/**
|
|||
* Test actual unit parsing integration (now that unit parsing is implemented) |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ActualUnitParsing ) |
|||
{ |
|||
// Test MM evaluator with unit expressions
|
|||
EXPRESSION_EVALUATOR evaluator_mm( EDA_UNITS::MM ); |
|||
BOOST_CHECK_EQUAL( evaluator_mm.GetDefaultUnits(), EDA_UNITS::MM ); |
|||
|
|||
// Test basic unit expressions (these should now work!)
|
|||
wxString result; |
|||
|
|||
// Debug: Test basic arithmetic first
|
|||
result = evaluator_mm.Evaluate( "@{2+3}" ); |
|||
BOOST_CHECK_EQUAL( result, "5" ); |
|||
|
|||
// Debug: Test just number
|
|||
result = evaluator_mm.Evaluate( "@{1}" ); |
|||
BOOST_CHECK_EQUAL( result, "1" ); |
|||
|
|||
// Debug: Test the working case
|
|||
result = evaluator_mm.Evaluate( "@{1in}" ); |
|||
if (result != "25.4") { |
|||
std::cout << "DEBUG: @{1in} returned '" << result.ToStdString() << "'" << std::endl; |
|||
if (evaluator_mm.HasErrors()) { |
|||
std::cout << "DEBUG: @{1in} Errors: " << evaluator_mm.GetErrorSummary().ToStdString() << std::endl; |
|||
} |
|||
} |
|||
BOOST_CHECK_EQUAL( result, "25.4" ); |
|||
|
|||
result = evaluator_mm.Evaluate( "@{1mil}" ); |
|||
BOOST_CHECK_EQUAL( result, "0.0254" ); |
|||
|
|||
result = evaluator_mm.Evaluate( "@{1mm}" ); |
|||
BOOST_CHECK_EQUAL( result, "1" ); |
|||
|
|||
// 1 inch should convert to 25.4 mm
|
|||
result = evaluator_mm.Evaluate( "@{1in}" ); |
|||
BOOST_CHECK_EQUAL( result, "25.4" ); |
|||
|
|||
// 1000 mils should convert to 25.4 mm (1000 mils = 1 inch)
|
|||
result = evaluator_mm.Evaluate( "@{1000mil}" ); |
|||
BOOST_CHECK_EQUAL( result, "25.4" ); |
|||
|
|||
// Test mixed unit arithmetic
|
|||
result = evaluator_mm.Evaluate( "@{1mm + 1in}" ); |
|||
BOOST_CHECK_EQUAL( result, "26.4" ); |
|||
|
|||
result = evaluator_mm.Evaluate( "@{1in + 1000mil}" ); |
|||
BOOST_CHECK_EQUAL( result, "50.8" ); |
|||
|
|||
// Test more complex expressions
|
|||
result = evaluator_mm.Evaluate( "@{10mm + 0.5in + 500mil}" ); |
|||
BOOST_CHECK_EQUAL( result, "35.4" ); |
|||
|
|||
// Test unit expressions with spaces (if supported)
|
|||
result = evaluator_mm.Evaluate( "@{1 mm}" ); |
|||
BOOST_CHECK_EQUAL( result, "1" ); |
|||
|
|||
// Test with different default units
|
|||
EXPRESSION_EVALUATOR evaluator_inch( EDA_UNITS::INCH ); |
|||
|
|||
// 1 inch should be 1 when default unit is inches
|
|||
result = evaluator_inch.Evaluate( "@{1in}" ); |
|||
BOOST_CHECK_EQUAL( result, "1" ); |
|||
|
|||
// 25.4mm should convert to 1 inch (with floating point tolerance)
|
|||
result = evaluator_inch.Evaluate( "@{25.4mm}" ); |
|||
// Use approximate comparison for floating point
|
|||
double result_val = wxAtof(result); |
|||
BOOST_CHECK( std::abs(result_val - 1.0) < 0.001 ); |
|||
|
|||
// Test arithmetic with inch evaluator
|
|||
result = evaluator_inch.Evaluate( "@{1in + 1000mil}" ); |
|||
BOOST_CHECK_EQUAL( result, "2" ); // 1 inch + 1 inch = 2 inches
|
|||
|
|||
// Test centimeters
|
|||
result = evaluator_mm.Evaluate( "@{1cm}" ); |
|||
BOOST_CHECK_EQUAL( result, "10" ); // 1 cm = 10 mm
|
|||
|
|||
// Test micrometers
|
|||
result = evaluator_mm.Evaluate( "@{1000um}" ); |
|||
BOOST_CHECK_EQUAL( result, "1" ); // 1000 um = 1 mm
|
|||
|
|||
// Test quotes for inches
|
|||
result = evaluator_mm.Evaluate( "@{1\"}" ); |
|||
BOOST_CHECK_EQUAL( result, "25.4" ); // 1" = 25.4 mm
|
|||
|
|||
// Test complex mixed expressions with parentheses
|
|||
result = evaluator_mm.Evaluate( "@{(1in + 500mil) * 2}" ); |
|||
// Expected: (25.4 + 12.7) * 2 = 38.1 * 2 = 76.2mm
|
|||
double result_val2 = wxAtof(result); |
|||
BOOST_CHECK( std::abs(result_val2 - 76.2) < 0.001 ); |
|||
} |
|||
|
|||
/**
|
|||
* Test unit parsing edge cases and error handling |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( UnitParsingEdgeCases, * boost::unit_test::enabled() ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator_mm( EDA_UNITS::MM ); |
|||
|
|||
// Test invalid units (should be treated as plain numbers)
|
|||
wxString result = evaluator_mm.Evaluate( "@{1xyz}" ); |
|||
// Should parse as "1" followed by identifier "xyz", might be an error or treat as 1
|
|||
// This behavior depends on implementation details
|
|||
|
|||
// Test numbers without units (should work normally)
|
|||
result = evaluator_mm.Evaluate( "@{25.4}" ); |
|||
BOOST_CHECK_EQUAL( result, "25.4" ); |
|||
|
|||
// Test zero with units
|
|||
result = evaluator_mm.Evaluate( "@{0mm}" ); |
|||
BOOST_CHECK_EQUAL( result, "0" ); |
|||
|
|||
result = evaluator_mm.Evaluate( "@{0in}" ); |
|||
BOOST_CHECK_EQUAL( result, "0" ); |
|||
|
|||
// Test decimal values with units
|
|||
result = evaluator_mm.Evaluate( "@{2.54cm}" ); |
|||
BOOST_CHECK_EQUAL( result, "25.4" ); // 2.54 cm = 25.4 mm
|
|||
|
|||
result = evaluator_mm.Evaluate( "@{0.5in}" ); |
|||
BOOST_CHECK_EQUAL( result, "12.7" ); // 0.5 inch = 12.7 mm
|
|||
|
|||
// Test very small values
|
|||
result = evaluator_mm.Evaluate( "@{1um}" ); |
|||
BOOST_CHECK_EQUAL( result, "0.001" ); // 1 um = 0.001 mm
|
|||
} |
|||
|
|||
BOOST_AUTO_TEST_CASE( NumericEvaluatorCompatibility ) |
|||
{ |
|||
// Test the NUMERIC_EVALUATOR_COMPAT wrapper class that provides
|
|||
// a drop-in replacement for NUMERIC_EVALUATOR using EXPRESSION_EVALUATOR backend
|
|||
|
|||
NUMERIC_EVALUATOR_COMPAT eval( EDA_UNITS::MM ); |
|||
|
|||
// Test basic arithmetic
|
|||
BOOST_CHECK( eval.Process( "1 + 2" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "3" ); |
|||
BOOST_CHECK_EQUAL( eval.OriginalText(), "1 + 2" ); |
|||
|
|||
// Test variables
|
|||
eval.SetVar( "x", 5.0 ); |
|||
eval.SetVar( "y", 3.0 ); |
|||
|
|||
BOOST_CHECK( eval.Process( "x + y" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "8" ); |
|||
|
|||
// Test GetVar
|
|||
BOOST_CHECK_CLOSE( eval.GetVar( "x" ), 5.0, 0.001 ); |
|||
BOOST_CHECK_CLOSE( eval.GetVar( "y" ), 3.0, 0.001 ); |
|||
BOOST_CHECK_CLOSE( eval.GetVar( "undefined" ), 0.0, 0.001 ); |
|||
|
|||
// Test units (should work seamlessly)
|
|||
BOOST_CHECK( eval.Process( "1in + 1mm" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "26.4" ); // 1 inch + 1mm in mm
|
|||
|
|||
// Test mathematical functions
|
|||
BOOST_CHECK( eval.Process( "sqrt(16)" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "4" ); |
|||
|
|||
// Test invalid expression - use something clearly invalid
|
|||
BOOST_CHECK( !eval.Process( "1 + * 2" ) ); // Clearly invalid: two operators in a row
|
|||
BOOST_CHECK( !eval.IsValid() ); |
|||
|
|||
// Test Clear() - should reset state but keep variables
|
|||
eval.Clear(); |
|||
BOOST_CHECK_CLOSE( eval.GetVar( "x" ), 5.0, 0.001 ); // Variables should still be there
|
|||
|
|||
BOOST_CHECK( eval.Process( "x * 2" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "10" ); |
|||
|
|||
// Test variable removal
|
|||
eval.RemoveVar( "x" ); |
|||
BOOST_CHECK_CLOSE( eval.GetVar( "x" ), 0.0, 0.001 ); // Should be 0.0 for undefined
|
|||
BOOST_CHECK_CLOSE( eval.GetVar( "y" ), 3.0, 0.001 ); // y should still exist
|
|||
|
|||
// Test ClearVar()
|
|||
eval.ClearVar(); |
|||
BOOST_CHECK_CLOSE( eval.GetVar( "y" ), 0.0, 0.001 ); // All variables should be gone
|
|||
|
|||
// Test that we can still use the evaluator after clearing
|
|||
BOOST_CHECK( eval.Process( "42" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "42" ); |
|||
|
|||
// Test LocaleChanged() - should be a no-op but not crash
|
|||
eval.LocaleChanged(); |
|||
BOOST_CHECK( eval.Process( "3.14" ) ); |
|||
BOOST_CHECK( eval.IsValid() ); |
|||
BOOST_CHECK_EQUAL( eval.Result(), "3.14" ); |
|||
} |
|||
|
|||
BOOST_AUTO_TEST_SUITE_END() |
@ -0,0 +1,589 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.TXT for contributors. |
|||
* |
|||
* This program is free software; you can redistribute it and/or |
|||
* modify it under the terms of the GNU General Public License |
|||
* as published by the Free Software Foundation; either version 2 |
|||
* of the License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License |
|||
* along with this program; if not, you may find one here: |
|||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|||
* or you may search the http://www.gnu.org website for the version 2 license,
|
|||
* or you may write to the Free Software Foundation, Inc., |
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|||
*/ |
|||
|
|||
/**
|
|||
* @file |
|||
* Test suite for text_eval_parser routines |
|||
*/ |
|||
|
|||
#include <qa_utils/wx_utils/unit_test_utils.h>
|
|||
|
|||
// Code under test
|
|||
#include <text_eval/text_eval_wrapper.h>
|
|||
|
|||
#include <chrono>
|
|||
#include <cmath>
|
|||
#include <regex>
|
|||
|
|||
/**
|
|||
* Declare the test suite |
|||
*/ |
|||
BOOST_AUTO_TEST_SUITE( TextEvalParser ) |
|||
|
|||
/**
|
|||
* Test basic arithmetic operations |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( BasicArithmetic ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Basic operations
|
|||
{ "Text @{2 + 3} more text", "Text 5 more text", false }, |
|||
{ "@{10 - 4}", "6", false }, |
|||
{ "@{7 * 8}", "56", false }, |
|||
{ "@{15 / 3}", "5", false }, |
|||
{ "@{17 % 5}", "2", false }, |
|||
{ "@{2^3}", "8", false }, |
|||
|
|||
// Order of operations
|
|||
{ "@{2 + 3 * 4}", "14", false }, |
|||
{ "@{(2 + 3) * 4}", "20", false }, |
|||
{ "@{2^3^2}", "512", false }, // Right associative
|
|||
{ "@{-5}", "-5", false }, |
|||
{ "@{+5}", "5", false }, |
|||
|
|||
// Floating point
|
|||
{ "@{3.14 + 1.86}", "5", false }, |
|||
{ "@{10.5 / 2}", "5.25", false }, |
|||
{ "@{3.5 * 2}", "7", false }, |
|||
|
|||
// Edge cases
|
|||
{ "@{1 / 0}", "Text @{1 / 0} more text", true }, // Division by zero
|
|||
{ "@{1 % 0}", "Text @{1 % 0} more text", true }, // Modulo by zero
|
|||
|
|||
// Multiple calculations in one string
|
|||
{ "@{2 + 2} and @{3 * 3}", "4 and 9", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test variable substitution |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( VariableSubstitution ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Set up some variables
|
|||
evaluator.SetVariable( "x", 10.0 ); |
|||
evaluator.SetVariable( "y", 5.0 ); |
|||
evaluator.SetVariable( wxString("name"), wxString("KiCad") ); |
|||
evaluator.SetVariable( "version", 8.0 ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Basic variable substitution
|
|||
{ "@{${x}}", "10", false }, |
|||
{ "@{${y}}", "5", false }, |
|||
{ "Hello ${name}!", "Hello KiCad!", false }, |
|||
|
|||
// Variables in calculations
|
|||
{ "@{${x} + ${y}}", "15", false }, |
|||
{ "@{${x} * ${y}}", "50", false }, |
|||
{ "@{${x} - ${y}}", "5", false }, |
|||
{ "@{${x} / ${y}}", "2", false }, |
|||
|
|||
// Mixed text and variable calculations
|
|||
{ "Product: @{${x} * ${y}} units", "Product: 50 units", false }, |
|||
{ "Version ${version}.0", "Version 8.0", false }, |
|||
|
|||
// Undefined variables
|
|||
{ "@{${undefined}}", "@{${undefined}}", true }, |
|||
|
|||
// String variables
|
|||
{ "Welcome to ${name}", "Welcome to KiCad", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test string operations and concatenation |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( StringOperations ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
evaluator.SetVariable( wxString("prefix"), wxString("Hello") ); |
|||
evaluator.SetVariable( wxString("suffix"), wxString("World") ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// String concatenation with +
|
|||
{ "@{\"Hello\" + \" \" + \"World\"}", "Hello World", false }, |
|||
{ "@{${prefix} + \" \" + ${suffix}}", "Hello World", false }, |
|||
|
|||
// Mixed string and number concatenation
|
|||
{ "@{\"Count: \" + 42}", "Count: 42", false }, |
|||
{ "@{42 + \" items\"}", "42 items", false }, |
|||
|
|||
// String literals
|
|||
{ "@{\"Simple string\"}", "Simple string", false }, |
|||
{ "Prefix @{\"middle\"} suffix", "Prefix middle suffix", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test mathematical functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( MathematicalFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
double tolerance; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Basic math functions
|
|||
{ "@{abs(-5)}", "5", false, 0.001 }, |
|||
{ "@{abs(3.14)}", "3.14", false, 0.001 }, |
|||
{ "@{sqrt(16)}", "4", false, 0.001 }, |
|||
{ "@{sqrt(2)}", "1.414", false, 0.01 }, |
|||
{ "@{pow(2, 3)}", "8", false, 0.001 }, |
|||
{ "@{pow(3, 2)}", "9", false, 0.001 }, |
|||
|
|||
// Rounding functions
|
|||
{ "@{floor(3.7)}", "3", false, 0.001 }, |
|||
{ "@{ceil(3.2)}", "4", false, 0.001 }, |
|||
{ "@{round(3.7)}", "4", false, 0.001 }, |
|||
{ "@{round(3.2)}", "3", false, 0.001 }, |
|||
{ "@{round(3.14159, 2)}", "3.14", false, 0.001 }, |
|||
|
|||
// Min/Max functions
|
|||
{ "@{min(5, 3, 8, 1)}", "1", false, 0.001 }, |
|||
{ "@{max(5, 3, 8, 1)}", "8", false, 0.001 }, |
|||
{ "@{min(3.5, 3.1)}", "3.1", false, 0.001 }, |
|||
|
|||
// Sum and average
|
|||
{ "@{sum(1, 2, 3, 4)}", "10", false, 0.001 }, |
|||
{ "@{avg(2, 4, 6)}", "4", false, 0.001 }, |
|||
|
|||
// Error cases
|
|||
{ "@{sqrt(-1)}", "Text @{sqrt(-1)} more text", true, 0 }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
if( testCase.tolerance > 0 ) |
|||
{ |
|||
// For floating point comparisons
|
|||
double actualValue = wxStrtod( result, nullptr ); |
|||
double expectedValue = wxStrtod( testCase.expected, nullptr ); |
|||
BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test string manipulation functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( StringFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
evaluator.SetVariable( wxString("text"), wxString("Hello World") ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Case conversion
|
|||
{ "@{upper(\"hello world\")}", "HELLO WORLD", false }, |
|||
{ "@{lower(\"HELLO WORLD\")}", "hello world", false }, |
|||
{ "@{upper(${text})}", "HELLO WORLD", false }, |
|||
|
|||
// String concatenation function
|
|||
{ "@{concat(\"Hello\", \" \", \"World\")}", "Hello World", false }, |
|||
{ "@{concat(\"Count: \", 42, \" items\")}", "Count: 42 items", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test formatting functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( FormattingFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Number formatting
|
|||
{ "@{format(3.14159)}", "3.14", false }, |
|||
{ "@{format(3.14159, 3)}", "3.142", false }, |
|||
{ "@{format(1234.5)}", "1234.50", false }, |
|||
{ "@{fixed(3.14159, 2)}", "3.14", false }, |
|||
|
|||
// Currency formatting
|
|||
{ "@{currency(1234.56)}", "$1234.56", false }, |
|||
{ "@{currency(999.99, \"€\")}", "€999.99", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test date and time functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( DateTimeFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Note: These tests will be time-sensitive. We test the functions exist
|
|||
// and return reasonable values rather than exact matches.
|
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
bool shouldContainNumbers; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Date functions that return numbers (days since epoch)
|
|||
{ "@{today()}", true, false }, |
|||
{ "@{now()}", true, false }, // Returns timestamp
|
|||
|
|||
// Date formatting (these return specific dates so we can test exactly)
|
|||
{ "@{dateformat(0)}", false, false }, // Should format epoch date
|
|||
{ "@{dateformat(0, \"ISO\")}", false, false }, |
|||
{ "@{dateformat(0, \"US\")}", false, false }, |
|||
{ "@{weekdayname(0)}", false, false }, // Should return weekday name
|
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK( !result.empty() ); |
|||
|
|||
if( testCase.shouldContainNumbers ) |
|||
{ |
|||
// Result should be a number
|
|||
BOOST_CHECK( std::all_of( result.begin(), result.end(), |
|||
[]( char c ) { return std::isdigit( c ) || c == '.' || c == '-'; } ) ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Test specific date formatting with known values
|
|||
auto result1 = evaluator.Evaluate( "@{dateformat(0, \"ISO\")}" ); |
|||
BOOST_CHECK_EQUAL( result1, "1970-01-01" ); // Unix epoch
|
|||
|
|||
auto result2 = evaluator.Evaluate( "@{weekdayname(0)}" ); |
|||
BOOST_CHECK_EQUAL( result2, "Thursday" ); // Unix epoch was a Thursday
|
|||
} |
|||
|
|||
/**
|
|||
* Test conditional functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ConditionalFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
evaluator.SetVariable( "x", 10.0 ); |
|||
evaluator.SetVariable( "y", 5.0 ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Basic if function
|
|||
{ "@{if(1, \"true\", \"false\")}", "true", false }, |
|||
{ "@{if(0, \"true\", \"false\")}", "false", false }, |
|||
{ "@{if(${x} > ${y}, \"greater\", \"not greater\")}", "greater", false }, |
|||
{ "@{if(${x} < ${y}, \"less\", \"not less\")}", "not less", false }, |
|||
|
|||
// Numeric if results
|
|||
{ "@{if(1, 42, 24)}", "42", false }, |
|||
{ "@{if(0, 42, 24)}", "24", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test random functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( RandomFunctions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Test that random function returns a value between 0 and 1
|
|||
auto result = evaluator.Evaluate( "@{random()}" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
double randomValue = wxStrtod( result, nullptr ); |
|||
BOOST_CHECK_GE( randomValue, 0.0 ); |
|||
BOOST_CHECK_LT( randomValue, 1.0 ); |
|||
|
|||
// Test that consecutive calls return different values (with high probability)
|
|||
auto result2 = evaluator.Evaluate( "@{random()}" ); |
|||
double randomValue2 = wxStrtod( result2, nullptr ); |
|||
|
|||
// It's theoretically possible these could be equal, but extremely unlikely
|
|||
BOOST_CHECK_NE( randomValue, randomValue2 ); |
|||
} |
|||
|
|||
/**
|
|||
* Test error handling and edge cases |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ErrorHandling ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
bool shouldError; |
|||
std::string description; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Syntax errors
|
|||
{ "@{2 +}", true, "incomplete expression" }, |
|||
{ "@{(2 + 3", true, "unmatched parenthesis" }, |
|||
{ "@{2 + 3)}", true, "extra closing parenthesis" }, |
|||
{ "@{}", true, "empty calculation" }, |
|||
|
|||
// Unknown functions
|
|||
{ "@{unknownfunc(1, 2)}", true, "unknown function" }, |
|||
|
|||
// Wrong number of arguments
|
|||
{ "@{abs()}", true, "abs with no arguments" }, |
|||
{ "@{abs(1, 2)}", true, "abs with too many arguments" }, |
|||
{ "@{sqrt()}", true, "sqrt with no arguments" }, |
|||
|
|||
// Runtime errors
|
|||
{ "@{1 / 0}", true, "division by zero" }, |
|||
{ "@{sqrt(-1)}", true, "square root of negative" }, |
|||
|
|||
// Valid expressions that should not error
|
|||
{ "Plain text", false, "plain text should not error" }, |
|||
{ "@{2 + 2}", false, "simple calculation should work" }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK_MESSAGE( evaluator.HasErrors(), |
|||
"Expected error for: " + testCase.description ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Unexpected error for: " + testCase.description ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test complex nested expressions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ComplexExpressions ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
evaluator.SetVariable( "pi", 3.14159 ); |
|||
evaluator.SetVariable( "radius", 5.0 ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
double tolerance; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Complex mathematical expressions
|
|||
{ "@{2 * ${pi} * ${radius}}", "31.42", 0.01 }, // Circumference
|
|||
{ "@{${pi} * pow(${radius}, 2)}", "78.54", 0.01 }, // Area
|
|||
{ "@{sqrt(pow(3, 2) + pow(4, 2))}", "5", 0.001 }, // Pythagorean theorem
|
|||
|
|||
// Nested function calls
|
|||
{ "@{max(abs(-5), sqrt(16), floor(3.7))}", "5", 0.001 }, |
|||
{ "@{round(avg(1.1, 2.2, 3.3), 1)}", "2.2", 0.001 }, |
|||
|
|||
// Mixed string and math
|
|||
{ "Circle with radius @{${radius}} has area @{format(${pi} * pow(${radius}, 2), 1)}", |
|||
"Circle with radius 5 has area 78.5", 0 }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
std::string resultStr = result.ToStdString(); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
if( testCase.tolerance > 0 ) |
|||
{ |
|||
// Extract numeric part for comparison
|
|||
std::regex numberRegex( R"([\d.]+)" ); |
|||
std::smatch match; |
|||
|
|||
if( std::regex_search( resultStr, match, numberRegex ) ) |
|||
{ |
|||
double actualValue = std::stod( match[0].str() ); |
|||
double expectedValue = std::stod( testCase.expected ); |
|||
BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 ); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test performance with large expressions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( Performance ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Build a large expression with many calculations
|
|||
std::string largeExpression = "Result: "; |
|||
for( int i = 0; i < 50; ++i ) |
|||
{ |
|||
largeExpression += "@{" + std::to_string(i) + " * 2} "; |
|||
} |
|||
|
|||
auto start = std::chrono::high_resolution_clock::now(); |
|||
auto result = evaluator.Evaluate( largeExpression ); |
|||
auto end = std::chrono::high_resolution_clock::now(); |
|||
|
|||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ); |
|||
|
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK( !result.empty() ); |
|||
|
|||
// Should complete in reasonable time (less than 1 second)
|
|||
BOOST_CHECK_LT( duration.count(), 1000 ); |
|||
} |
|||
|
|||
BOOST_AUTO_TEST_SUITE_END() |
@ -0,0 +1,508 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.TXT for contributors. |
|||
* |
|||
* This program is free software; you can redistribute it and/or |
|||
* modify it under the terms of the GNU General Public License |
|||
* as published by the Free Software Foundation; either version 2 |
|||
* of the License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License |
|||
* along with this program; if not, you may find one here: |
|||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|||
* or you may search the http://www.gnu.org website for the version 2 license,
|
|||
* or you may write to the Free Software Foundation, Inc., |
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|||
*/ |
|||
|
|||
/**
|
|||
* @file |
|||
* Test suite for low-level text_eval_parser functionality |
|||
*/ |
|||
|
|||
#include <qa_utils/wx_utils/unit_test_utils.h>
|
|||
|
|||
// Code under test
|
|||
#include <text_eval/text_eval_parser.h>
|
|||
|
|||
#include <memory>
|
|||
#include <unordered_map>
|
|||
|
|||
using namespace calc_parser; |
|||
|
|||
/**
|
|||
* Declare the test suite |
|||
*/ |
|||
BOOST_AUTO_TEST_SUITE( TextEvalParserLowLevel ) |
|||
|
|||
/**
|
|||
* Helper function to create a simple variable resolver for testing |
|||
*/ |
|||
auto CreateTestVariableResolver() |
|||
{ |
|||
auto variables = std::make_shared<std::unordered_map<std::string, Value>>(); |
|||
|
|||
// Set up some test variables
|
|||
(*variables)["x"] = 10.0; |
|||
(*variables)["y"] = 5.0; |
|||
(*variables)["name"] = std::string("KiCad"); |
|||
(*variables)["pi"] = 3.14159; |
|||
|
|||
return [variables]( const std::string& varName ) -> Result<Value> |
|||
{ |
|||
auto it = variables->find( varName ); |
|||
if( it != variables->end() ) |
|||
return MakeValue( it->second ); |
|||
|
|||
return MakeError<Value>( "Variable not found: " + varName ); |
|||
}; |
|||
} |
|||
|
|||
/**
|
|||
* Test VALUE_UTILS functionality |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ValueUtils ) |
|||
{ |
|||
// Test ToDouble conversion
|
|||
{ |
|||
Value numVal = 42.5; |
|||
auto result = calc_parser::VALUE_UTILS::ToDouble( numVal ); |
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK_CLOSE( result.GetValue(), 42.5, 0.001 ); |
|||
} |
|||
|
|||
{ |
|||
Value strVal = std::string("123.45"); |
|||
auto result = calc_parser::VALUE_UTILS::ToDouble( strVal ); |
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK_CLOSE( result.GetValue(), 123.45, 0.001 ); |
|||
} |
|||
|
|||
{ |
|||
Value invalidStr = std::string("not_a_number"); |
|||
auto result = calc_parser::VALUE_UTILS::ToDouble( invalidStr ); |
|||
BOOST_CHECK( result.HasError() ); |
|||
} |
|||
|
|||
// Test ToString conversion
|
|||
{ |
|||
Value numVal = 42.0; |
|||
auto result = calc_parser::VALUE_UTILS::ToString( numVal ); |
|||
BOOST_CHECK_EQUAL( result, "42" ); |
|||
} |
|||
|
|||
{ |
|||
Value strVal = std::string("Hello"); |
|||
auto result = calc_parser::VALUE_UTILS::ToString( strVal ); |
|||
BOOST_CHECK_EQUAL( result, "Hello" ); |
|||
} |
|||
|
|||
// Test IsTruthy
|
|||
{ |
|||
BOOST_CHECK( calc_parser::VALUE_UTILS::IsTruthy( Value{1.0} ) ); |
|||
BOOST_CHECK( !calc_parser::VALUE_UTILS::IsTruthy( Value{0.0} ) ); |
|||
BOOST_CHECK( calc_parser::VALUE_UTILS::IsTruthy( Value{std::string("non-empty")} ) ); |
|||
BOOST_CHECK( !calc_parser::VALUE_UTILS::IsTruthy( Value{std::string("")} ) ); |
|||
} |
|||
|
|||
// Test ArithmeticOp
|
|||
{ |
|||
Value left = 10.0; |
|||
Value right = 3.0; |
|||
|
|||
auto addResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '+' ); |
|||
BOOST_CHECK( addResult.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( addResult.GetValue() ), 13.0, 0.001 ); |
|||
|
|||
auto subResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '-' ); |
|||
BOOST_CHECK( subResult.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( subResult.GetValue() ), 7.0, 0.001 ); |
|||
|
|||
auto mulResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '*' ); |
|||
BOOST_CHECK( mulResult.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( mulResult.GetValue() ), 30.0, 0.001 ); |
|||
|
|||
auto divResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '/' ); |
|||
BOOST_CHECK( divResult.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( divResult.GetValue() ), 3.333, 0.1 ); |
|||
|
|||
auto modResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '%' ); |
|||
BOOST_CHECK( modResult.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( modResult.GetValue() ), 1.0, 0.001 ); |
|||
|
|||
auto powResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '^' ); |
|||
BOOST_CHECK( powResult.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( powResult.GetValue() ), 1000.0, 0.001 ); |
|||
} |
|||
|
|||
// Test division by zero
|
|||
{ |
|||
Value left = 10.0; |
|||
Value right = 0.0; |
|||
|
|||
auto divResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '/' ); |
|||
BOOST_CHECK( divResult.HasError() ); |
|||
|
|||
auto modResult = calc_parser::VALUE_UTILS::ArithmeticOp( left, right, '%' ); |
|||
BOOST_CHECK( modResult.HasError() ); |
|||
} |
|||
|
|||
// Test ConcatStrings
|
|||
{ |
|||
Value left = std::string("Hello "); |
|||
Value right = std::string("World"); |
|||
auto result = calc_parser::VALUE_UTILS::ConcatStrings( left, right ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( result ), "Hello World" ); |
|||
} |
|||
|
|||
{ |
|||
Value left = 42.0; |
|||
Value right = std::string(" items"); |
|||
auto result = calc_parser::VALUE_UTILS::ConcatStrings( left, right ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( result ), "42 items" ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test Node creation and basic structure |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( NodeCreation ) |
|||
{ |
|||
// Test number node
|
|||
{ |
|||
auto node = NODE::CreateNumber( 42.5 ); |
|||
BOOST_CHECK( node->type == NodeType::Number ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( node->data ), 42.5, 0.001 ); |
|||
} |
|||
|
|||
// Test string node
|
|||
{ |
|||
auto node = NODE::CreateString( "Hello World" ); |
|||
BOOST_CHECK( node->type == NodeType::String ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( node->data ), "Hello World" ); |
|||
} |
|||
|
|||
// Test variable node
|
|||
{ |
|||
auto node = NODE::CreateVar( "testVar" ); |
|||
BOOST_CHECK( node->type == NodeType::Var ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( node->data ), "testVar" ); |
|||
} |
|||
|
|||
// Test binary operation node
|
|||
{ |
|||
auto left = NODE::CreateNumber( 10.0 ); |
|||
auto right = NODE::CreateNumber( 5.0 ); |
|||
auto binOp = NODE::CreateBinOp( std::move( left ), '+', std::move( right ) ); |
|||
|
|||
BOOST_CHECK( binOp->type == NodeType::BinOp ); |
|||
const auto& binOpData = std::get<BIN_OP_DATA>( binOp->data ); |
|||
BOOST_CHECK( binOpData.op == '+' ); |
|||
BOOST_CHECK( binOpData.left != nullptr ); |
|||
BOOST_CHECK( binOpData.right != nullptr ); |
|||
} |
|||
|
|||
// Test function node
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( 5.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "abs", std::move( args ) ); |
|||
BOOST_CHECK( funcNode->type == NodeType::Function ); |
|||
|
|||
const auto& funcData = std::get<FUNC_DATA>( funcNode->data ); |
|||
BOOST_CHECK_EQUAL( funcData.name, "abs" ); |
|||
BOOST_CHECK_EQUAL( funcData.args.size(), 1 ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test evaluation visitor with simple expressions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( EvaluationVisitor ) |
|||
{ |
|||
ERROR_COLLECTOR errors; |
|||
auto varResolver = CreateTestVariableResolver(); |
|||
calc_parser::EVAL_VISITOR evaluator( varResolver, errors ); |
|||
|
|||
// Test number evaluation
|
|||
{ |
|||
auto node = NODE::CreateNumber( 42.5 ); |
|||
auto result = node->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<double>( result.GetValue() ) ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( result.GetValue() ), 42.5, 0.001 ); |
|||
} |
|||
|
|||
// Test string evaluation
|
|||
{ |
|||
auto node = NODE::CreateString( "Hello" ); |
|||
auto result = node->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<std::string>( result.GetValue() ) ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( result.GetValue() ), "Hello" ); |
|||
} |
|||
|
|||
// Test variable evaluation
|
|||
{ |
|||
auto node = NODE::CreateVar( "x" ); |
|||
auto result = node->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<double>( result.GetValue() ) ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( result.GetValue() ), 10.0, 0.001 ); |
|||
} |
|||
|
|||
// Test undefined variable
|
|||
{ |
|||
auto node = NODE::CreateVar( "undefined" ); |
|||
auto result = node->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasError() ); |
|||
} |
|||
|
|||
// Test binary operation
|
|||
{ |
|||
auto left = NODE::CreateNumber( 10.0 ); |
|||
auto right = NODE::CreateNumber( 5.0 ); |
|||
auto binOp = NODE::CreateBinOp( std::move( left ), '+', std::move( right ) ); |
|||
|
|||
auto result = binOp->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( result.GetValue() ), 15.0, 0.001 ); |
|||
} |
|||
|
|||
// Test string concatenation with +
|
|||
{ |
|||
auto left = NODE::CreateString( "Hello " ); |
|||
auto right = NODE::CreateString( "World" ); |
|||
auto binOp = NODE::CreateBinOp( std::move( left ), '+', std::move( right ) ); |
|||
|
|||
auto result = binOp->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<std::string>( result.GetValue() ) ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( result.GetValue() ), "Hello World" ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test function evaluation |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( FunctionEvaluation ) |
|||
{ |
|||
ERROR_COLLECTOR errors; |
|||
auto varResolver = CreateTestVariableResolver(); |
|||
calc_parser::EVAL_VISITOR evaluator( varResolver, errors ); |
|||
|
|||
// Test abs function
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( -5.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "abs", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( result.GetValue() ), 5.0, 0.001 ); |
|||
} |
|||
|
|||
// Test sqrt function
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( 16.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "sqrt", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( result.GetValue() ), 4.0, 0.001 ); |
|||
} |
|||
|
|||
// Test sqrt with negative number (should error)
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( -1.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "sqrt", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasError() ); |
|||
} |
|||
|
|||
// Test max function
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( 3.0 ) ); |
|||
args.push_back( NODE::CreateNumber( 7.0 ) ); |
|||
args.push_back( NODE::CreateNumber( 1.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "max", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK_CLOSE( std::get<double>( result.GetValue() ), 7.0, 0.001 ); |
|||
} |
|||
|
|||
// Test string function - upper
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateString( "hello" ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "upper", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<std::string>( result.GetValue() ) ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( result.GetValue() ), "HELLO" ); |
|||
} |
|||
|
|||
// Test format function
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( 3.14159 ) ); |
|||
args.push_back( NODE::CreateNumber( 2.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "format", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<std::string>( result.GetValue() ) ); |
|||
BOOST_CHECK_EQUAL( std::get<std::string>( result.GetValue() ), "3.14" ); |
|||
} |
|||
|
|||
// Test unknown function
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; |
|||
args.push_back( NODE::CreateNumber( 1.0 ) ); |
|||
|
|||
auto funcNode = NODE::CreateFunction( "unknownfunc", std::move( args ) ); |
|||
auto result = funcNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasError() ); |
|||
} |
|||
|
|||
// Test zero-argument functions
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; // Empty args
|
|||
|
|||
auto todayNode = NODE::CreateFunction( "today", std::move( args ) ); |
|||
auto result = todayNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<double>( result.GetValue() ) ); |
|||
// Should return a reasonable number of days since epoch
|
|||
auto days = std::get<double>( result.GetValue() ); |
|||
BOOST_CHECK_GT( days, 18000 ); // Should be after year 2019
|
|||
} |
|||
|
|||
{ |
|||
std::vector<std::unique_ptr<NODE>> args; // Empty args
|
|||
|
|||
auto randomNode = NODE::CreateFunction( "random", std::move( args ) ); |
|||
auto result = randomNode->Accept( evaluator ); |
|||
|
|||
BOOST_CHECK( result.HasValue() ); |
|||
BOOST_CHECK( std::holds_alternative<double>( result.GetValue() ) ); |
|||
auto randomVal = std::get<double>( result.GetValue() ); |
|||
BOOST_CHECK_GE( randomVal, 0.0 ); |
|||
BOOST_CHECK_LT( randomVal, 1.0 ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test DOC_PROCESSOR functionality |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( DocumentProcessor ) |
|||
{ |
|||
auto varResolver = CreateTestVariableResolver(); |
|||
|
|||
// Create a simple document with text and calculations
|
|||
auto doc = std::make_unique<DOC>(); |
|||
|
|||
// Add text node
|
|||
doc->AddNode( NODE::CreateText( "Value is " ) ); |
|||
|
|||
// Add calculation node
|
|||
auto calcExpr = NODE::CreateBinOp( |
|||
NODE::CreateNumber( 2.0 ), |
|||
'+', |
|||
NODE::CreateNumber( 3.0 ) |
|||
); |
|||
doc->AddNode( NODE::CreateCalc( std::move( calcExpr ) ) ); |
|||
|
|||
// Add more text
|
|||
doc->AddNode( NODE::CreateText( " units" ) ); |
|||
|
|||
// Process the document
|
|||
auto [result, hadErrors] = calc_parser::DOC_PROCESSOR::Process( *doc, varResolver ); |
|||
|
|||
BOOST_CHECK( !hadErrors ); |
|||
BOOST_CHECK_EQUAL( result, "Value is 5 units" ); |
|||
} |
|||
|
|||
/**
|
|||
* Test error collection and reporting |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ErrorHandling ) |
|||
{ |
|||
ERROR_COLLECTOR errors; |
|||
|
|||
// Test adding errors
|
|||
errors.AddError( "Test error 1" ); |
|||
errors.AddWarning( "Test warning 1" ); |
|||
errors.AddError( "Test error 2" ); |
|||
|
|||
BOOST_CHECK( errors.HasErrors() ); |
|||
BOOST_CHECK( errors.HasWarnings() ); |
|||
|
|||
const auto& errorList = errors.GetErrors(); |
|||
BOOST_CHECK_EQUAL( errorList.size(), 2 ); |
|||
BOOST_CHECK_EQUAL( errorList[0], "Test error 1" ); |
|||
BOOST_CHECK_EQUAL( errorList[1], "Test error 2" ); |
|||
|
|||
const auto& warningList = errors.GetWarnings(); |
|||
BOOST_CHECK_EQUAL( warningList.size(), 1 ); |
|||
BOOST_CHECK_EQUAL( warningList[0], "Test warning 1" ); |
|||
|
|||
// Test error message formatting
|
|||
auto allMessages = errors.GetAllMessages(); |
|||
BOOST_CHECK( allMessages.find( "Error: Test error 1" ) != std::string::npos ); |
|||
BOOST_CHECK( allMessages.find( "Warning: Test warning 1" ) != std::string::npos ); |
|||
|
|||
// Test clearing
|
|||
errors.Clear(); |
|||
BOOST_CHECK( !errors.HasErrors() ); |
|||
BOOST_CHECK( !errors.HasWarnings() ); |
|||
} |
|||
|
|||
/**
|
|||
* Test TOKEN_TYPE utilities |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( TokenTypes ) |
|||
{ |
|||
// Test string token
|
|||
{ |
|||
auto token = MakeStringToken( "Hello World" ); |
|||
BOOST_CHECK( token.isString ); |
|||
BOOST_CHECK_EQUAL( GetTokenString( token ), "Hello World" ); |
|||
} |
|||
|
|||
// Test number token
|
|||
{ |
|||
auto token = MakeNumberToken( 42.5 ); |
|||
BOOST_CHECK( !token.isString ); |
|||
BOOST_CHECK_CLOSE( GetTokenDouble( token ), 42.5, 0.001 ); |
|||
} |
|||
} |
|||
|
|||
BOOST_AUTO_TEST_SUITE_END() |
@ -0,0 +1,471 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.TXT for contributors. |
|||
* |
|||
* This program is free softwar { "@{datestring('2023年12月25日')}", "19716", false }, // Christmas 2023 { "@{datestring('2023年12月25日')}", "19716", false }, // Christmas 2023 { "@{datestring('2023年12월25일')}", "19716", false }, // Christmas 2023; you can redistribute it and/or
|
|||
* modify it under the terms of the GNU General Public License |
|||
* as published by the Free Software Foundation; either version 2 |
|||
* of the License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License |
|||
* along with this program; if not, you may find one here: |
|||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|||
* or you may search the http://www.gnu.org website for the version 2 license,
|
|||
* or you may write to the Free Software Foundation, Inc., |
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|||
*/ |
|||
|
|||
/**
|
|||
* @file |
|||
* Test suite for text_eval_parser date and time functionality |
|||
*/ |
|||
|
|||
#include <qa_utils/wx_utils/unit_test_utils.h>
|
|||
#include <text_eval/text_eval_wrapper.h>
|
|||
|
|||
#include <chrono>
|
|||
#include <regex>
|
|||
|
|||
BOOST_AUTO_TEST_SUITE( TextEvalParserDateTime ) |
|||
|
|||
BOOST_AUTO_TEST_CASE( DateFormatting ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Test Unix epoch date (1970-01-01)
|
|||
{ "@{dateformat(0)}", "1970-01-01", false }, |
|||
{ "@{dateformat(0, \"ISO\")}", "1970-01-01", false }, |
|||
{ "@{dateformat(0, \"iso\")}", "1970-01-01", false }, |
|||
{ "@{dateformat(0, \"US\")}", "01/01/1970", false }, |
|||
{ "@{dateformat(0, \"us\")}", "01/01/1970", false }, |
|||
{ "@{dateformat(0, \"EU\")}", "01/01/1970", false }, |
|||
{ "@{dateformat(0, \"european\")}", "01/01/1970", false }, |
|||
{ "@{dateformat(0, \"long\")}", "January 1, 1970", false }, |
|||
{ "@{dateformat(0, \"short\")}", "Jan 1, 1970", false }, |
|||
|
|||
// Test some known dates
|
|||
{ "@{dateformat(365)}", "1971-01-01", false }, // One year after epoch
|
|||
{ "@{dateformat(1000)}", "1972-09-27", false }, // 1000 days after epoch
|
|||
|
|||
// Test weekday names
|
|||
{ "@{weekdayname(0)}", "Thursday", false }, // Unix epoch was Thursday
|
|||
{ "@{weekdayname(1)}", "Friday", false }, // Next day
|
|||
{ "@{weekdayname(2)}", "Saturday", false }, // Weekend
|
|||
{ "@{weekdayname(3)}", "Sunday", false }, |
|||
{ "@{weekdayname(4)}", "Monday", false }, |
|||
{ "@{weekdayname(5)}", "Tuesday", false }, |
|||
{ "@{weekdayname(6)}", "Wednesday", false }, |
|||
{ "@{weekdayname(7)}", "Thursday", false }, // Week cycles
|
|||
|
|||
// Test negative dates (before epoch)
|
|||
{ "@{dateformat(-1)}", "1969-12-31", false }, |
|||
{ "@{weekdayname(-1)}", "Wednesday", false }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Error in expression: " + testCase.expression + |
|||
" Errors: " + evaluator.GetErrorSummary() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test CJK (Chinese, Japanese, Korean) date formatting |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( CJKDateFormatting ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Test Unix epoch date (1970-01-01) in CJK formats
|
|||
{ "@{dateformat(0, \"Chinese\")}", "1970年01月01日", false }, |
|||
{ "@{dateformat(0, \"chinese\")}", "1970年01月01日", false }, |
|||
{ "@{dateformat(0, \"CN\")}", "1970年01月01日", false }, |
|||
{ "@{dateformat(0, \"cn\")}", "1970年01月01日", false }, |
|||
|
|||
{ "@{dateformat(0, \"Japanese\")}", "1970年01月01日", false }, |
|||
{ "@{dateformat(0, \"japanese\")}", "1970年01月01日", false }, |
|||
{ "@{dateformat(0, \"JP\")}", "1970年01月01日", false }, |
|||
{ "@{dateformat(0, \"jp\")}", "1970年01月01日", false }, |
|||
|
|||
{ "@{dateformat(0, \"Korean\")}", "1970년 01월 01일", false }, |
|||
{ "@{dateformat(0, \"korean\")}", "1970년 01월 01일", false }, |
|||
{ "@{dateformat(0, \"KR\")}", "1970년 01월 01일", false }, |
|||
{ "@{dateformat(0, \"kr\")}", "1970년 01월 01일", false }, |
|||
|
|||
// Test some other dates in CJK formats
|
|||
{ "@{dateformat(365, \"Chinese\")}", "1971年01月01日", false }, // One year after epoch
|
|||
{ "@{dateformat(365, \"Japanese\")}", "1971年01月01日", false }, // One year after epoch
|
|||
{ "@{dateformat(365, \"Korean\")}", "1971년 01월 01일", false }, // One year after epoch
|
|||
|
|||
{ "@{dateformat(1000, \"Chinese\")}", "1972年09月27日", false }, // 1000 days after epoch
|
|||
{ "@{dateformat(1000, \"Japanese\")}", "1972年09月27日", false }, // 1000 days after epoch
|
|||
{ "@{dateformat(1000, \"Korean\")}", "1972년 09월 27일", false }, // 1000 days after epoch
|
|||
|
|||
// Test negative dates (before epoch) in CJK formats
|
|||
{ "@{dateformat(-1, \"Chinese\")}", "1969年12月31日", false }, |
|||
{ "@{dateformat(-1, \"Japanese\")}", "1969年12月31日", false }, |
|||
{ "@{dateformat(-1, \"Korean\")}", "1969년 12월 31일", false }, |
|||
|
|||
// Test leap year date (Feb 29, 1972) in CJK formats
|
|||
{ "@{dateformat(789, \"Chinese\")}", "1972年02月29日", false }, // Feb 29, 1972 (leap year)
|
|||
{ "@{dateformat(789, \"Japanese\")}", "1972年02月29日", false }, // Feb 29, 1972 (leap year)
|
|||
{ "@{dateformat(789, \"Korean\")}", "1972년 02월 29일", false }, // Feb 29, 1972 (leap year)
|
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Error in expression: " + testCase.expression + |
|||
" Errors: " + evaluator.GetErrorSummary() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test CJK (Chinese, Japanese, Korean) date parsing with datestring function |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( CJKDateParsing ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Test if basic functions work first
|
|||
{ "@{dateformat(0)}", "1970-01-01", false }, // Test basic dateformat
|
|||
{ "@{upper(\"test\")}", "TEST", false }, // Test basic string function
|
|||
|
|||
// Test ASCII date parsing first to see if datestring function works
|
|||
{ "@{datestring(\"2024-03-15\")}", "19797", false }, // Test ASCII date
|
|||
{ "@{datestring(\"1970-01-01\")}", "0", false }, // Unix epoch
|
|||
|
|||
// Test Chinese date parsing (年月日)
|
|||
{ "@{datestring('2024年03月15日')}", "19797", false }, // Days since epoch for 2024-03-15
|
|||
{ "@{datestring('1970年01月01日')}", "0", false }, // Unix epoch
|
|||
{ "@{datestring('2024年01月01日')}", "19723", false }, // New Year 2024
|
|||
{ "@{datestring('1972年02月29日')}", "789", false }, // Leap year date
|
|||
{ "@{datestring('1969年12月31日')}", "-1", false }, // Day before epoch
|
|||
|
|||
// Test Korean date parsing (년월일) with spaces
|
|||
{ "@{datestring(\"2024년 03월 15일\")}", "19797", false }, // Days since epoch for 2024-03-15
|
|||
{ "@{datestring(\"1970년 01월 01일\")}", "0", false }, // Unix epoch
|
|||
{ "@{datestring(\"2024년 01월 01일\")}", "19723", false }, // New Year 2024
|
|||
{ "@{datestring(\"1972년 02월 29일\")}", "789", false }, // Leap year date
|
|||
{ "@{datestring(\"1969년 12월 31일\")}", "-1", false }, // Day before epoch
|
|||
|
|||
// Test Korean date parsing (년월일) without spaces
|
|||
{ "@{datestring(\"2024년03월15일\")}", "19797", false }, // Days since epoch for 2024-03-15
|
|||
{ "@{datestring(\"1970년01월01일\")}", "0", false }, // Unix epoch
|
|||
|
|||
// Test integration: parse CJK date and format in different style
|
|||
{ "@{dateformat(datestring('2024年03월15일'), 'ISO')}", "2024-03-15", false }, |
|||
{ "@{dateformat(datestring('2024년 03월 15일'), 'ISO')}", "2024-03-15", false }, |
|||
{ "@{dateformat(datestring('1970年01月01日'), 'US')}", "01/01/1970", false }, |
|||
{ "@{dateformat(datestring('1970년 01월 01일'), 'EU')}", "01/01/1970", false }, |
|||
|
|||
// Test round-trip: CJK -> parse -> format back to CJK
|
|||
{ "@{dateformat(datestring('2024年03월15日'), 'Chinese')}", "2024年03月15日", false }, |
|||
{ "@{dateformat(datestring('2024년 03월 15일'), 'Korean')}", "2024년 03월 15일", false }, |
|||
|
|||
// Test invalid CJK dates (should error)
|
|||
{ "@{datestring('2024年13月15日')}", "", true }, // Invalid month
|
|||
{ "@{datestring('2024년 02월 30일')}", "", true }, // Invalid day for February
|
|||
{ "@{datestring('2024年02月')}", "", true }, // Missing day
|
|||
{ "@{datestring('2024년')}", "", true }, // Missing month and day
|
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK_MESSAGE( evaluator.HasErrors(), |
|||
"Expected error but got result: " + result + |
|||
" for expression: " + testCase.expression ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Error in expression: " + testCase.expression + |
|||
" Errors: " + evaluator.GetErrorSummary() ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test current date/time functions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( CurrentDateTime ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
auto todayResult = evaluator.Evaluate( "@{today()}" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Should return a number (days since epoch)
|
|||
double todayDays = std::stod( todayResult.ToStdString() ); |
|||
|
|||
// Should be a reasonable number of days since 1970
|
|||
// As of 2024, this should be over 19,000 days
|
|||
BOOST_CHECK_GT( todayDays, 19000 ); |
|||
BOOST_CHECK_LT( todayDays, 50000 ); // Reasonable upper bound
|
|||
|
|||
// Test now() function
|
|||
auto nowResult = evaluator.Evaluate( "@{now()}" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Should return a timestamp (seconds since epoch)
|
|||
double nowTimestamp = std::stod( nowResult.ToStdString() ); |
|||
|
|||
// Should be a reasonable timestamp
|
|||
auto currentTime = std::chrono::system_clock::now(); |
|||
auto currentTimestamp = std::chrono::system_clock::to_time_t( currentTime ); |
|||
double currentTimestampDouble = static_cast<double>( currentTimestamp ); |
|||
|
|||
// Should be within a few seconds of current time
|
|||
BOOST_CHECK_CLOSE( nowTimestamp, currentTimestampDouble, 1.0 ); // Within 1%
|
|||
|
|||
// Test that consecutive calls to today() return the same value
|
|||
auto todayResult2 = evaluator.Evaluate( "@{today()}" ); |
|||
BOOST_CHECK_EQUAL( todayResult, todayResult2 ); |
|||
|
|||
// Test formatting current date
|
|||
auto formattedToday = evaluator.Evaluate( "@{dateformat(today(), \"ISO\")}" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Should be in ISO format: YYYY-MM-DD
|
|||
std::regex isoDateRegex( R"(\d{4}-\d{2}-\d{2})" ); |
|||
BOOST_CHECK( std::regex_match( formattedToday.ToStdString(), isoDateRegex ) ); |
|||
} |
|||
|
|||
/**
|
|||
* Test date arithmetic and calculations |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( DateArithmetic ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Date arithmetic
|
|||
{ "@{dateformat(0 + 1)}", "1970-01-02", false }, // Add one day
|
|||
{ "@{dateformat(0 + 7)}", "1970-01-08", false }, // Add one week
|
|||
{ "@{dateformat(0 + 30)}", "1970-01-31", false }, // Add 30 days
|
|||
{ "@{dateformat(0 + 365)}", "1971-01-01", false }, // Add one year (1970 was not leap)
|
|||
|
|||
// Leap year test
|
|||
{ "@{dateformat(365 + 365 + 366)}", "1973-01-01", false }, // 1972 was leap year
|
|||
|
|||
// Date differences
|
|||
{ "@{365 - 0}", "365", false }, // Days between dates
|
|||
|
|||
// Complex date expressions
|
|||
{ "@{weekdayname(today())}", "", false }, // Should return a weekday name (we can't predict which)
|
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
if( !testCase.expected.empty() ) |
|||
{ |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
else |
|||
{ |
|||
// For dynamic results like weekday names, just check it's not empty
|
|||
BOOST_CHECK( !result.empty() ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test date edge cases and boundary conditions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( DateEdgeCases ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Leap year boundaries
|
|||
{ "@{dateformat(365 + 365 + 59)}", "1972-02-29", false }, // Feb 29, 1972 (leap year)
|
|||
{ "@{dateformat(365 + 365 + 60)}", "1972-03-01", false }, // Mar 1, 1972
|
|||
|
|||
// Year boundaries
|
|||
{ "@{dateformat(365 - 1)}", "1970-12-31", false }, // Last day of 1970
|
|||
{ "@{dateformat(365)}", "1971-01-01", false }, // First day of 1971
|
|||
|
|||
// Month boundaries
|
|||
{ "@{dateformat(30)}", "1970-01-31", false }, // Last day of January
|
|||
{ "@{dateformat(31)}", "1970-02-01", false }, // First day of February
|
|||
{ "@{dateformat(58)}", "1970-02-28", false }, // Last day of February 1970 (not leap)
|
|||
{ "@{dateformat(59)}", "1970-03-01", false }, // First day of March 1970
|
|||
|
|||
// Large date values
|
|||
{ "@{dateformat(36525)}", "2070-01-01", false }, // 100 years after epoch
|
|||
|
|||
// Negative dates (before epoch)
|
|||
{ "@{dateformat(-365)}", "1969-01-01", false }, // One year before epoch
|
|||
{ "@{dateformat(-1)}", "1969-12-31", false }, // One day before epoch
|
|||
|
|||
// Weekday wrap-around
|
|||
{ "@{weekdayname(-1)}", "Wednesday", false }, // Day before Thursday
|
|||
{ "@{weekdayname(-7)}", "Thursday", false }, // One week before
|
|||
|
|||
// Edge case: very large weekday values
|
|||
{ "@{weekdayname(7000)}", "Thursday", false }, // Should still work
|
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Error in expression: " + testCase.expression ); |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test date formatting with mixed expressions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( DateFormattingMixed ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
evaluator.SetVariable( "days_offset", 100.0 ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
bool shouldWork; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Complex expressions combining dates and variables
|
|||
{ "Today is @{dateformat(today())} which is @{weekdayname(today())}", true }, |
|||
{ "Date: @{dateformat(0 + ${days_offset}, \"long\")}", true }, |
|||
{ "In @{format(${days_offset})} days: @{dateformat(today() + ${days_offset})}", true }, |
|||
|
|||
// Nested function calls
|
|||
{ "@{upper(weekdayname(today()))}", true }, |
|||
{ "@{lower(dateformat(today(), \"long\"))}", true }, |
|||
|
|||
// Multiple date calculations
|
|||
{ "Start: @{dateformat(0)} End: @{dateformat(365)} Duration: @{365 - 0} days", true }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldWork ) |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Error in expression: " + testCase.expression + |
|||
" Result: " + result ); |
|||
BOOST_CHECK( !result.empty() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test performance of date operations |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( DatePerformance ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Test that date operations are reasonably fast
|
|||
auto start = std::chrono::high_resolution_clock::now(); |
|||
|
|||
// Perform many date operations
|
|||
for( int i = 0; i < 1000; ++i ) |
|||
{ |
|||
auto result = evaluator.Evaluate( "@{dateformat(" + std::to_string(i) + ")}" ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
} |
|||
|
|||
auto end = std::chrono::high_resolution_clock::now(); |
|||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ); |
|||
|
|||
// Should complete in reasonable time (less than 100 milliseconds for 1000 operations)
|
|||
BOOST_CHECK_LT( duration.count(), 100 ); |
|||
} |
|||
|
|||
BOOST_AUTO_TEST_SUITE_END() |
@ -0,0 +1,441 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.TXT for contributors. |
|||
* |
|||
* This program is free software; you can redistribute it and/or |
|||
* modify it under the terms of the GNU General Public License |
|||
* as published by the Free Software Foundation; either version 2 |
|||
* of the License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License |
|||
* along with this program; if not, you may find one here: |
|||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|||
* or you may search the http://www.gnu.org website for the version 2 license,
|
|||
* or you may write to the Free Software Foundation, Inc., |
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|||
*/ |
|||
|
|||
/**
|
|||
* @file |
|||
* Integration tests for text_eval_parser functionality including real-world scenarios |
|||
*/ |
|||
|
|||
#include <qa_utils/wx_utils/unit_test_utils.h>
|
|||
|
|||
// Code under test
|
|||
#include <text_eval/text_eval_wrapper.h>
|
|||
|
|||
#include <chrono>
|
|||
#include <regex>
|
|||
|
|||
/**
|
|||
* Declare the test suite |
|||
*/ |
|||
BOOST_AUTO_TEST_SUITE( TextEvalParserIntegration ) |
|||
|
|||
/**
|
|||
* Test real-world expression scenarios |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( RealWorldScenarios ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Set up variables that might be used in actual KiCad projects
|
|||
evaluator.SetVariable( "board_width", 100.0 ); |
|||
evaluator.SetVariable( "board_height", 80.0 ); |
|||
evaluator.SetVariable( "trace_width", 0.2 ); |
|||
evaluator.SetVariable( "component_count", 45.0 ); |
|||
evaluator.SetVariable( "revision", 3.0 ); |
|||
evaluator.SetVariable( std::string("project_name"), std::string("My PCB Project") ); |
|||
evaluator.SetVariable( std::string("designer"), std::string("John Doe") ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expectedPattern; // Can be exact match or regex pattern
|
|||
bool isRegex; |
|||
bool shouldError; |
|||
std::string description; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Board dimension calculations
|
|||
{ |
|||
"Board area: @{${board_width} * ${board_height}} mm²", |
|||
"Board area: 8000 mm²", |
|||
false, false, |
|||
"Board area calculation" |
|||
}, |
|||
{ |
|||
"Perimeter: @{2 * (${board_width} + ${board_height})} mm", |
|||
"Perimeter: 360 mm", |
|||
false, false, |
|||
"Board perimeter calculation" |
|||
}, |
|||
{ |
|||
"Diagonal: @{format(sqrt(pow(${board_width}, 2) + pow(${board_height}, 2)), 1)} mm", |
|||
"Diagonal: 128.1 mm", |
|||
false, false, |
|||
"Board diagonal calculation" |
|||
}, |
|||
|
|||
// Text formatting scenarios
|
|||
{ |
|||
"Project: ${project_name} | Designer: ${designer} | Rev: @{${revision}}", |
|||
"Project: My PCB Project | Designer: John Doe | Rev: 3", |
|||
false, false, |
|||
"Title block information" |
|||
}, |
|||
{ |
|||
"Components: @{${component_count}} | Density: @{format(${component_count} / (${board_width} * ${board_height} / 10000), 2)} per cm²", |
|||
"Components: 45 | Density: 56.25 per cm²", |
|||
false, false, |
|||
"Component density calculation" |
|||
}, |
|||
|
|||
// Date-based revision tracking
|
|||
{ |
|||
"Created: @{dateformat(today())} | Build: @{today()} days since epoch", |
|||
R"(Created: \d{4}-\d{2}-\d{2} \| Build: \d+ days since epoch)", |
|||
true, false, |
|||
"Date-based tracking" |
|||
}, |
|||
|
|||
// Conditional formatting
|
|||
{ |
|||
"Status: @{if(${component_count} > 50, \"Complex\", \"Simple\")} design", |
|||
"Status: Simple design", |
|||
false, false, |
|||
"Conditional design complexity" |
|||
}, |
|||
{ |
|||
"Status: @{if(${trace_width} >= 0.2, \"Standard\", \"Fine pitch\")} (@{${trace_width}}mm)", |
|||
"Status: Standard (0.2mm)", |
|||
false, false, |
|||
"Conditional trace width description" |
|||
}, |
|||
|
|||
// Multi-line documentation
|
|||
{ |
|||
"PCB Summary:\n- Size: @{${board_width}}×@{${board_height}}mm\n- Area: @{${board_width} * ${board_height}}mm²\n- Components: @{${component_count}}", |
|||
"PCB Summary:\n- Size: 100×80mm\n- Area: 8000mm²\n- Components: 45", |
|||
false, false, |
|||
"Multi-line documentation" |
|||
}, |
|||
|
|||
// Error scenarios - undefined variables error and return unchanged
|
|||
{ |
|||
"Invalid: @{${undefined_var}} test", |
|||
"Invalid: @{${undefined_var}} test", |
|||
false, true, |
|||
"Undefined variable behavior" |
|||
}, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK_MESSAGE( evaluator.HasErrors(), |
|||
"Expected error for: " + testCase.description ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Unexpected error for: " + testCase.description + |
|||
" - " + evaluator.GetErrorSummary().ToStdString() ); |
|||
|
|||
if( testCase.isRegex ) |
|||
{ |
|||
std::regex pattern( testCase.expectedPattern ); |
|||
BOOST_CHECK_MESSAGE( std::regex_match( result.ToStdString(), pattern ), |
|||
"Result '" + result.ToStdString() + "' doesn't match pattern '" + |
|||
testCase.expectedPattern + "' for: " + testCase.description ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( result.ToStdString() == testCase.expectedPattern, |
|||
"Expected '" + testCase.expectedPattern + "' but got '" + |
|||
result.ToStdString() + "' for: " + testCase.description ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test callback-based variable resolution |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( CallbackVariableResolution ) |
|||
{ |
|||
// Create evaluator with custom callback
|
|||
auto variableCallback = []( const std::string& varName ) -> calc_parser::Result<calc_parser::Value> { |
|||
if( varName == "dynamic_value" ) |
|||
return calc_parser::MakeValue<calc_parser::Value>( 42.0 ); |
|||
else if( varName == "dynamic_string" ) |
|||
return calc_parser::MakeValue<calc_parser::Value>( std::string("Hello from callback") ); |
|||
else if( varName == "computed_value" ) |
|||
return calc_parser::MakeValue<calc_parser::Value>( std::sin( 3.14159 / 4 ) * 100.0 ); // Should be about 70.7
|
|||
else |
|||
return calc_parser::MakeError<calc_parser::Value>( "Variable '" + varName + "' not found in callback" ); |
|||
}; |
|||
|
|||
EXPRESSION_EVALUATOR evaluator( variableCallback, false ); |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
double tolerance; |
|||
bool shouldError; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
{ "@{${dynamic_value}}", "42", 0, false }, |
|||
{ "Message: ${dynamic_string}", "Message: Hello from callback", 0, false }, |
|||
{ "@{format(${computed_value}, 1)}", "70.7", 0.1, false }, |
|||
{ "@{${dynamic_value} + ${computed_value}}", "112.7", 0.1, false }, |
|||
{ "${nonexistent}", "${nonexistent}", 0, true }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK( evaluator.HasErrors() ); |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
if( testCase.tolerance > 0 ) |
|||
{ |
|||
// For floating point comparisons, extract the number
|
|||
std::regex numberRegex( R"([\d.]+)" ); |
|||
std::smatch match; |
|||
std::string resultStr = result.ToStdString(); |
|||
if( std::regex_search( resultStr, match, numberRegex ) ) |
|||
{ |
|||
double actualValue = std::stod( match[0].str() ); |
|||
double expectedValue = std::stod( testCase.expected ); |
|||
BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.tolerance * 100 ); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_EQUAL( result, testCase.expected ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test concurrent/thread safety (basic test) |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ThreadSafety ) |
|||
{ |
|||
// Create multiple evaluators that could be used in different threads
|
|||
std::vector<std::unique_ptr<EXPRESSION_EVALUATOR>> evaluators; |
|||
|
|||
for( int i = 0; i < 10; ++i ) |
|||
{ |
|||
auto evaluator = std::make_unique<EXPRESSION_EVALUATOR>(); |
|||
evaluator->SetVariable( "thread_id", static_cast<double>( i ) ); |
|||
evaluator->SetVariable( "multiplier", 5.0 ); |
|||
evaluators.push_back( std::move( evaluator ) ); |
|||
} |
|||
|
|||
// Test that each evaluator maintains its own state
|
|||
for( int i = 0; i < 10; ++i ) |
|||
{ |
|||
auto result = evaluators[i]->Evaluate( "@{${thread_id} * ${multiplier}}" ); |
|||
BOOST_CHECK( !evaluators[i]->HasErrors() ); |
|||
|
|||
double expected = static_cast<double>( i * 5 ); |
|||
double actual = std::stod( result.ToStdString() ); |
|||
BOOST_CHECK_CLOSE( actual, expected, 0.001 ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test memory management and large expressions |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( MemoryManagement ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Test large nested expressions
|
|||
std::string complexExpression = "@{"; |
|||
for( int i = 0; i < 100; ++i ) |
|||
{ |
|||
if( i > 0 ) complexExpression += " + "; |
|||
complexExpression += std::to_string( i ); |
|||
} |
|||
complexExpression += "}"; |
|||
|
|||
auto result = evaluator.Evaluate( complexExpression ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
|
|||
// Sum of 0..99 is 4950
|
|||
BOOST_CHECK_EQUAL( result, "4950" ); |
|||
|
|||
// Test many small expressions
|
|||
for( int i = 0; i < 1000; ++i ) |
|||
{ |
|||
auto expr = "@{" + std::to_string( i ) + " * 2}"; |
|||
auto result = evaluator.Evaluate( expr ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK_EQUAL( result, std::to_string( i * 2 ) ); |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test edge cases in parsing and evaluation |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( ParsingEdgeCases ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
struct TestCase { |
|||
std::string expression; |
|||
std::string expected; |
|||
bool shouldError; |
|||
double precision; |
|||
std::string description; |
|||
}; |
|||
|
|||
const std::vector<TestCase> cases = { |
|||
// Whitespace handling
|
|||
{ "@{ 2 + 3 }", "5", false, 0.0, "Spaces in expression" }, |
|||
{ "@{\t2\t+\t3\t}", "5", false, 0.0, "Tabs in expression" }, |
|||
{ "@{\n2\n+\n3\n}", "5", false, 0.0, "Newlines in expression" }, |
|||
|
|||
// String escaping and special characters
|
|||
{ "@{\"Hello\\\"World\\\"\"}", "Hello\"World\"", false, 0.0, "Escaped quotes in string" }, |
|||
{ "@{\"Line1\\nLine2\"}", "Line1\nLine2", false, 0.0, "Newline in string" }, |
|||
|
|||
// Multiple calculations in complex text
|
|||
{ "A: @{1+1}, B: @{2*2}, C: @{3^2}", "A: 2, B: 4, C: 9", false, 0.0, "Multiple calculations" }, |
|||
|
|||
// Edge cases with parentheses
|
|||
{ "@{((((2))))}", "2", false, 0.0, "Multiple nested parentheses" }, |
|||
{ "@{(2 + 3) * (4 + 5)}", "45", false, 0.0, "Grouped operations" }, |
|||
|
|||
// Empty and minimal expressions
|
|||
{ "No calculations here", "No calculations here", false, 0.0, "Plain text" }, |
|||
{ "", "", false, 0.0, "Empty string" }, |
|||
{ "@{0}", "0", false, 0.0, "Zero value" }, |
|||
{ "@{-0}", "0", false, 0.0, "Negative zero" }, |
|||
|
|||
// Precision and rounding edge cases
|
|||
{ "@{0.1 + 0.2}", "0.3", false, 0.01, "Floating point precision" }, |
|||
{ "@{1.0 / 3.0}", "0.333333", false, 0.01, "Repeating decimal" }, |
|||
|
|||
// Large numbers
|
|||
{ "@{1000000 * 1000000}", "1e+12", false, 0.01, "Large number result" }, |
|||
|
|||
// Error recovery - malformed expressions left unchanged, valid ones evaluated
|
|||
{ "Good @{2+2} bad @{2+} good @{3+3}", "Good 4 bad @{2+} good 6", true, 0.0, "Error recovery" }, |
|||
}; |
|||
|
|||
for( const auto& testCase : cases ) |
|||
{ |
|||
auto result = evaluator.Evaluate( testCase.expression ); |
|||
|
|||
if( testCase.shouldError ) |
|||
{ |
|||
BOOST_CHECK_MESSAGE( evaluator.HasErrors(), "Expected error for: " + testCase.description ); |
|||
} |
|||
else |
|||
{ |
|||
if( testCase.precision > 0.0 ) |
|||
{ |
|||
// For floating point comparisons, extract the number
|
|||
std::regex numberRegex( R"([\d.eE+-]+)" ); |
|||
std::smatch match; |
|||
std::string resultStr = result.ToStdString(); |
|||
if( std::regex_search( resultStr, match, numberRegex ) ) |
|||
{ |
|||
double actualValue = std::stod( match[0].str() ); |
|||
double expectedValue = std::stod( testCase.expected ); |
|||
BOOST_CHECK_CLOSE( actualValue, expectedValue, testCase.precision * 100 ); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
BOOST_CHECK_MESSAGE( !evaluator.HasErrors(), |
|||
"Unexpected error for: " + testCase.description + |
|||
" - " + evaluator.GetErrorSummary() ); |
|||
BOOST_CHECK_MESSAGE( result == testCase.expected, |
|||
"Expected '" + testCase.expected + "' but got '" + |
|||
result + "' for: " + testCase.description ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/**
|
|||
* Test performance with realistic workloads |
|||
*/ |
|||
BOOST_AUTO_TEST_CASE( RealWorldPerformance ) |
|||
{ |
|||
EXPRESSION_EVALUATOR evaluator; |
|||
|
|||
// Set up variables for a typical PCB project
|
|||
evaluator.SetVariable( "board_layers", 4.0 ); |
|||
evaluator.SetVariable( "component_count", 150.0 ); |
|||
evaluator.SetVariable( "net_count", 200.0 ); |
|||
evaluator.SetVariable( "via_count", 300.0 ); |
|||
evaluator.SetVariable( "board_width", 120.0 ); |
|||
evaluator.SetVariable( "board_height", 80.0 ); |
|||
|
|||
// Simulate processing many text objects (like in a real PCB layout)
|
|||
std::vector<std::string> expressions = { |
|||
"Layer @{${board_layers}}/4", |
|||
"Components: @{${component_count}}", |
|||
"Nets: @{${net_count}}", |
|||
"Vias: @{${via_count}}", |
|||
"Area: @{${board_width} * ${board_height}} mm²", |
|||
"Density: @{format(${component_count} / (${board_width} * ${board_height} / 100), 1)} /cm²", |
|||
"Via density: @{format(${via_count} / (${board_width} * ${board_height} / 100), 1)} /cm²", |
|||
"Layer utilization: @{format(${net_count} / ${board_layers}, 1)} nets/layer", |
|||
"Design complexity: @{if(${component_count} > 100, \"High\", \"Low\")}", |
|||
"Board aspect ratio: @{format(${board_width} / ${board_height}, 2)}:1", |
|||
}; |
|||
|
|||
auto start = std::chrono::high_resolution_clock::now(); |
|||
|
|||
// Process expressions many times (simulating real usage)
|
|||
for( int iteration = 0; iteration < 100; ++iteration ) |
|||
{ |
|||
for( const auto& expr : expressions ) |
|||
{ |
|||
auto result = evaluator.Evaluate( expr ); |
|||
BOOST_CHECK( !evaluator.HasErrors() ); |
|||
BOOST_CHECK( !result.empty() ); |
|||
} |
|||
} |
|||
|
|||
auto end = std::chrono::high_resolution_clock::now(); |
|||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ); |
|||
|
|||
// Should process 1000 expressions in reasonable time (less than 100 ms)
|
|||
BOOST_CHECK_LT( duration.count(), 100 ); |
|||
|
|||
// Test that results are consistent
|
|||
for( auto& expr : expressions ) |
|||
{ |
|||
auto result1 = evaluator.Evaluate( expr ); |
|||
auto result2 = evaluator.Evaluate( expr ); |
|||
BOOST_CHECK_EQUAL( result1, result2 ); |
|||
} |
|||
} |
|||
|
|||
BOOST_AUTO_TEST_SUITE_END() |
@ -0,0 +1,54 @@ |
|||
/*
|
|||
* This program source code file is part of KiCad, a free EDA CAD application. |
|||
* |
|||
* Copyright The KiCad Developers, see AUTHORS.txt for contributors. |
|||
* |
|||
* This program is free software; you can redistribute it and/or |
|||
* modify it under the terms of the GNU General Public License |
|||
* as published by the Free Software Foundation; either version 2 |
|||
* of the License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU General Public License |
|||
* along with this program; if not, you may find one here: |
|||
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|||
* or you may search the http://www.gnu.org website for the version 2 license,
|
|||
* or you may write to the Free Software Foundation, Inc., |
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|||
*/ |
|||
|
|||
/**
|
|||
* @file |
|||
* Test rendering helper functions with expression evaluation. |
|||
*/ |
|||
|
|||
#include <boost/test/unit_test.hpp>
|
|||
#include <gr_text.h>
|
|||
#include <font/font.h>
|
|||
#include <math/util.h>
|
|||
|
|||
BOOST_AUTO_TEST_SUITE( TextEvalRender ) |
|||
|
|||
BOOST_AUTO_TEST_CASE( GrTextWidthEval ) |
|||
{ |
|||
KIFONT::FONT* font = KIFONT::FONT::GetFont(); |
|||
VECTOR2I size( 100, 100 ); |
|||
int thickness = 1; |
|||
const KIFONT::METRICS& metrics = KIFONT::METRICS::Default(); |
|||
|
|||
int widthExpr = GRTextWidth( wxS( "@{1+1}" ), font, size, thickness, false, false, metrics ); |
|||
int widthExpected = KiROUND( font->StringBoundaryLimits( wxS( "2" ), size, thickness, false, |
|||
false, metrics ).x ); |
|||
int widthRaw = KiROUND( font->StringBoundaryLimits( wxS( "@{1+1}" ), size, thickness, false, |
|||
false, metrics ).x ); |
|||
|
|||
BOOST_CHECK_EQUAL( widthExpr, widthExpected ); |
|||
BOOST_CHECK( widthExpr != widthRaw ); |
|||
} |
|||
|
|||
BOOST_AUTO_TEST_SUITE_END() |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue