From c92633976c424aac0f79f73901ad10bc642b609c Mon Sep 17 00:00:00 2001 From: Marko Ristin Date: Wed, 6 Mar 2024 19:07:44 +0100 Subject: [PATCH] Fix C++ code to deal with date and date-times We fix the generated code and the test snippets so that we pass the related verification tests in the C++ SDK. --- .../cpp/verification/_generate.py | 2 + .../expected_output/verification.cpp | 133 +++++++++++++++--- .../expected_output/verification.hpp | 21 +-- .../snippets/verification/is_xs_date_time.cpp | 2 +- .../verification/is_xs_date_time_UTC.cpp | 77 +++++++--- .../verification/is_xs_date_time_UTC.hpp | 10 -- .../value_consistent_with_XSD_type.cpp | 52 +++++++ .../value_consistent_with_XSD_type.hpp | 11 ++ 8 files changed, 242 insertions(+), 66 deletions(-) diff --git a/aas_core_codegen/cpp/verification/_generate.py b/aas_core_codegen/cpp/verification/_generate.py index 801fcf194..1522f84be 100644 --- a/aas_core_codegen/cpp/verification/_generate.py +++ b/aas_core_codegen/cpp/verification/_generate.py @@ -3270,6 +3270,8 @@ def generate_implementation( cpp_common.WARNING, Stripped( f"""\ +#include "./BigInt.hpp" +#include "{include_prefix_path}/common.hpp" #include "{include_prefix_path}/constants.hpp" #include "{include_prefix_path}/verification.hpp" diff --git a/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.cpp b/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.cpp index 543987cb0..646659d66 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.cpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.cpp @@ -1,6 +1,8 @@ // This code has been automatically generated by aas-core-codegen. // Do NOT edit or append. +#include "./BigInt.hpp" +#include "aas_core/aas_3_0/common.hpp" #include "aas_core/aas_3_0/constants.hpp" #include "aas_core/aas_3_0/verification.hpp" @@ -220,9 +222,48 @@ bool MatchesXsDateTimeUtc( } const std::wregex kRegexDatePrefix( - L"^(-?[0-9]+)-(0[1-9]|11|12)-(0[0-9]|1[0-9]|2[0-9]|30|31)" + L"^(-?[0-9]+)-(0[1-9]|1[0-2])-(0[0-9]|1[0-9]|2[0-9]|30|31)" ); +template< + typename T, + std::enable_if< + std::is_integral::value + || std::is_same::value + >* = nullptr +> +bool IsLeapYear(T year) { + // NOTE (mristin): + // We consider the years B.C. to be one-off. + // See the note at: https://www.w3.org/TR/xmlschema-2/#dateTime: + // "'-0001' is the lexical representation of the year 1 Before Common Era + // (1 BCE, sometimes written "1 BC")." + // + // Hence, -1 year in XML is 1 BCE, which is 0 year in astronomical years. + if (year < 0) { + year = -year - 1; + } + + // See: See: https://en.wikipedia.org/wiki/Leap_year#Algorithm + if (year % 4 > 0) + { + return false; + } + + if (year % 100 > 0) + { + return true; + } + + if (year % 400 > 0) + { + return false; + } + + return true; +} + + bool IsLeapYear(long long year) { // NOTE (mristin): // We consider the years B.C. to be one-off. @@ -272,7 +313,7 @@ const std::map kDaysInMonth = { }; /** - * \brief Check that \p value is a valid `xs:date`. + * \brief Check that \p value is a valid `xs:date` without the offset. * * Year 1 BCE is the last leap BCE year. * See: https://www.w3.org/TR/xmlschema-2/#dateTime. @@ -280,9 +321,7 @@ const std::map kDaysInMonth = { * \param value to be checked * \return true if \p value is a valid `xs:date` */ -bool IsXsDate( - const std::wstring& text -) { +bool IsXsDateWithoutOffset(const std::wstring& text) { // NOTE (mristin): // We can not use date functions from the operation system as they do not // handle years BCE (*e.g.*, `-0003-01-02`). @@ -301,10 +340,14 @@ bool IsXsDate( // difficult. Hence, we sacrifice the efficiency a bit for the clearer code & code // generation. - long long year; + bool is_zero_year; + bool is_leap_year; try { - year = std::stoll(match[1].str()); + const long long year = std::stoll(match[1].str()); + + is_zero_year = year == 0; + is_leap_year = IsLeapYear(year); } catch (const std::invalid_argument&) { std::wstringstream wss; wss @@ -315,19 +358,11 @@ bool IsXsDate( common::WstringToUtf8(wss.str()) ); } catch (const std::out_of_range&) { - std::wstringstream wss; - wss - << "The year is out of range for long long integers: " - << match[1].str() - << ( - "; we at aas-core-works planned to include handling of BigInt years " - "in the SDK, but eventually lacked the time for it. Please let the developers " - "know that you need this feature." - ); - - throw std::out_of_range( - common::WstringToUtf8(wss.str()) + const BigInt year( + common::WstringToUtf8(match[1].str()) ); + is_zero_year = year == 0; + is_leap_year = IsLeapYear(std::move(year)); } const int month = std::stoi(match[2].str()); @@ -336,7 +371,7 @@ bool IsXsDate( // NOTE (mristin): // We do not accept year zero, see the note at: // https://www.w3.org/TR/xmlschema-2/#dateTime - if (year == 0) { + if (is_zero_year) { return false; } @@ -350,7 +385,7 @@ bool IsXsDate( const int max_days( (month == 2) - ? (IsLeapYear(year) ? 29 : 28) + ? (is_leap_year ? 29 : 28) : kDaysInMonth.at(month) ); @@ -386,7 +421,7 @@ bool IsXsDateTimeUtc( // should be used here. std::wstring date = text.substr(0, pos); - return IsXsDate(date); + return IsXsDateWithoutOffset(date); } std::wregex ConstructMatchesMimeType() { @@ -1610,7 +1645,7 @@ bool IsXsDateTime( // should be used here. std::wstring date = text.substr(0, pos); - return IsXsDate(date); + return IsXsDateWithoutOffset(date); } std::wregex ConstructMatchesXsDecimal() { @@ -2239,6 +2274,58 @@ bool MatchesXsString( ); } +bool IsXsDate(const std::wstring& text) { + std::wsmatch match; + const bool matched = std::regex_match(text, match, kRegexMatchesXsDate); + + if (!matched) { + return false; + } + + size_t cursor = 0; + if (text[0] == L'-') { + cursor = 1; + } + + while (std::isdigit(text[cursor])) { + ++cursor; + } + + if (text[cursor] != L'-') { + throw std::logic_error( + common::Concat( + "Expected a dash after a year, but got the date text: ", + common::WstringToUtf8(text) + ) + ); + } + ++cursor; + + while (std::isdigit(text[cursor])) { + ++cursor; + } + + if (text[cursor] != L'-') { + throw std::logic_error( + common::Concat( + "Expected a dash after a month, but got the date text: ", + common::WstringToUtf8(text) + ) + ); + } + ++cursor; + + while (std::isdigit(text[cursor])) { + ++cursor; + } + + const std::wstring date_without_offset( + text.substr(0, cursor) + ); + + return IsXsDateWithoutOffset(date_without_offset); +} + bool IsXsDouble(const std::wstring& value) { // NOTE (mristin): // We need to check explicitly for the regular expression since diff --git a/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.hpp b/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.hpp index a9e2d8925..63f7b43d3 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.hpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/expected_output/verification.hpp @@ -240,16 +240,6 @@ bool MatchesXsDateTimeUtc( const std::wstring& text ); -/** - * \brief Check whether the given \p year is a leap year. - * - * Year 1 BCE is a leap year. - * - * \param year to be checked - * \return true if \p year is a leap year - */ -bool IsLeapYear(long long year); - /** * \brief Check that \p text is a `xs:dateTime` with time zone set to UTC. * @@ -561,6 +551,17 @@ bool ValueConsistentWithXsdType( types::DataTypeDefXsd value_type ); +/** + * \brief Check that \p value is a valid `xs:date`. + * + * Year 1 BCE is the last leap BCE year. + * See: https://www.w3.org/TR/xmlschema-2/#dateTime. + * + * \param value to be checked + * \return true if \p value is a valid `xs:date` + */ +bool IsXsDate(const std::wstring& text); + /** * \brief Check that \p value is a valid `xs:double`. * diff --git a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time.cpp b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time.cpp index 1d8d0d878..2e2a6444b 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time.cpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time.cpp @@ -23,5 +23,5 @@ bool IsXsDateTime( // should be used here. std::wstring date = text.substr(0, pos); - return IsXsDate(date); + return IsXsDateWithoutOffset(date); } diff --git a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.cpp b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.cpp index 46ccf5ff2..0bb05d0d6 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.cpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.cpp @@ -1,7 +1,46 @@ const std::wregex kRegexDatePrefix( - L"^(-?[0-9]+)-(0[1-9]|11|12)-(0[0-9]|1[0-9]|2[0-9]|30|31)" + L"^(-?[0-9]+)-(0[1-9]|1[0-2])-(0[0-9]|1[0-9]|2[0-9]|30|31)" ); +template< + typename T, + std::enable_if< + std::is_integral::value + || std::is_same::value + >* = nullptr +> +bool IsLeapYear(T year) { + // NOTE (mristin): + // We consider the years B.C. to be one-off. + // See the note at: https://www.w3.org/TR/xmlschema-2/#dateTime: + // "'-0001' is the lexical representation of the year 1 Before Common Era + // (1 BCE, sometimes written "1 BC")." + // + // Hence, -1 year in XML is 1 BCE, which is 0 year in astronomical years. + if (year < 0) { + year = -year - 1; + } + + // See: See: https://en.wikipedia.org/wiki/Leap_year#Algorithm + if (year % 4 > 0) + { + return false; + } + + if (year % 100 > 0) + { + return true; + } + + if (year % 400 > 0) + { + return false; + } + + return true; +} + + bool IsLeapYear(long long year) { // NOTE (mristin): // We consider the years B.C. to be one-off. @@ -51,7 +90,7 @@ const std::map kDaysInMonth = { }; /** - * \brief Check that \p value is a valid `xs:date`. + * \brief Check that \p value is a valid `xs:date` without the offset. * * Year 1 BCE is the last leap BCE year. * See: https://www.w3.org/TR/xmlschema-2/#dateTime. @@ -59,9 +98,7 @@ const std::map kDaysInMonth = { * \param value to be checked * \return true if \p value is a valid `xs:date` */ -bool IsXsDate( - const std::wstring& text -) { +bool IsXsDateWithoutOffset(const std::wstring& text) { // NOTE (mristin): // We can not use date functions from the operation system as they do not // handle years BCE (*e.g.*, `-0003-01-02`). @@ -80,10 +117,14 @@ bool IsXsDate( // difficult. Hence, we sacrifice the efficiency a bit for the clearer code & code // generation. - long long year; + bool is_zero_year; + bool is_leap_year; try { - year = std::stoll(match[1].str()); + const long long year = std::stoll(match[1].str()); + + is_zero_year = year == 0; + is_leap_year = IsLeapYear(year); } catch (const std::invalid_argument&) { std::wstringstream wss; wss @@ -94,19 +135,11 @@ bool IsXsDate( common::WstringToUtf8(wss.str()) ); } catch (const std::out_of_range&) { - std::wstringstream wss; - wss - << "The year is out of range for long long integers: " - << match[1].str() - << ( - "; we at aas-core-works planned to include handling of BigInt years " - "in the SDK, but eventually lacked the time for it. Please let the developers " - "know that you need this feature." - ); - - throw std::out_of_range( - common::WstringToUtf8(wss.str()) + const BigInt year( + common::WstringToUtf8(match[1].str()) ); + is_zero_year = year == 0; + is_leap_year = IsLeapYear(std::move(year)); } const int month = std::stoi(match[2].str()); @@ -115,7 +148,7 @@ bool IsXsDate( // NOTE (mristin): // We do not accept year zero, see the note at: // https://www.w3.org/TR/xmlschema-2/#dateTime - if (year == 0) { + if (is_zero_year) { return false; } @@ -129,7 +162,7 @@ bool IsXsDate( const int max_days( (month == 2) - ? (IsLeapYear(year) ? 29 : 28) + ? (is_leap_year ? 29 : 28) : kDaysInMonth.at(month) ); @@ -165,5 +198,5 @@ bool IsXsDateTimeUtc( // should be used here. std::wstring date = text.substr(0, pos); - return IsXsDate(date); + return IsXsDateWithoutOffset(date); } diff --git a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.hpp b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.hpp index cba54a54f..690e9fe3f 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.hpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/is_xs_date_time_UTC.hpp @@ -1,13 +1,3 @@ -/** - * \brief Check whether the given \p year is a leap year. - * - * Year 1 BCE is a leap year. - * - * \param year to be checked - * \return true if \p year is a leap year - */ -bool IsLeapYear(long long year); - /** * \brief Check that \p text is a `xs:dateTime` with time zone set to UTC. * diff --git a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.cpp b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.cpp index 4a5789abf..58e74d31f 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.cpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.cpp @@ -1,3 +1,55 @@ +bool IsXsDate(const std::wstring& text) { + std::wsmatch match; + const bool matched = std::regex_match(text, match, kRegexMatchesXsDate); + + if (!matched) { + return false; + } + + size_t cursor = 0; + if (text[0] == L'-') { + cursor = 1; + } + + while (std::isdigit(text[cursor])) { + ++cursor; + } + + if (text[cursor] != L'-') { + throw std::logic_error( + common::Concat( + "Expected a dash after a year, but got the date text: ", + common::WstringToUtf8(text) + ) + ); + } + ++cursor; + + while (std::isdigit(text[cursor])) { + ++cursor; + } + + if (text[cursor] != L'-') { + throw std::logic_error( + common::Concat( + "Expected a dash after a month, but got the date text: ", + common::WstringToUtf8(text) + ) + ); + } + ++cursor; + + while (std::isdigit(text[cursor])) { + ++cursor; + } + + const std::wstring date_without_offset( + text.substr(0, cursor) + ); + + return IsXsDateWithoutOffset(date_without_offset); +} + bool IsXsDouble(const std::wstring& value) { // NOTE (mristin): // We need to check explicitly for the regular expression since diff --git a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.hpp b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.hpp index a184f0329..1481b77e3 100644 --- a/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.hpp +++ b/test_data/cpp/test_main/aas_core_meta.v3/input/snippets/verification/value_consistent_with_XSD_type.hpp @@ -4,6 +4,17 @@ bool ValueConsistentWithXsdType( types::DataTypeDefXsd value_type ); +/** + * \brief Check that \p value is a valid `xs:date`. + * + * Year 1 BCE is the last leap BCE year. + * See: https://www.w3.org/TR/xmlschema-2/#dateTime. + * + * \param value to be checked + * \return true if \p value is a valid `xs:date` + */ +bool IsXsDate(const std::wstring& text); + /** * \brief Check that \p value is a valid `xs:double`. *