mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-09-13 17:53:11 +02:00
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:
parent
bae5d43c45
commit
a857ea77d9
@ -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 )
|
||||
|
@ -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();
|
||||
|
@ -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 );
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 );
|
||||
}
|
||||
|
200
common/text_eval/text_eval.lemon
Normal file
200
common/text_eval/text_eval.lemon
Normal 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();
|
||||
}
|
||||
}
|
636
common/text_eval/text_eval_parser.cpp
Normal file
636
common/text_eval/text_eval_parser.cpp
Normal 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
|
2068
common/text_eval/text_eval_wrapper.cpp
Normal file
2068
common/text_eval/text_eval_wrapper.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
454
include/text_eval/text_eval_parser.h
Normal file
454
include/text_eval/text_eval_parser.h
Normal 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
|
||||
|
120
include/text_eval/text_eval_types.h
Normal file
120
include/text_eval/text_eval_types.h
Normal 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;
|
||||
}
|
267
include/text_eval/text_eval_units.h
Normal file
267
include/text_eval/text_eval_units.h
Normal 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
|
529
include/text_eval/text_eval_wrapper.h
Normal file
529
include/text_eval/text_eval_wrapper.h
Normal 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();
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 );
|
||||
|
@ -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
|
||||
|
115
qa/tests/common/text_eval/README.md
Normal file
115
qa/tests/common/text_eval/README.md
Normal 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.
|
697
qa/tests/common/text_eval/test_text_eval_numeric_compat.cpp
Normal file
697
qa/tests/common/text_eval/test_text_eval_numeric_compat.cpp
Normal 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()
|
589
qa/tests/common/text_eval/test_text_eval_parser.cpp
Normal file
589
qa/tests/common/text_eval/test_text_eval_parser.cpp
Normal 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()
|
508
qa/tests/common/text_eval/test_text_eval_parser_core.cpp
Normal file
508
qa/tests/common/text_eval/test_text_eval_parser_core.cpp
Normal 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()
|
471
qa/tests/common/text_eval/test_text_eval_parser_datetime.cpp
Normal file
471
qa/tests/common/text_eval/test_text_eval_parser_datetime.cpp
Normal 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()
|
441
qa/tests/common/text_eval/test_text_eval_parser_integration.cpp
Normal file
441
qa/tests/common/text_eval/test_text_eval_parser_integration.cpp
Normal 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()
|
54
qa/tests/common/text_eval/test_text_eval_render.cpp
Normal file
54
qa/tests/common/text_eval/test_text_eval_render.cpp
Normal 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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user