Skip to content

Commit e8d050c

Browse files
authored
Fix handlig of leap years in C++ (#452)
We remove the dependency on BigInt library in C++ as it suffices to crop a year at the last four digits to determine whether it is a leap year or not. This is much faster and needs much less maintenance.
1 parent e90f027 commit e8d050c

File tree

3 files changed

+152
-172
lines changed

3 files changed

+152
-172
lines changed

aas_core_codegen/cpp/verification/_generate.py

-1
Original file line numberDiff line numberDiff line change
@@ -3270,7 +3270,6 @@ def generate_implementation(
32703270
cpp_common.WARNING,
32713271
Stripped(
32723272
f"""\
3273-
#include "./BigInt.hpp"
32743273
#include "{include_prefix_path}/common.hpp"
32753274
#include "{include_prefix_path}/constants.hpp"
32763275
#include "{include_prefix_path}/verification.hpp"

test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.cpp

+76-86
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// This code has been automatically generated by aas-core-codegen.
22
// Do NOT edit or append.
33

4-
#include "./BigInt.hpp"
54
#include "aas_core/aas_3_0/common.hpp"
65
#include "aas_core/aas_3_0/constants.hpp"
76
#include "aas_core/aas_3_0/verification.hpp"
@@ -225,74 +224,44 @@ const std::wregex kRegexDatePrefix(
225224
L"^(-?[0-9]+)-(0[1-9]|1[0-2])-(0[0-9]|1[0-9]|2[0-9]|30|31)"
226225
);
227226

228-
template<
229-
typename T,
230-
std::enable_if<
231-
std::is_integral<T>::value
232-
|| std::is_same<T, BigInt>::value
233-
>* = nullptr
234-
>
235-
bool IsLeapYear(T year) {
236-
// NOTE (mristin):
237-
// We consider the years B.C. to be one-off.
238-
// See the note at: https://www.w3.org/TR/xmlschema-2/#dateTime:
239-
// "'-0001' is the lexical representation of the year 1 Before Common Era
240-
// (1 BCE, sometimes written "1 BC")."
241-
//
242-
// Hence, -1 year in XML is 1 BCE, which is 0 year in astronomical years.
243-
if (year < 0) {
244-
year = -year - 1;
245-
}
246-
247-
// See: See: https://en.wikipedia.org/wiki/Leap_year#Algorithm
248-
if (year % 4 > 0)
249-
{
250-
return false;
251-
}
252-
253-
if (year % 100 > 0)
254-
{
255-
return true;
256-
}
257-
258-
if (year % 400 > 0)
259-
{
260-
return false;
227+
/**
228+
* Determine the sign of the given year as text.
229+
*
230+
* @param year_str year as text
231+
* @return -1, 0 or 1; -1 means BC, 1 means AD. 0 means a zero year,
232+
* even if specified as -0.
233+
*/
234+
int DetermineEra(const std::wstring& year_str) {
235+
#ifdef DEBUG
236+
if (year_str.empty()) {
237+
throw std::invalid_argument(
238+
"Expected a valid year string, but got an empty string"
239+
);
261240
}
241+
#endif
262242

263-
return true;
264-
}
265-
243+
const int sign = (year_str[0] == L'-') ? -1 : 1;
266244

267-
bool IsLeapYear(long long year) {
268-
// NOTE (mristin):
269-
// We consider the years B.C. to be one-off.
270-
// See the note at: https://www.w3.org/TR/xmlschema-2/#dateTime:
271-
// "'-0001' is the lexical representation of the year 1 Before Common Era
272-
// (1 BCE, sometimes written "1 BC")."
273-
//
274-
// Hence, -1 year in XML is 1 BCE, which is 0 year in astronomical years.
275-
if (year < 0) {
276-
year = -year - 1;
277-
}
278-
279-
// See: See: https://en.wikipedia.org/wiki/Leap_year#Algorithm
280-
if (year % 4 > 0)
281-
{
282-
return false;
245+
size_t cursor = 0;
246+
if (sign < 0) {
247+
// NOTE (mristin):
248+
// We skip the minus sign as prefix, including the edge case "-0".
249+
++cursor;
283250
}
284251

285-
if (year % 100 > 0)
286-
{
287-
return true;
252+
bool is_zero = true;
253+
for (; cursor < year_str.size(); ++cursor) {
254+
if (year_str[cursor] != L'0') {
255+
is_zero = false;
256+
break;
257+
}
288258
}
289259

290-
if (year % 400 > 0)
291-
{
292-
return false;
260+
if (is_zero) {
261+
return 0;
293262
}
294263

295-
return true;
264+
return sign;
296265
}
297266

298267
const std::map<int, int> kDaysInMonth = {
@@ -340,41 +309,62 @@ bool IsXsDateWithoutOffset(const std::wstring& text) {
340309
// difficult. Hence, we sacrifice the efficiency a bit for the clearer code & code
341310
// generation.
342311

343-
bool is_zero_year;
344-
bool is_leap_year;
345-
346-
try {
347-
const long long year = std::stoll(match[1].str());
348-
349-
is_zero_year = year == 0;
350-
is_leap_year = IsLeapYear<long long>(year);
351-
} catch (const std::invalid_argument&) {
352-
std::wstringstream wss;
353-
wss
354-
<< "The year matched the regex, but could not be parsed as integer: "
355-
<< match[1].str();
312+
// NOTE (mristin):
313+
// The year can be arbitrarily large in `xs:date` and `xs:dateTime`. Instead of
314+
// using a BigInt implementation -- and having to maintain another dependency --
315+
// we simply clip the year to the last four relevant digits for the computation of
316+
// leap years.
356317

357-
throw std::logic_error(
358-
common::WstringToUtf8(wss.str())
359-
);
360-
} catch (const std::out_of_range&) {
361-
const BigInt year(
362-
common::WstringToUtf8(match[1].str())
363-
);
364-
is_zero_year = year == 0;
365-
is_leap_year = IsLeapYear<BigInt>(std::move(year));
366-
}
318+
const std::wstring year_str = match[1].str();
367319

368-
const int month = std::stoi(match[2].str());
369-
const int day = std::stoi(match[3].str());
320+
const int era = DetermineEra(year_str);
370321

371322
// NOTE (mristin):
372323
// We do not accept year zero, see the note at:
373324
// https://www.w3.org/TR/xmlschema-2/#dateTime
374-
if (is_zero_year) {
325+
if (era == 0) {
375326
return false;
376327
}
377328

329+
std::wstring last_four_year_digits;
330+
331+
const size_t year_start = (era < 0) ? 1 : 0;
332+
const size_t end = year_str.size();
333+
size_t start = end - 4;
334+
if (start < year_start) {
335+
start = year_start;
336+
}
337+
338+
const std::wstring at_most_last_four_year_digits(
339+
year_str.substr(start, 4)
340+
);
341+
342+
int year_suffix = era * std::stoi(at_most_last_four_year_digits);
343+
344+
// NOTE (mristin):
345+
// We consider the years B.C. to be one-off.
346+
// See the note at: https://www.w3.org/TR/xmlschema-2/#dateTime:
347+
// "'-0001' is the lexical representation of the year 1 Before Common Era
348+
// (1 BCE, sometimes written "1 BC")."
349+
//
350+
// Hence, -1 year in XML is 1 BCE, which is 0 year in astronomical years.
351+
if (year_suffix < 0) {
352+
year_suffix = -year_suffix - 1;
353+
}
354+
355+
bool is_leap_year = true;
356+
357+
if (year_suffix % 4 > 0) {
358+
is_leap_year = false;
359+
} else if (year_suffix % 100 > 0) {
360+
is_leap_year = true;
361+
} else if (year_suffix % 400 > 0) {
362+
is_leap_year = false;
363+
}
364+
365+
const int month = std::stoi(match[2].str());
366+
const int day = std::stoi(match[3].str());
367+
378368
if (day <= 0) {
379369
return false;
380370
}

0 commit comments

Comments
 (0)