KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_text_eval_parser_datetime.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
24
27#include <fmt/ranges.h>
28
29#include <chrono>
30#include <regex>
31
32BOOST_AUTO_TEST_SUITE( TextEvalParserDateTime )
33
34BOOST_AUTO_TEST_CASE( DateFormatting )
35{
36 EXPRESSION_EVALUATOR evaluator;
37
38 struct TestCase {
39 std::string expression;
40 std::string expected;
41 bool shouldError;
42 };
43
44 const std::vector<TestCase> cases = {
45 // Test Unix epoch date (1970-01-01)
46 { "@{dateformat(0)}", "1970-01-01", false },
47 { "@{dateformat(0, \"ISO\")}", "1970-01-01", false },
48 { "@{dateformat(0, \"iso\")}", "1970-01-01", false },
49 { "@{dateformat(0, \"US\")}", "01/01/1970", false },
50 { "@{dateformat(0, \"us\")}", "01/01/1970", false },
51 { "@{dateformat(0, \"EU\")}", "01/01/1970", false },
52 { "@{dateformat(0, \"european\")}", "01/01/1970", false },
53 { "@{dateformat(0, \"long\")}", "January 1, 1970", false },
54 { "@{dateformat(0, \"short\")}", "Jan 1, 1970", false },
55
56 // Test some known dates
57 { "@{dateformat(365)}", "1971-01-01", false }, // One year after epoch
58 { "@{dateformat(1000)}", "1972-09-27", false }, // 1000 days after epoch
59
60 // Test weekday names
61 { "@{weekdayname(0)}", "Thursday", false }, // Unix epoch was Thursday
62 { "@{weekdayname(1)}", "Friday", false }, // Next day
63 { "@{weekdayname(2)}", "Saturday", false }, // Weekend
64 { "@{weekdayname(3)}", "Sunday", false },
65 { "@{weekdayname(4)}", "Monday", false },
66 { "@{weekdayname(5)}", "Tuesday", false },
67 { "@{weekdayname(6)}", "Wednesday", false },
68 { "@{weekdayname(7)}", "Thursday", false }, // Week cycles
69
70 // Test negative dates (before epoch)
71 { "@{dateformat(-1)}", "1969-12-31", false },
72 { "@{weekdayname(-1)}", "Wednesday", false },
73 };
74
75 for( const auto& testCase : cases )
76 {
77 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
78
79 if( testCase.shouldError )
80 {
81 BOOST_CHECK( evaluator.HasErrors() );
82 }
83 else
84 {
85 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
86 "Error in expression: " + testCase.expression +
87 " Errors: " + evaluator.GetErrorSummary().ToStdString() );
88 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
89 }
90 }
91}
92
96BOOST_AUTO_TEST_CASE( CJKDateFormatting )
97{
98 EXPRESSION_EVALUATOR evaluator;
99
100 struct TestCase {
101 std::string expression;
102 std::string expected;
103 bool shouldError;
104 };
105
106 const std::vector<TestCase> cases = {
107 // Test Unix epoch date (1970-01-01) in CJK formats
108 { "@{dateformat(0, \"Chinese\")}", "1970年01月01日", false },
109 { "@{dateformat(0, \"chinese\")}", "1970年01月01日", false },
110 { "@{dateformat(0, \"CN\")}", "1970年01月01日", false },
111 { "@{dateformat(0, \"cn\")}", "1970年01月01日", false },
112
113 { "@{dateformat(0, \"Japanese\")}", "1970年01月01日", false },
114 { "@{dateformat(0, \"japanese\")}", "1970年01月01日", false },
115 { "@{dateformat(0, \"JP\")}", "1970年01月01日", false },
116 { "@{dateformat(0, \"jp\")}", "1970年01月01日", false },
117
118 { "@{dateformat(0, \"Korean\")}", "1970년 01월 01일", false },
119 { "@{dateformat(0, \"korean\")}", "1970년 01월 01일", false },
120 { "@{dateformat(0, \"KR\")}", "1970년 01월 01일", false },
121 { "@{dateformat(0, \"kr\")}", "1970년 01월 01일", false },
122
123 // Test some other dates in CJK formats
124 { "@{dateformat(365, \"Chinese\")}", "1971年01月01日", false }, // One year after epoch
125 { "@{dateformat(365, \"Japanese\")}", "1971年01月01日", false }, // One year after epoch
126 { "@{dateformat(365, \"Korean\")}", "1971년 01월 01일", false }, // One year after epoch
127
128 { "@{dateformat(1000, \"Chinese\")}", "1972年09月27日", false }, // 1000 days after epoch
129 { "@{dateformat(1000, \"Japanese\")}", "1972年09月27日", false }, // 1000 days after epoch
130 { "@{dateformat(1000, \"Korean\")}", "1972년 09월 27일", false }, // 1000 days after epoch
131
132 // Test negative dates (before epoch) in CJK formats
133 { "@{dateformat(-1, \"Chinese\")}", "1969年12月31日", false },
134 { "@{dateformat(-1, \"Japanese\")}", "1969年12月31日", false },
135 { "@{dateformat(-1, \"Korean\")}", "1969년 12월 31일", false },
136
137 // Test leap year date (Feb 29, 1972) in CJK formats
138 { "@{dateformat(789, \"Chinese\")}", "1972年02月29日", false }, // Feb 29, 1972 (leap year)
139 { "@{dateformat(789, \"Japanese\")}", "1972年02月29日", false }, // Feb 29, 1972 (leap year)
140 { "@{dateformat(789, \"Korean\")}", "1972년 02월 29일", false }, // Feb 29, 1972 (leap year)
141 };
142
143 for( const auto& testCase : cases )
144 {
145 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
146
147 if( testCase.shouldError )
148 {
149 BOOST_CHECK( evaluator.HasErrors() );
150 }
151 else
152 {
153 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
154 "Error in expression: " + testCase.expression +
155 " Errors: " + evaluator.GetErrorSummary().ToStdString() );
156 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
157 }
158 }
159}
160
164BOOST_AUTO_TEST_CASE( CJKDateParsing )
165{
166 EXPRESSION_EVALUATOR evaluator;
167
168 struct TestCase {
169 std::string expression;
170 std::string expected;
171 bool shouldError;
172 };
173
174 const std::vector<TestCase> cases = {
175 // Test if basic functions work first
176 { "@{dateformat(0)}", "1970-01-01", false }, // Test basic dateformat
177 { "@{upper(\"test\")}", "TEST", false }, // Test basic string function
178
179 // Test ASCII date parsing first to see if datestring function works
180 { "@{datestring(\"2024-03-15\")}", "19797", false }, // Test ASCII date
181 { "@{datestring(\"1970-01-01\")}", "0", false }, // Unix epoch
182
183 // Test Chinese date parsing (年月日)
184 { "@{datestring('2024年03月15日')}", "19797", false }, // Days since epoch for 2024-03-15
185 { "@{datestring('1970年01月01日')}", "0", false }, // Unix epoch
186 { "@{datestring('2024年01月01日')}", "19723", false }, // New Year 2024
187 { "@{datestring('1972年02月29日')}", "789", false }, // Leap year date
188 { "@{datestring('1969年12月31日')}", "-1", false }, // Day before epoch
189
190 // Test Korean date parsing (년월일) with spaces
191 { "@{datestring(\"2024년 03월 15일\")}", "19797", false }, // Days since epoch for 2024-03-15
192 { "@{datestring(\"1970년 01월 01일\")}", "0", false }, // Unix epoch
193 { "@{datestring(\"2024년 01월 01일\")}", "19723", false }, // New Year 2024
194 { "@{datestring(\"1972년 02월 29일\")}", "789", false }, // Leap year date
195 { "@{datestring(\"1969년 12월 31일\")}", "-1", false }, // Day before epoch
196
197 // Test Korean date parsing (년월일) without spaces
198 { "@{datestring(\"2024년03월15일\")}", "19797", false }, // Days since epoch for 2024-03-15
199 { "@{datestring(\"1970년01월01일\")}", "0", false }, // Unix epoch
200
201 // Test integration: parse CJK date and format in different style
202 { "@{dateformat(datestring('2024年03월15일'), 'ISO')}", "2024-03-15", false },
203 { "@{dateformat(datestring('2024년 03월 15일'), 'ISO')}", "2024-03-15", false },
204 { "@{dateformat(datestring('1970年01月01日'), 'US')}", "01/01/1970", false },
205 { "@{dateformat(datestring('1970년 01월 01일'), 'EU')}", "01/01/1970", false },
206
207 // Test round-trip: CJK -> parse -> format back to CJK
208 { "@{dateformat(datestring('2024年03월15日'), 'Chinese')}", "2024年03月15日", false },
209 { "@{dateformat(datestring('2024년 03월 15일'), 'Korean')}", "2024년 03월 15일", false },
210
211 // Test invalid CJK dates (should error)
212 { "@{datestring('2024年13月15日')}", "", true }, // Invalid month
213 { "@{datestring('2024년 02월 30일')}", "", true }, // Invalid day for February
214 { "@{datestring('2024年02月')}", "", true }, // Missing day
215 { "@{datestring('2024년')}", "", true }, // Missing month and day
216 };
217
218 for( const auto& testCase : cases )
219 {
220 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
221
222 if( testCase.shouldError )
223 {
224 BOOST_CHECK_MESSAGE( evaluator.HasErrors(),
225 "Expected error but got result: " +
226 result.ToStdString( wxConvUTF8 ) +
227 " for expression: " + testCase.expression );
228 }
229 else
230 {
231 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
232 "Error in expression: " + testCase.expression +
233 " Errors: " + evaluator.GetErrorSummary().ToStdString() );
234 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
235 }
236 }
237}
238
242BOOST_AUTO_TEST_CASE( CurrentDateTime )
243{
244 EXPRESSION_EVALUATOR evaluator;
245
246 auto todayResult = evaluator.Evaluate( "@{today()}" );
247 BOOST_CHECK( !evaluator.HasErrors() );
248
249 // Should return a number (days since epoch)
250 double todayDays = std::stod( todayResult.ToStdString() );
251
252 // Should be a reasonable number of days since 1970
253 // As of 2024, this should be over 19,000 days
254 BOOST_CHECK_GT( todayDays, 19000 );
255 BOOST_CHECK_LT( todayDays, 50000 ); // Reasonable upper bound
256
257 // Test now() function
258 auto nowResult = evaluator.Evaluate( "@{now()}" );
259 BOOST_CHECK( !evaluator.HasErrors() );
260
261 // Should return a timestamp (seconds since epoch)
262 double nowTimestamp = std::stod( nowResult.ToStdString() );
263
264 // Should be a reasonable timestamp
265 auto currentTime = std::chrono::system_clock::now();
266 auto currentTimestamp = std::chrono::system_clock::to_time_t( currentTime );
267 double currentTimestampDouble = static_cast<double>( currentTimestamp );
268
269 // Should be within a few seconds of current time
270 BOOST_CHECK_CLOSE( nowTimestamp, currentTimestampDouble, 1.0 ); // Within 1%
271
272 // Test that consecutive calls to today() return the same value
273 auto todayResult2 = evaluator.Evaluate( "@{today()}" );
274 BOOST_CHECK_EQUAL( todayResult, todayResult2 );
275
276 // Test formatting current date
277 auto formattedToday = evaluator.Evaluate( "@{dateformat(today(), \"ISO\")}" );
278 BOOST_CHECK( !evaluator.HasErrors() );
279
280 // Should be in ISO format: YYYY-MM-DD
281 std::regex isoDateRegex( R"(\d{4}-\d{2}-\d{2})" );
282 BOOST_CHECK( std::regex_match( formattedToday.ToStdString(), isoDateRegex ) );
283}
284
288BOOST_AUTO_TEST_CASE( DateArithmetic )
289{
290 EXPRESSION_EVALUATOR evaluator;
291
292 struct TestCase {
293 std::string expression;
294 std::string expected;
295 bool shouldError;
296 };
297
298 const std::vector<TestCase> cases = {
299 // Date arithmetic
300 { "@{dateformat(0 + 1)}", "1970-01-02", false }, // Add one day
301 { "@{dateformat(0 + 7)}", "1970-01-08", false }, // Add one week
302 { "@{dateformat(0 + 30)}", "1970-01-31", false }, // Add 30 days
303 { "@{dateformat(0 + 365)}", "1971-01-01", false }, // Add one year (1970 was not leap)
304
305 // Leap year test
306 { "@{dateformat(365 + 365 + 366)}", "1973-01-01", false }, // 1972 was leap year
307
308 // Date differences
309 { "@{365 - 0}", "365", false }, // Days between dates
310
311 // Complex date expressions
312 { "@{weekdayname(today())}", "", false }, // Should return a weekday name (we can't predict which)
313 };
314
315 for( const auto& testCase : cases )
316 {
317 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
318
319 if( testCase.shouldError )
320 {
321 BOOST_CHECK( evaluator.HasErrors() );
322 }
323 else
324 {
325 BOOST_CHECK( !evaluator.HasErrors() );
326
327 if( !testCase.expected.empty() )
328 {
329 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
330 }
331 else
332 {
333 // For dynamic results like weekday names, just check it's not empty
334 BOOST_CHECK( !result.empty() );
335 }
336 }
337 }
338}
339
343BOOST_AUTO_TEST_CASE( DateEdgeCases )
344{
345 EXPRESSION_EVALUATOR evaluator;
346
347 struct TestCase {
348 std::string expression;
349 std::string expected;
350 bool shouldError;
351 };
352
353 const std::vector<TestCase> cases = {
354 // Leap year boundaries
355 { "@{dateformat(365 + 365 + 59)}", "1972-02-29", false }, // Feb 29, 1972 (leap year)
356 { "@{dateformat(365 + 365 + 60)}", "1972-03-01", false }, // Mar 1, 1972
357
358 // Year boundaries
359 { "@{dateformat(365 - 1)}", "1970-12-31", false }, // Last day of 1970
360 { "@{dateformat(365)}", "1971-01-01", false }, // First day of 1971
361
362 // Month boundaries
363 { "@{dateformat(30)}", "1970-01-31", false }, // Last day of January
364 { "@{dateformat(31)}", "1970-02-01", false }, // First day of February
365 { "@{dateformat(58)}", "1970-02-28", false }, // Last day of February 1970 (not leap)
366 { "@{dateformat(59)}", "1970-03-01", false }, // First day of March 1970
367
368 // Large date values
369 { "@{dateformat(36525)}", "2070-01-01", false }, // 100 years after epoch
370
371 // Negative dates (before epoch)
372 { "@{dateformat(-365)}", "1969-01-01", false }, // One year before epoch
373 { "@{dateformat(-1)}", "1969-12-31", false }, // One day before epoch
374
375 // Weekday wrap-around
376 { "@{weekdayname(-1)}", "Wednesday", false }, // Day before Thursday
377 { "@{weekdayname(-7)}", "Thursday", false }, // One week before
378
379 // Edge case: very large weekday values
380 { "@{weekdayname(7000)}", "Thursday", false }, // Should still work
381 };
382
383 for( const auto& testCase : cases )
384 {
385 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
386
387 if( testCase.shouldError )
388 {
389 BOOST_CHECK( evaluator.HasErrors() );
390 }
391 else
392 {
393 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
394 "Error in expression: " + testCase.expression );
395 BOOST_CHECK_EQUAL( result.ToStdString( wxConvUTF8 ), testCase.expected );
396 }
397 }
398}
399
403BOOST_AUTO_TEST_CASE( DateFormattingMixed )
404{
405 EXPRESSION_EVALUATOR evaluator;
406 evaluator.SetVariable( "days_offset", 100.0 );
407
408 struct TestCase {
409 std::string expression;
410 bool shouldWork;
411 };
412
413 const std::vector<TestCase> cases = {
414 // Complex expressions combining dates and variables
415 { "Today is @{dateformat(today())} which is @{weekdayname(today())}", true },
416 { "Date: @{dateformat(0 + ${days_offset}, \"long\")}", true },
417 { "In @{format(${days_offset})} days: @{dateformat(today() + ${days_offset})}", true },
418
419 // Nested function calls
420 { "@{upper(weekdayname(today()))}", true },
421 { "@{lower(dateformat(today(), \"long\"))}", true },
422
423 // Multiple date calculations
424 { "Start: @{dateformat(0)} End: @{dateformat(365)} Duration: @{365 - 0} days", true },
425 };
426
427 for( const auto& testCase : cases )
428 {
429 auto result = evaluator.Evaluate( wxString::FromUTF8( testCase.expression ) );
430
431 if( testCase.shouldWork )
432 {
433 BOOST_CHECK_MESSAGE( !evaluator.HasErrors(),
434 "Error in expression: " + testCase.expression +
435 " Result: " + result.ToStdString( wxConvUTF8 ) );
436 BOOST_CHECK( !result.empty() );
437 }
438 else
439 {
440 BOOST_CHECK( evaluator.HasErrors() );
441 }
442 }
443}
444
448BOOST_AUTO_TEST_CASE( DatePerformance )
449{
450 EXPRESSION_EVALUATOR evaluator;
451
452 // Test that date operations are reasonably fast
453 auto start = std::chrono::high_resolution_clock::now();
454
455 std::set<int> errors;
456
457 // Perform many date operations
458 for( int i = 0; i < 1000; ++i )
459 {
460 auto result = evaluator.Evaluate( "@{dateformat(" + std::to_string(i) + ")}" );
461
462 if( evaluator.HasErrors() )
463 errors.insert( i );
464 }
465
466 BOOST_REQUIRE_MESSAGE( errors.empty(), fmt::format( "Evaluation of operations had errors: {}", fmt::join( errors, ", " ) ) );
467
468 auto end = std::chrono::high_resolution_clock::now();
469 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start );
470
471 // Sanitizer instrumentation changes execution cost, not the evaluation contract.
472#if defined( KICAD_SANITIZE_THREADS ) || defined( KICAD_SANITIZE_ADDRESS )
473 BOOST_TEST_MESSAGE( "Instrumented date evaluation: " << duration.count() << " ms" );
474#else
475 BOOST_CHECK_LT( duration.count(), 100 );
476#endif
477}
478
High-level wrapper for evaluating mathematical and string expressions in wxString format.
wxString Evaluate(const wxString &aInput)
Main evaluation function - processes input string and evaluates all} expressions.
bool HasErrors() const
Check if the last evaluation had errors.
wxString GetErrorSummary() const
Get detailed error information from the last evaluation.
void SetVariable(const wxString &aName, double aValue)
Set a numeric variable for use in expressions.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
VECTOR2I end
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
BOOST_AUTO_TEST_CASE(DateFormatting)