ADDED: Text expression evaluation

Arbitrary text strings now support full evaluation with a rich
functional language

Fixes https://gitlab.com/kicad/code/kicad/-/issues/6643
This commit is contained in:
Seth Hillbrand 2025-09-01 08:41:25 -07:00
parent bae5d43c45
commit a857ea77d9
26 changed files with 7228 additions and 4 deletions

View File

@ -130,6 +130,9 @@ set( KICOMMON_SRCS
project/project_local_settings.cpp
project/time_domain_parameters.cpp
text_eval/text_eval_wrapper.cpp
text_eval/text_eval_parser.cpp
# This is basically a settings object, but for the toolbar
tool/ui/toolbar_configuration.cpp
@ -1014,6 +1017,13 @@ generate_lemon_grammar(
libeval_compiler/grammar.lemon
)
generate_lemon_grammar(
kicommon
text_eval
text_eval/text_eval_wrapper.cpp
text_eval/text_eval.lemon
)
# auto-generate stroke_params_lexer.h and stroke_params_keywords.cpp
# Called twice one for common and one for gal, to ensure the files are created
# on all devel tools ( Linux and msys2 )

View File

@ -35,6 +35,7 @@
#include <font/glyph.h>
#include <gr_text.h>
#include <string_utils.h> // for UnescapeString
#include <text_eval/text_eval_wrapper.h>
#include <math/util.h> // for KiROUND
#include <math/vector2d.h>
#include <core/kicad_algo.h>
@ -638,6 +639,14 @@ void EDA_TEXT::cacheShownText()
}
wxString EDA_TEXT::EvaluateText( const wxString& aText ) const
{
static EXPRESSION_EVALUATOR evaluator;
return evaluator.Evaluate( aText );
}
KIFONT::FONT* EDA_TEXT::GetDrawFont( const RENDER_SETTINGS* aSettings ) const
{
KIFONT::FONT* font = GetFont();

View File

@ -29,6 +29,7 @@
#include <trigo.h>
#include <math/util.h> // for KiROUND
#include <font/font.h>
#include <text_eval/text_eval_wrapper.h>
#include <callback_gal.h>
@ -100,8 +101,15 @@ int GRTextWidth( const wxString& aText, KIFONT::FONT* aFont, const VECTOR2I& aSi
{
if( !aFont )
aFont = KIFONT::FONT::GetFont();
wxString evaluated( aText );
return KiROUND( aFont->StringBoundaryLimits( aText, aSize, aThickness, aBold, aItalic,
if( evaluated.Contains( wxS( "@{" ) ) )
{
EXPRESSION_EVALUATOR evaluator;
evaluated = evaluator.Evaluate( evaluated );
}
return KiROUND( aFont->StringBoundaryLimits( evaluated, aSize, aThickness, aBold, aItalic,
aFontMetrics ).x );
}
@ -114,6 +122,13 @@ void GRPrintText( wxDC* aDC, const VECTOR2I& aPos, const COLOR4D& aColor, const
{
KIGFX::GAL_DISPLAY_OPTIONS empty_opts;
bool fill_mode = true;
wxString evaluatedText( aText );
if( evaluatedText.Contains( wxS( "@{" ) ) )
{
EXPRESSION_EVALUATOR evaluator;
evaluatedText = evaluator.Evaluate( evaluatedText );
}
if( !aFont )
aFont = KIFONT::FONT::GetFont();
@ -156,7 +171,7 @@ void GRPrintText( wxDC* aDC, const VECTOR2I& aPos, const COLOR4D& aColor, const
attributes.m_Valign = aV_justify;
attributes.m_Size = aSize;
aFont->Draw( &callback_gal, aText, aPos, attributes, aFontMetrics );
aFont->Draw( &callback_gal, evaluatedText, aPos, attributes, aFontMetrics );
}

View File

@ -38,6 +38,7 @@
#include <trigo.h>
#include <plotters/plotter.h>
#include <text_eval/text_eval_wrapper.h>
#include <geometry/shape_line_chain.h>
#include <bezier_curves.h>
#include <callback_gal.h>
@ -637,6 +638,13 @@ void PLOTTER::Text( const VECTOR2I& aPos,
void* aData )
{
KIGFX::GAL_DISPLAY_OPTIONS empty_opts;
wxString text( aText );
if( text.Contains( wxS( "@{" ) ) )
{
EXPRESSION_EVALUATOR evaluator;
text = evaluator.Evaluate( text );
}
SetColor( aColor );
@ -680,7 +688,7 @@ void PLOTTER::Text( const VECTOR2I& aPos,
if( !aFont )
aFont = KIFONT::FONT::GetFont( m_renderSettings->GetDefaultFont() );
aFont->Draw( &callback_gal, aText, aPos, attributes, aFontMetrics );
aFont->Draw( &callback_gal, text, aPos, attributes, aFontMetrics );
}
@ -693,6 +701,13 @@ void PLOTTER::PlotText( const VECTOR2I& aPos,
void* aData )
{
KIGFX::GAL_DISPLAY_OPTIONS empty_opts;
wxString text( aText );
if( text.Contains( wxS( "@{" ) ) )
{
EXPRESSION_EVALUATOR evaluator;
text = evaluator.Evaluate( text );
}
TEXT_ATTRIBUTES attributes = aAttributes;
int penWidth = attributes.m_StrokeWidth;
@ -725,5 +740,5 @@ void PLOTTER::PlotText( const VECTOR2I& aPos,
if( !aFont )
aFont = KIFONT::FONT::GetFont( m_renderSettings->GetDefaultFont() );
aFont->Draw( &callback_gal, aText, aPos, attributes, aFontMetrics );
aFont->Draw( &callback_gal, text, aPos, attributes, aFontMetrics );
}

View File

@ -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();
}
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -286,6 +286,9 @@ wxString SCH_FIELD::GetShownText( const SCH_SHEET_PATH* aPath, bool aAllowExtraT
if( m_id == FIELD_T::SHEET_FILENAME && aAllowExtraText && !IsNameShown() )
text = _( "File:" ) + wxS( " " ) + text;
if( text.Contains( wxT( "@{" ) ) )
text = EvaluateText( text );
return text;
}

View File

@ -906,6 +906,9 @@ wxString SCH_LABEL_BASE::GetShownText( const SCH_SHEET_PATH* aPath, bool aAllowE
text = ExpandTextVars( text, &textResolver );
}
if( text.Contains( wxT( "@{" ) ) )
text = EvaluateText( text );
return text;
}

View File

@ -355,6 +355,9 @@ wxString SCH_TEXT::GetShownText( const SCH_SHEET_PATH* aPath, bool aAllowExtraTe
text = ExpandTextVars( text, &textResolver );
}
if( text.Contains( wxT( "@{" ) ) )
text = EvaluateText( text );
return text;
}

View File

@ -297,6 +297,9 @@ wxString SCH_TEXTBOX::GetShownText( const RENDER_SETTINGS* aSettings, const SCH_
text = ExpandTextVars( text, &textResolver );
}
if( text.Contains( wxT( "@{" ) ) )
text = EvaluateText( text );
VECTOR2I size = GetEnd() - GetStart();
int colWidth;

View File

@ -117,6 +117,8 @@ public:
virtual void SetText( const wxString& aText );
wxString EvaluateText( const wxString& aText ) const;
/**
* The TextThickness is that set by the user. The EffectiveTextPenWidth also factors
* in bold text and thickness clamping.

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

View File

@ -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;
}

View File

@ -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

View File

@ -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();
};

View File

@ -168,6 +168,9 @@ wxString PCB_TEXT::GetShownText( bool aAllowExtraText, int aDepth ) const
text = ExpandTextVars( text, &resolver );
}
if( text.Contains( wxT( "@{" ) ) )
text = EvaluateText( text );
return text;
}

View File

@ -445,6 +445,9 @@ wxString PCB_TEXTBOX::GetShownText( bool aAllowExtraText, int aDepth ) const
text = ExpandTextVars( text, &resolver );
}
if( text.Contains( wxT( "@{" ) ) )
text = EvaluateText( text );
KIFONT::FONT* font = GetDrawFont( nullptr );
EDA_ANGLE drawAngle = GetDrawRotation();
std::vector<VECTOR2I> corners = GetCornersInSequence( drawAngle );

View File

@ -61,6 +61,12 @@ set( QA_COMMON_SRCS
test_grid_helper.cpp
test_richio.cpp
test_text_attributes.cpp
text_eval/test_text_eval_parser.cpp
text_eval/test_text_eval_parser_core.cpp
text_eval/test_text_eval_parser_datetime.cpp
text_eval/test_text_eval_parser_integration.cpp
text_eval/test_text_eval_numeric_compat.cpp
text_eval/test_text_eval_render.cpp
test_title_block.cpp
test_types.cpp
test_utf8.cpp

View File

@ -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.

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()