diff --git a/.gitattribute b/.gitattribute new file mode 100644 index 0000000..1467fc5 --- /dev/null +++ b/.gitattribute @@ -0,0 +1,2 @@ +*.ll text +*.yy text diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c255e9..430d112 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ option(ENABLE_COVERAGE "enable coverage on debug build" OFF) include(GNUInstallDirs) find_package(mpdecpp REQUIRED) +find_package(FLEX REQUIRED) +find_package(BISON 3.6 REQUIRED) find_package(ICU 60 COMPONENTS diff --git a/README.md b/README.md index 2306b27..c0714a2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ git submodule update --init --recursive ```dockerfile FROM ubuntu:22.04 -RUN apt update -y && apt install -y git build-essential cmake ninja-build libboost-container-dev libboost-stacktrace-dev libicu-dev +RUN apt update -y && apt install -y git build-essential cmake ninja-build libboost-container-dev libboost-stacktrace-dev libicu-dev flex bison ``` optional packages: @@ -40,6 +40,25 @@ make -j4 make install # or sudo make install ``` +#### GNU Bison `>= 3.6` + +This project requires GNU Bison `>= 3.6`. +Please run `bison --version` and check the printed version. + +```sh +# install packages to build bison +sudo apt update -y +sudo apt install -y curl m4 + +curl http://ftp.jaist.ac.jp/pub/GNU/bison/bison-3.6.4.tar.gz | tar zxv +cd bison-3.6.4 +./configure --prefix=/path/to/install +make -j4 +make install # or sudo make install +``` + +If you install the above to a non-standard path, please specify `-DCMAKE_PREFIX_PATH=` to cmake. + ## How to build ```sh diff --git a/include/takatori/datetime/conversion.h b/include/takatori/datetime/conversion.h new file mode 100644 index 0000000..ec3d08e --- /dev/null +++ b/include/takatori/datetime/conversion.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +#include + +#include "conversion_info.h" + +namespace takatori::datetime { + +/** + * @brief represents the result of conversion, or error message. + * @tparam T the type of conversion result + */ +template +using conversion_result = util::either; + +/** + * @brief parses the given contents as a date. + * @details The date format must be `YYYY-MM-DD`. + * @param contents the source contents + * @return the parsed date information + * @return an error message if the conversion was failed + */ +[[nodiscard]] conversion_result parse_date(std::string const& contents); + +/** + * @brief parses the given contents as a time of day. + * @details The date format must be `hh:mm:ss.SSSSSSSSS`. + * @param contents the source contents + * @return the parsed time of day information + * @return an error message if the conversion was failed + */ +[[nodiscard]] conversion_result parse_time(std::string const& contents); + +/** + * @brief parses the given contents as a datetime, with or without zone offset. + * @details The date format must be `YY-MM-DD hh:mm:ss.SSSSSSSSS+HH:MM`. + * @param contents the source contents + * @return the parsed datetime information with or without zone offset + * @return an error message if the conversion was failed + */ +[[nodiscard]] conversion_result parse_datetime(std::string const& contents); + +/** + * @brief parses the given contents as a zone offset. + * @details The date format must be `+HH:MM` or just `Z`. + * @param contents the source contents + * @return the parsed zone offset + * @return an error message if the conversion was failed + */ +[[nodiscard]] conversion_result parse_zone_offset(std::string const& contents); + +} // namespace takatori::datetime diff --git a/include/takatori/datetime/conversion_info.h b/include/takatori/datetime/conversion_info.h new file mode 100644 index 0000000..d03204c --- /dev/null +++ b/include/takatori/datetime/conversion_info.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +#include + +namespace takatori::datetime { + +/** + * @brief represents date information. + */ +struct date_info { + + /// @brief the year field of date. + std::uint32_t year {}; + + /// @brief the month field of date. + std::uint32_t month {}; + + /// @brief the day of month field of date. + std::uint32_t day {}; +}; + +/** + * @brief represents time of day information. + */ +struct time_info { + + /// @brief the sub-second field type. + using subsecond_type = std::chrono::duration; + + /// @brief the hour field of time of day. + std::uint32_t hour {}; + + /// @brief the minute field of time of day. + std::uint32_t minute {}; + + /// @brief the second field of time of day. + std::uint32_t second {}; + + /// @brief the sub-second field of time of day. + subsecond_type subsecond {}; +}; + +/** + * @brief represents timezone offset information. + */ +struct zone_offset_info { + + /// @brief the sign of timezone offset. + bool plus { true }; + + /// @brief the hour field of timezone offset. + std::uint32_t hour {}; + + /// @brief the minute field of timezone offset. + std::uint32_t minute {}; +}; + +/** + * @brief represents datetime information. + */ +struct datetime_info { + + /// @brief the date information. + date_info date; + + /// @brief the time of day information. + time_info time; + + /// @brief the optional timezone offset information. + std::optional offset; +}; + +} // namespace takatori::datetime diff --git a/include/takatori/util/either.h b/include/takatori/util/either.h new file mode 100644 index 0000000..001b206 --- /dev/null +++ b/include/takatori/util/either.h @@ -0,0 +1,275 @@ +#pragma once + +#include +#include +#include +#include + +namespace takatori::util { + +/** + * @brief provides either normal value or erroneous information. + * @tparam Error the erroneous information type + * @tparam Normal the normal value type + */ +template +class either { + + static_assert(!std::is_same_v); + static_assert(!std::is_reference_v); + static_assert(!std::is_reference_v); + +public: + /// @brief normal value type. + using value_type = Normal; + /// @brief the element type of normal value. + using element_type = value_type; + /// @brief the pointer type of normal value. + using pointer = std::add_pointer_t; + /// @brief the const pointer type of normal value. + using const_pointer = std::add_pointer_t>; + /// @brief the L-value reference type of normal value. + using reference = std::add_lvalue_reference_t; + /// @brief the L-value const reference type of normal value. + using const_reference = std::add_lvalue_reference_t>; + /// @brief the R-value reference type of normal value. + using rvalue_reference = std::add_rvalue_reference_t; + + /// @brief erroneous information type. + using error_type = Error; + /// @brief the L-value reference type of erroneous information. + using error_reference = std::add_lvalue_reference_t; + /// @brief the L-value reference type of erroneous information. + using error_const_reference = std::add_lvalue_reference_t>; + /// @brief the R-value reference type of erroneous information. + using error_rvalue_reference = std::add_rvalue_reference_t; + + /** + * @brief constructs a new object. + * @details this is available only if the erroneous value type is default constructible. + */ + either() = default; + + /** + * @brief constructs a new object. + * @param value the value + */ + either(const_reference value) noexcept(std::is_nothrow_copy_constructible_v) // NOLINT + : alternatives_(std::in_place_index, value) + {} + + /** + * @brief assigns the given value. + * @param value the value + * @return this + */ + either& operator=(const_reference value) noexcept(std::is_nothrow_copy_constructible_v) { + alternatives_.template emplace(value); + return *this; + } + + /** + * @brief constructs a new object. + * @param value the value + */ + either(rvalue_reference value) noexcept(std::is_nothrow_move_constructible_v) // NOLINT + : alternatives_(std::in_place_index, std::move(value)) + {} + + /** + * @brief assigns the given value. + * @param value the value + * @return this + */ + either& operator=(rvalue_reference value) noexcept(std::is_nothrow_move_constructible_v) { + alternatives_.template emplace(std::move(value)); + return *this; + } + + /** + * @brief constructs an erroneous object. + * @param error the error information + */ + either(error_const_reference error) noexcept(std::is_nothrow_copy_constructible_v) // NOLINT + : alternatives_(std::in_place_index, error) + {} + + /** + * @brief assigns the given erroneous information. + * @param error the error information + * @return this + */ + either& operator=(error_const_reference error) noexcept(std::is_nothrow_copy_constructible_v) { + alternatives_.template emplace(error); + return *this; + } + + /** + * @brief constructs an erroneous object. + * @param error the error information + */ + either(error_rvalue_reference error) noexcept(std::is_nothrow_move_constructible_v) // NOLINT + : alternatives_(std::in_place_index, std::move(error)) + {} + + /** + * @brief assigns the given erroneous information. + * @param error the error information + * @return this + */ + either& operator=(error_rvalue_reference error) noexcept(std::is_nothrow_move_constructible_v) { + alternatives_.template emplace(std::move(error)); + return *this; + } + + /** + * @brief constructs a new object. + * @tparam T the target type, must be either erroneous information or normal value type + * @tparam Args the parameter types of target type constructor + * @param args the constructor arguments + */ + template + explicit either(std::in_place_type_t, Args&&... args) noexcept(std::is_nothrow_constructible_v) + : alternatives_(std::in_place_type, std::forward(args)...) + {} + + /** + * @brief emplaces a new normal value or erroneous information. + * @tparam T the target type, must be either erroneous information or normal value type + * @tparam Args the parameter types of target type constructor + * @param args the constructor arguments + */ + template + T& emplace(Args&&... args) noexcept(std::is_nothrow_constructible_v) { + return alternatives_.template emplace(std::forward(args)...); + } + + /** + * @brief returns whether or not this object holds a normal value. + * @return true if this object holds a normal value + * @return false otherwise + */ + [[nodiscard]] constexpr bool has_value() const noexcept { return alternatives_.index() == value_index; } + + /// @copydoc has_value() + [[nodiscard]] explicit constexpr operator bool() const noexcept { return has_value(); } + + /** + * @brief returns the holding normal value. + * @return the normal value + * @see has_value() + * @warning undefined behavior if this object does not hold a normal value + */ + [[nodiscard]] reference value() { return std::get(alternatives_); } + + /// @copydoc value() + [[nodiscard]] const_reference value() const { return std::get(alternatives_); } + + /** + * @brief returns a pointer to the holding normal value only if it exists. + * @return pointer to the normal value + * @return nullptr if this does not have normal value + */ + [[nodiscard]] pointer get() noexcept { return std::get_if(&alternatives_); } + + /// @copydoc get() + [[nodiscard]] const_pointer get() const noexcept { return std::get_if(&alternatives_); } + + /// @copydoc value() + [[nodiscard]] reference operator*() { return value(); } + + /// @copydoc value() + [[nodiscard]] const_reference operator*() const { return value(); } + + /// @copydoc get() + [[nodiscard]] pointer operator->() noexcept { return get(); } + + /// @copydoc get() + [[nodiscard]] const_pointer operator->() const noexcept { return get(); } + + /** + * @brief returns whether or not this object holds an erroneous information. + * @return true if this object holds an erroneous information + * @return false otherwise + */ + [[nodiscard]] constexpr bool has_error() const noexcept { return alternatives_.index() == error_index; } + + /// @copydoc has_error() + [[nodiscard]] constexpr bool is_error() const noexcept { return has_error(); } + + /** + * @brief returns the holding erroneous information. + * @return the normal value + * @see has_error() + * @warning undefined behavior if this object does not hold erroneous information + */ + [[nodiscard]] error_reference error() { return std::get(alternatives_); } + + /// @copydoc error() + [[nodiscard]] error_const_reference error() const { return std::get(alternatives_); } + +private: + std::variant alternatives_ { std::in_place_index }; + + static constexpr std::size_t value_index = 1; + static constexpr std::size_t error_index = 0; +}; + +/** + * @brief returns whether or not the each object has equivalent value or error information. + * @tparam E1 the erroneous information type of the first object + * @tparam N1 the normal value type of the first object + * @tparam E2 the erroneous information type of the second object + * @tparam N2 the normal value type of the second object + * @param a the first object + * @param b the second object + * @return true if the both have equivalent value or error information + * @return false otherwise + */ +template +inline bool operator==(either const& a, either const& b) noexcept { + if (a.has_value() && b.has_value()) { + return a.value() == b.value(); + } + if (a.has_error() && b.has_error()) { + return a.error() == b.error(); + } + return false; +} + +/** + * @brief returns whether or not the each object has different value or error information. + * @tparam E1 the erroneous information type of the first object + * @tparam N1 the normal value type of the first object + * @tparam E2 the erroneous information type of the second object + * @tparam N2 the normal value type of the second object + * @param a the first object + * @param b the second object + * @return true if the both have different value or error information + * @return false otherwise + */ +template +inline bool operator!=(either const& a, either const& b) noexcept { + return !(a == b); +} + +/** + * @brief appends a string representation of the given value. + * @tparam E the erroneous information type + * @tparam N the normal value type + * @param out the target output stream + * @param value the target value + * @return the output stream + */ +template +inline std::ostream& operator<<(std::ostream& out, either const& value) { + if (value.has_value()) { + return out << value.value(); + } + if (value.has_error()) { + return out << value.error(); + } + return out << "(broken)"; +} + +} // namespace takatori::util diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 714082a..602fcfc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,37 @@ +set(FLEX_FLAGS_DEBUG "-d") +set(FLEX_FLAGS_RELEASE "-Cfe") +set(FLEX_FLAGS_RELWITHDEBINFO "-Cfe") +set(FLEX_FLAGS_MINSIZEREL "-Ce") + +set(BISON_FLAGS_DEBUG "-Dparse.assert -Dparse.trace") +set(BISON_FLAGS_RELEASE "") +set(BISON_FLAGS_RELWITHDEBINFO "") +set(BISON_FLAGS_MINSIZEREL "") + +string(TOUPPER ${CMAKE_BUILD_TYPE} UPPER_CMAKE_BUILD_TYPE) +set(FLEX_FLAGS "${FLEX_FLAGS_${UPPER_CMAKE_BUILD_TYPE}}") +set(BISON_FLAGS "-Wall -Werror -Wno-error=conflicts-sr -ra ${BISON_FLAGS_${UPPER_CMAKE_BUILD_TYPE}}") + +FILE(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/takatori/datetime/parser) + +FLEX_TARGET(datetime_scanner + takatori/datetime/parser/scanner.ll + ${CMAKE_CURRENT_BINARY_DIR}/takatori/datetime/parser/scanner_generated.cpp + COMPILE_FLAGS "${FLEX_FLAGS}" +) + +BISON_TARGET(datetime_parser + takatori/datetime/parser/parser.yy + ${CMAKE_CURRENT_BINARY_DIR}/takatori/datetime/parser/parser_generated.cpp + COMPILE_FLAGS "${BISON_FLAGS}" + VERBOSE REPORT_FILE ${CMAKE_CURRENT_BINARY_DIR}/takatori/datetime/parser/parser_report.log +) + +ADD_FLEX_BISON_DEPENDENCY( + datetime_scanner + datetime_parser +) + add_library(takatori # name @@ -41,8 +75,17 @@ add_library(takatori takatori/datetime/datetime_interval.cpp takatori/datetime/time_zone.cpp takatori/datetime/time_zone_impl.cpp + takatori/datetime/conversion.cpp takatori/datetime/printing.cpp + # datetime parser + takatori/datetime/parser/region.cpp + takatori/datetime/parser/driver.cpp + takatori/datetime/parser/scanner.cpp + takatori/datetime/parser/parser.cpp + ${FLEX_datetime_scanner_OUTPUTS} + ${BISON_datetime_parser_OUTPUTS} + # scalar takatori/scalar/expression.cpp @@ -182,7 +225,9 @@ add_library(takatori ) target_include_directories(takatori - PRIVATE . + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_BINARY_DIR} + PRIVATE ${FLEX_INCLUDE_DIRS} ) target_link_libraries(takatori diff --git a/src/takatori/datetime/conversion.cpp b/src/takatori/datetime/conversion.cpp new file mode 100644 index 0000000..890638b --- /dev/null +++ b/src/takatori/datetime/conversion.cpp @@ -0,0 +1,158 @@ +#include + +#include + +#include "parser/parser.h" + +namespace takatori::datetime { + +using std::string_literals::operator""s; + +namespace { + +std::optional validate_date(date_info const& info) { + if (info.year < 1) { + return "invalid year field"s; + } + if (info.month < 1 || info.month > 12) { + return "invalid month field"s; + } + if (info.day < 1 || info.day > 31) { + return "invalid day field"s; + } + return {}; +} + +std::optional require_date(std::optional const& info) { + if (!info) { + return "date field is absent"s; + } + return validate_date(*info); +} + +std::optional validate_time(time_info const& info) { + if (info.hour > 23) { + return "invalid hour field"s; + } + if (info.minute > 59) { + return "invalid minute field"s; + } + if (info.second > 59) { + return "invalid second field"s; + } + if (info.subsecond.count() >= 1'000'000'000) { + return "invalid sub-second field"s; + } + return {}; +} + +std::optional require_time(std::optional const& info) { + if (!info) { + return "date field is absent"s; + } + return validate_time(*info); +} + +std::optional validate_offset(zone_offset_info const& info) { + if (info.hour > 23) { + return "invalid offset hour field"s; + } + if (info.minute > 59) { + return "invalid offset minute field"s; + } + return {}; +} + +std::optional require_offset(std::optional const& info) { + if (!info) { + return "zone offset field is absent"s; + } + return validate_offset(*info); +} + +} // namespace + +conversion_result parse_date(std::string const& contents) { + parser::parser p {}; + auto result = p(contents); + if (!result) { + return std::move(result.error().message); + } + auto&& info = result.value(); + if (auto&& error = require_date(info.date)) { + return std::move(*error); + } + if (info.time) { + return "invalid time field is present"s; + } + if (info.offset) { + return "invalid zone offset field is present"s; + } + return *info.date; +} + +conversion_result parse_time(std::string const& contents) { + parser::parser p {}; + auto result = p(contents); + if (!result) { + return std::move(result.error().message); + } + auto&& info = result.value(); + if (info.date) { + return "invalid date field is present"s; + } + if (auto&& error = require_time(info.time)) { + return std::move(*error); + } + if (info.offset) { + return "invalid zone offset field is present"s; + } + return *info.time; +} + +conversion_result parse_datetime(std::string const& contents) { + parser::parser p {}; + auto result = p(contents); + if (!result) { + return std::move(result.error().message); + } + auto&& info = result.value(); + if (auto&& error = require_date(info.date)) { + return std::move(*error); + } + if (info.time) { + if (auto&& error = validate_time(*info.time)) { + return std::move(*error); + } + } else { + info.time.emplace(); + } + // NOTE: we don't care whether the offset is empty + if (info.offset) { + if (auto&& error = validate_offset(*info.offset)) { + return std::move(*error); + } + } + return datetime_info { *info.date, *info.time, info.offset }; +} + +conversion_result parse_zone_offset(std::string const& contents) { + parser::parser p {}; + auto result = p(contents); + if (!result) { + return std::move(result.error().message); + } + auto&& info = result.value(); + if (info.date) { + return "invalid date field is present"s; + } + if (info.time) { + return "invalid time field is present"s; + } + if (auto&& error = require_offset(info.offset)) { + return std::move(*error); + } + return *info.offset; +} + +} // namespace takatori::datetime diff --git a/src/takatori/datetime/parser/driver.cpp b/src/takatori/datetime/parser/driver.cpp new file mode 100644 index 0000000..9b8cb2d --- /dev/null +++ b/src/takatori/datetime/parser/driver.cpp @@ -0,0 +1,73 @@ +#include "driver.h" + +#include + +namespace takatori::datetime::parser { + +void driver::success(success_type result) { + result_ = result; +} + +void driver::error(error_type error) { + result_ = std::move(error); +} + +driver::result_type& driver::result() noexcept { + return result_; +} + +driver::result_type const& driver::result() const noexcept { + return result_; +} + +std::optional driver::parse_integer(std::string const& token) { + std::uint32_t value {}; + auto const* begin = &*token.begin(); + auto const* end = &*token.end(); + auto result = std::from_chars(begin, end, value); + if (result.ptr != end) { + return {}; + } + return value; +} + +std::optional> driver::parse_decimal(std::string const& token) { + auto dotAt = token.find('.'); + if (dotAt == std::string::npos) { + return {}; + } + std::uint32_t integral_part {}; + { + auto const* begin = &*token.begin(); + auto const* end = &*(token.begin() + static_cast(dotAt)); + auto result = std::from_chars(begin, end, integral_part); + if (result.ptr != end) { + return {}; + } + } + std::uint32_t decimal_part {}; + { + const std::size_t max_decimal_part_size = 9; + auto decimal_part_size = token.size() - dotAt - 1; + if (decimal_part_size > max_decimal_part_size) { + return {}; + } + if (decimal_part_size == 0) { // empty decimal part + decimal_part = 0; + } else { + auto const* begin = &*(token.begin() + static_cast(dotAt + 1)); + auto const* end = &*token.end(); + auto result = std::from_chars(begin, end, decimal_part); + if (result.ptr != end) { + return {}; + } + // adjust scale of decimal part + for (std::size_t index = decimal_part_size; index < max_decimal_part_size; ++index) { + decimal_part *= 10; + } + } + } + return std::make_pair(integral_part, decimal_part); +} + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/driver.h b/src/takatori/datetime/parser/driver.h new file mode 100644 index 0000000..1c10430 --- /dev/null +++ b/src/takatori/datetime/parser/driver.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "parser.h" +#include "region.h" +#include "parser_error.h" +#include "parser_result_info.h" + +namespace takatori::datetime::parser { + +class driver { +public: + using location_type = region; + using success_type = parser::success_type; + using error_type = parser::error_type; + using result_type = parser::result_type; + + void success(success_type result); + + void error(error_type error); + + [[nodiscard]] result_type& result() noexcept; + + [[nodiscard]] result_type const& result() const noexcept; + + [[nodiscard]] std::optional parse_integer(std::string const& token); + + [[nodiscard]] std::optional> parse_decimal(std::string const& token); + +private: + result_type result_ {}; +}; + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/parser.cpp b/src/takatori/datetime/parser/parser.cpp new file mode 100644 index 0000000..7e77fae --- /dev/null +++ b/src/takatori/datetime/parser/parser.cpp @@ -0,0 +1,37 @@ +#include "parser.h" + +#include + +#include +#include +#include + +namespace takatori::datetime::parser { + +parser &parser::set_debug(int level) noexcept { + debug_ = level; + return *this; +} + +parser::result_type parser::operator()(std::string const& contents) const { + std::istringstream input { + contents, + }; + driver driver {}; + scanner scanner { input }; + parser_generated parser { scanner, driver }; + +#if YYDEBUG + parser.set_debug_level(static_cast(debug_)); + parser.set_debug_stream(std::cout); +#endif // YYDEBUG + + parser.parse(); + return std::move(driver.result()); +} + +parser::result_type parse(std::string const& contents, parser const& engine) { + return engine(contents); +} + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/parser.h b/src/takatori/datetime/parser/parser.h new file mode 100644 index 0000000..84df9a6 --- /dev/null +++ b/src/takatori/datetime/parser/parser.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include + +#include "parser_error.h" +#include "parser_result_info.h" + +namespace takatori::datetime::parser { + +class parser { +public: + using success_type = parser_result_info; + using error_type = parser_error; + using result_type = ::takatori::util::either; + + parser& set_debug(int level = 1) noexcept; + + [[nodiscard]] result_type operator()(std::string const& contents) const; + +private: + int debug_ {}; +}; + +parser::result_type parse(std::string const& contents, parser const& engine = {}); + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/parser.yy b/src/takatori/datetime/parser/parser.yy new file mode 100644 index 0000000..1041870 --- /dev/null +++ b/src/takatori/datetime/parser/parser.yy @@ -0,0 +1,172 @@ +%skeleton "lalr1.cc" +%require "3.6" + +%defines +%define api.token.constructor true +%define api.value.type variant +%define api.value.automove true +%define api.namespace { takatori::datetime::parser } +%define api.parser.class { parser_generated } +%define api.token.prefix {} +%define parse.error detailed +%define parse.lac full + +%code requires { + + #include + + #include + + #include + #include + + namespace takatori::datetime::parser { + + class scanner; + class driver; + + using integer_type = std::uint32_t; + using decimal_type = std::pair; + using sign_type = bool; + + using datetime_type = parser_result_info; + using date_type = date_info; + using time_type = time_info; + using offset_type = zone_offset_info; + + } // namespace takatori::datetime::parser +} + +%locations +%define api.location.type { ::takatori::datetime::parser::region } + +%code { + #include + #include + + namespace takatori::datetime::parser { + + static parser_generated::symbol_type yylex(scanner& scanner, driver& driver) { + return scanner.next_token(driver); + } + + void parser_generated::error(location_type const& location, std::string const& message) { + driver.error({ location, message }); + } + + } // namespace takatori::datetime::parser +} + +%param { ::takatori::datetime::parser::scanner& scanner } +%param { ::takatori::datetime::parser::driver& driver } + +%token INTEGER "" +%token DECIMAL "" + +%token COLON ":" +%token PLUS "+" +%token MINUS "-" + +%token CHAR_T "T" +%token CHAR_Z "Z" +%token WHITESPACE "" + +%token ERROR "" +%token INVALID_FIELD "" +%token END_OF_FILE 0 "" + +%nterm program + +%nterm datetime +%nterm date +%nterm time +%nterm offset +%nterm > offset_opt +%nterm sign; + +%nterm date_time_separator + +%start program + +%% + +program + : datetime[r] END_OF_FILE + { + driver.success($r); + } + ; + +datetime + : date[d] + { + $$ = datetime_type { $d, {}, {} }; + } + | time[t] offset_opt[o] + { + $$ = datetime_type { {}, $t, $o }; + } + | date[d] date_time_separator time[t] offset_opt[o] + { + $$ = datetime_type { $d, $t, $o }; + } + | offset[o] + { + $$ = datetime_type { {}, {}, $o }; + } + ; + +date + : INTEGER[y] "-" INTEGER[m] "-" INTEGER[d] + { + $$ = date_type { $y, $m, $d }; + } + ; + +time + : INTEGER[h] ":" INTEGER[m] ":" INTEGER[s] + { + $$ = time_type { $h, $m, $s, {} }; + } + | INTEGER[h] ":" INTEGER[m] ":" DECIMAL[s] + { + auto [sec, nano] = $s; + $$ = time_type { $h, $m, sec, time_type::subsecond_type { nano } }; + } + ; + +offset + : sign[s] INTEGER[h] + { + $$ = offset_type { $s, $h, 0 }; + } + | sign[s] INTEGER[h] ":" INTEGER[m] + { + $$ = offset_type { $s, $h, $m }; + } + | "Z" + { + $$ = offset_type {}; + } + ; + +offset_opt + : offset[o] + { + $$ = $o; + } + | %empty + { + $$ = std::nullopt; + } + ; + +sign + : "+" { $$ = true; } + | "-" { $$ = false; } + ; + +date_time_separator + : "T" + | WHITESPACE + ; diff --git a/src/takatori/datetime/parser/parser_error.h b/src/takatori/datetime/parser/parser_error.h new file mode 100644 index 0000000..18a50ac --- /dev/null +++ b/src/takatori/datetime/parser/parser_error.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include "region.h" + +namespace takatori::datetime::parser { + +struct parser_error { + ::takatori::datetime::parser::region region {}; + std::string message {}; +}; + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/parser_result_info.h b/src/takatori/datetime/parser/parser_result_info.h new file mode 100644 index 0000000..0526844 --- /dev/null +++ b/src/takatori/datetime/parser/parser_result_info.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace takatori::datetime::parser { + +/** + * @brief represents datetime information. + */ +struct parser_result_info { + + /// @brief the date information. + std::optional date; + + /// @brief the time of day information. + std::optional time; + + /// @brief the timezone offset information. + std::optional offset; +}; + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/region.cpp b/src/takatori/datetime/parser/region.cpp new file mode 100644 index 0000000..80ebc7b --- /dev/null +++ b/src/takatori/datetime/parser/region.cpp @@ -0,0 +1,12 @@ +#include "region.h" + +namespace takatori::datetime::parser { + +std::ostream& operator<<(std::ostream& out, region value) { + return out + << "[" << value.begin + << "-" << value.end + << "]"; +} + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/region.h b/src/takatori/datetime/parser/region.h new file mode 100644 index 0000000..364617e --- /dev/null +++ b/src/takatori/datetime/parser/region.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +namespace takatori::datetime::parser { + +struct region { + /// @brief the position type. + using position_type = std::size_t; + + /// @brief represents an invalid position. + static constexpr position_type npos = static_cast(-1); + + /// @brief the beginning position (inclusive, 0-origin). + position_type begin { npos }; // NOLINT(misc-non-private-member-variables-in-classes): for parser generator's convention + + /// @brief the ending position (exclusive, 0-origin). + position_type end { npos }; // NOLINT(misc-non-private-member-variables-in-classes): for parser generator's convention + + /** + * @brief creates a new instance with empty region. + */ + constexpr region() = default; + + /** + * @brief creates a new instance. + * @param begin the beginning position (inclusive, 0-origin) + * @param end the ending position (exclusive, 0-origin) + */ + constexpr region(position_type begin, position_type end) noexcept : + begin { begin }, + end { end } + {} + + /** + * @brief returns whether or not this region is valid. + * @return true if the region is valid + * @return false if begin or end is not valid + */ + [[nodiscard]] explicit operator bool() const noexcept { + return begin != npos && end != npos; + } + + /** + * @brief returns the beginning position of this region. + * @return the beginning position (inclusive) + */ + [[nodiscard]] constexpr position_type first() const noexcept { + return begin; + } + + /** + * @brief returns the ending position of this region. + * @return the ending position (exclusive) + */ + [[nodiscard]] constexpr position_type last() const noexcept { + return end; + } + + /** + * @brief returns the size of this region. + * @return the number of position in this region + */ + [[nodiscard]] constexpr position_type size() const noexcept { + return end - begin; + } +}; + +/** + * @brief returns the union of the two regions. + * @details The regions need not have intersections. + * @param a the first region + * @param b the second region + * @return the union of the regions + */ +[[nodiscard]] region operator|(region a, region b) noexcept; + +/** + * @brief returns whether or not the two regions are equivalent. + * @param a the first region + * @param b the second region + * @return true if the both are equivalent + * @return false otherwise + */ +[[nodiscard]] bool operator==(region a, region b) noexcept; + +/** + * @brief returns whether or not the two regions are different. + * @param a the first region + * @param b the second region + * @return true if the both are different + * @return false otherwise + */ +[[nodiscard]] bool operator!=(region a, region b) noexcept; + +/** + * @brief appends string representation of the given value. + * @param out the target output + * @param value the target value + * @return the output + */ +std::ostream& operator<<(std::ostream& out, region value); + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/scanner.cpp b/src/takatori/datetime/parser/scanner.cpp new file mode 100644 index 0000000..74d2ec7 --- /dev/null +++ b/src/takatori/datetime/parser/scanner.cpp @@ -0,0 +1,31 @@ +#include "scanner.h" + +namespace takatori::datetime::parser { + +scanner::scanner(std::istream& input) : + super { std::addressof(input) } +{} + +void scanner::LexerError(const char *msg) { + // FIXME: impl + super::LexerError(msg); +} + +void scanner::user_action() noexcept { + cursor_ += yyleng; +} + +scanner::location_type scanner::location(bool eof) noexcept { + return { cursor_ - (eof ? 0 : yyleng), cursor_ }; +} + +std::string scanner::get_image(driver const&) { + std::string image { + yytext, + static_cast(yyleng), + }; + return image; +} + + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/scanner.h b/src/takatori/datetime/parser/scanner.h new file mode 100644 index 0000000..e4783fe --- /dev/null +++ b/src/takatori/datetime/parser/scanner.h @@ -0,0 +1,51 @@ +#pragma once + +// manually include flex system header +#if !defined(FLEX_SCANNER) + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage): Flex framework matter +#define yyFlexLexer _generated_takatori__datetime__parser__scanner_FlexLexer +#include +#undef yyFlexLexer +#undef yyFlexLexerOnce + +#endif // !defined(FLEX_SCANNER) + +#include + +#include +#include + +namespace takatori::datetime::parser { + +class scanner : _generated_takatori__datetime__parser__scanner_FlexLexer { +private: + using super = _generated_takatori__datetime__parser__scanner_FlexLexer; + +public: + using parser_type = ::takatori::datetime::parser::parser_generated; + using driver_type = ::takatori::datetime::parser::driver; + using value_type = typename parser_type::symbol_type; + using location_type = driver_type::location_type; + + explicit scanner(std::istream& input); + + // will be generated by flex + [[nodiscard]] value_type next_token(driver_type& driver); + +protected: + void LexerError(char const* msg) override; + +private: + std::size_t npos = static_cast(-1); + + std::size_t cursor_ {}; + std::size_t comment_begin_ { npos }; + + void user_action() noexcept; + [[nodiscard]] location_type location(bool eof = false) noexcept; + + std::string get_image(driver_type const& driver); +}; + +} // namespace takatori::datetime::parser diff --git a/src/takatori/datetime/parser/scanner.ll b/src/takatori/datetime/parser/scanner.ll new file mode 100644 index 0000000..0a23404 --- /dev/null +++ b/src/takatori/datetime/parser/scanner.ll @@ -0,0 +1,73 @@ +%{ +#include +#include + +#define YY_USER_ACTION user_action(); + +#define yyterminate() return parser_type::make_ERROR(location()) + +#undef YY_DECL +#define YY_DECL ::takatori::datetime::parser::scanner::value_type takatori::datetime::parser::scanner::next_token(::takatori::datetime::parser::driver& driver) +%} + +%option noyywrap +%option c++ +%option prefix="_generated_takatori__datetime__parser__scanner_" +%option yyclass="::takatori::datetime::parser::scanner" + +digit [0-9] +dot "." + +integer {digit}+ +decimal {integer}{dot}{integer} + +/* error handling */ +ASCII [\x00-\x7f] +UTF8_2 [\xc2-\xdf] +UTF8_3 [\xe0-\xef] +UTF8_4 [\xf0-\xf4] +U [\x80-\xbf] + +UTF8_CHAR {ASCII}|{UTF8_2}{U}|{UTF8_3}{U}{U}|{UTF8_4}{U}{U}{U} + +%% + +":" { return parser_type::make_COLON(location()); } +"+" { return parser_type::make_PLUS(location()); } +"-" { return parser_type::make_MINUS(location()); } + +"T" { return parser_type::make_CHAR_T(location()); } +"Z" { return parser_type::make_CHAR_Z(location()); } +" " { return parser_type::make_WHITESPACE(location()); } + +{integer} { + auto token = get_image(driver); + auto value = driver.parse_integer(token); + if (value) { + return parser_type::make_INTEGER(*value, location()); + } + return parser_type::make_INVALID_FIELD(location()); +} + +{decimal} { + auto token = get_image(driver); + auto value = driver.parse_decimal(token); + if (value) { + return parser_type::make_DECIMAL(*value, location()); + } + return parser_type::make_INVALID_FIELD(location()); +} + +{UTF8_CHAR} { + return parser_type::make_ERROR(location()); +} + +[\x00-\xff] { + return parser_type::make_ERROR(location()); +} + +<> { + return parser_type::make_END_OF_FILE(location(true)); +} + +%% \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d99834d..a0175d9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -59,6 +59,10 @@ add_test_executable(takatori/datetime/time_interval_test.cpp) add_test_executable(takatori/datetime/datetime_interval_test.cpp) add_test_executable(takatori/datetime/time_point_test.cpp) add_test_executable(takatori/datetime/time_zone_test.cpp) +add_test_executable(takatori/datetime/datetime_conversion_test.cpp) + +# datetime parser +add_test_executable(takatori/datetime/parser/datetime_parser_test.cpp) # descriptors add_test_executable(takatori/descriptor/descriptor_element_test.cpp) @@ -168,6 +172,7 @@ add_test_executable(takatori/util/basic_bitset_view_test.cpp) add_test_executable(takatori/util/clonable_test.cpp) add_test_executable(takatori/util/clonable_ptr_test.cpp) add_test_executable(takatori/util/downcast_test.cpp) +add_test_executable(takatori/util/either_test.cpp) add_test_executable(takatori/util/enum_set_test.cpp) add_test_executable(takatori/util/exception_test.cpp) add_test_executable(takatori/util/fail_test.cpp) diff --git a/test/takatori/datetime/datetime_conversion_test.cpp b/test/takatori/datetime/datetime_conversion_test.cpp new file mode 100644 index 0000000..2a5e2b8 --- /dev/null +++ b/test/takatori/datetime/datetime_conversion_test.cpp @@ -0,0 +1,214 @@ +#include + +#include + +#include + +namespace takatori::datetime { + +class datetime_conversion_test : public ::testing::Test {}; + +TEST_F(datetime_conversion_test, parse_date) { + auto r = parse_date("2024-09-16"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_EQ(info.year, 2024); + EXPECT_EQ(info.month, 9); + EXPECT_EQ(info.day, 16); +} + +TEST_F(datetime_conversion_test, parse_date_invalid_field) { + EXPECT_TRUE(parse_date("1-1-1")); + EXPECT_FALSE(parse_date("YYYY-MM-DD")); + + EXPECT_FALSE(parse_date("0-1-1")); + EXPECT_FALSE(parse_date("1-0-1")); + EXPECT_FALSE(parse_date("1-1-0")); + + EXPECT_FALSE(parse_date("1-13-1")); + EXPECT_FALSE(parse_date("1-1-32")); +} + +TEST_F(datetime_conversion_test, parse_date_invalid_form) { + EXPECT_FALSE(parse_date("00:00:00")); + EXPECT_FALSE(parse_date("2024-09-16 00:00:00")); +} + +TEST_F(datetime_conversion_test, parse_time) { + auto r = parse_time("12:34:56.789"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_EQ(info.hour, 12); + EXPECT_EQ(info.minute, 34); + EXPECT_EQ(info.second, 56); + EXPECT_EQ(info.subsecond, time_info::subsecond_type(789'000'000)); +} + +TEST_F(datetime_conversion_test, parse_time_invalid_field) { + EXPECT_TRUE(parse_time("0:0:0.0")); + EXPECT_FALSE(parse_time("HH:MM:SS.NNNNNNNNNN")); + + EXPECT_FALSE(parse_time("0:0:0.")); + EXPECT_FALSE(parse_time("24:0:0.0")); + EXPECT_FALSE(parse_time("0:60:0.0")); + EXPECT_FALSE(parse_time("0:0:60.0")); + EXPECT_FALSE(parse_time("0:0:0.0000000001")); +} + +TEST_F(datetime_conversion_test, parse_time_invalid_form) { + EXPECT_FALSE(parse_time("1970-1-1")); + EXPECT_FALSE(parse_time("0:0:0Z")); + EXPECT_FALSE(parse_time("+09")); +} + +TEST_F(datetime_conversion_test, parse_datetime) { + auto r = parse_datetime("1970-01-02 12:34:56.789"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_EQ(info.date.year, 1970); + EXPECT_EQ(info.date.month, 1); + EXPECT_EQ(info.date.day, 2); + + EXPECT_EQ(info.time.hour, 12); + EXPECT_EQ(info.time.minute, 34); + EXPECT_EQ(info.time.second, 56); + EXPECT_EQ(info.time.subsecond, time_info::subsecond_type(789'000'000)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_conversion_test, parse_datetime_with_offset) { + auto r = parse_datetime("1970-01-02 12:34:56.789Z"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_EQ(info.date.year, 1970); + EXPECT_EQ(info.date.month, 1); + EXPECT_EQ(info.date.day, 2); + + EXPECT_EQ(info.time.hour, 12); + EXPECT_EQ(info.time.minute, 34); + EXPECT_EQ(info.time.second, 56); + EXPECT_EQ(info.time.subsecond, time_info::subsecond_type(789'000'000)); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 0); + EXPECT_EQ(info.offset->minute, 0); +} + +TEST_F(datetime_conversion_test, parse_datetime_with_offset_numeric) { + auto r = parse_datetime("1970-01-02 12:34:56.789+09"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_EQ(info.date.year, 1970); + EXPECT_EQ(info.date.month, 1); + EXPECT_EQ(info.date.day, 2); + + EXPECT_EQ(info.time.hour, 12); + EXPECT_EQ(info.time.minute, 34); + EXPECT_EQ(info.time.second, 56); + EXPECT_EQ(info.time.subsecond, time_info::subsecond_type(789'000'000)); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 9); + EXPECT_EQ(info.offset->minute, 0); +} + +TEST_F(datetime_conversion_test, parse_datetime_without_time) { + auto r = parse_datetime("1970-01-02"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_EQ(info.date.year, 1970); + EXPECT_EQ(info.date.month, 1); + EXPECT_EQ(info.date.day, 2); + + EXPECT_EQ(info.time.hour, 0); + EXPECT_EQ(info.time.minute, 0); + EXPECT_EQ(info.time.second, 0); + EXPECT_EQ(info.time.subsecond, time_info::subsecond_type(0)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_conversion_test, parse_datetime_invalid_field) { + EXPECT_TRUE(parse_datetime("1-1-1 0:0:0.0")); + + EXPECT_FALSE(parse_datetime("0-1-1 0:0:0.0")); + EXPECT_FALSE(parse_datetime("1-0-1 0:0:0.0")); + EXPECT_FALSE(parse_datetime("1-1-0 0:0:0.0")); + + EXPECT_FALSE(parse_datetime("1-13-1 0:0:0.0")); + EXPECT_FALSE(parse_datetime("1-1-32 0:0:0.0")); + EXPECT_FALSE(parse_datetime("1-1-1 24:0:0.0")); + EXPECT_FALSE(parse_datetime("1-1-1 0:60:0.0")); + EXPECT_FALSE(parse_datetime("1-1-1 0:0:60.0000000001")); + + EXPECT_TRUE(parse_datetime("1-1-1 0:0:0.0+0:0")); + EXPECT_FALSE(parse_datetime("1-1-1 0:0:0.0+24:0")); + EXPECT_FALSE(parse_datetime("1-1-1 0:0:0.0+0:60")); + EXPECT_FALSE(parse_datetime("1-1-1 0:0:0.0-24:0")); + EXPECT_FALSE(parse_datetime("1-1-1 0:0:0.0-0:60")); +} + +TEST_F(datetime_conversion_test, parse_datetime_invalid_form) { + EXPECT_FALSE(parse_datetime("0:0:0.0")); + EXPECT_FALSE(parse_datetime("Z")); +} + +TEST_F(datetime_conversion_test, parse_zone_offset) { + auto r = parse_zone_offset("Z"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_TRUE(info.plus); + EXPECT_EQ(info.hour, 0); + EXPECT_EQ(info.minute, 0); +} + +TEST_F(datetime_conversion_test, parse_zone_offset_numeric) { + auto r = parse_zone_offset("+9:00"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_TRUE(info.plus); + EXPECT_EQ(info.hour, 9); + EXPECT_EQ(info.minute, 0); +} + +TEST_F(datetime_conversion_test, parse_zone_offset_hour) { + auto r = parse_zone_offset("-12:34"); + ASSERT_TRUE(r.has_value()) << r.error(); + auto&& info = r.value(); + + EXPECT_FALSE(info.plus); + EXPECT_EQ(info.hour, 12); + EXPECT_EQ(info.minute, 34); +} + +TEST_F(datetime_conversion_test, parse_zone_offset_invalid_field) { + EXPECT_TRUE(parse_zone_offset("+0:0")); + EXPECT_FALSE(parse_zone_offset("+24:0")); + EXPECT_FALSE(parse_zone_offset("+0:60")); + + EXPECT_TRUE(parse_zone_offset("-0:0")); + EXPECT_FALSE(parse_zone_offset("-24:0")); + EXPECT_FALSE(parse_zone_offset("-0:60")); + + EXPECT_TRUE(parse_zone_offset("+0")); + EXPECT_FALSE(parse_zone_offset("+24")); +} + +TEST_F(datetime_conversion_test, parse_zone_offset_invalid_form) { + EXPECT_FALSE(parse_zone_offset("1970-01-01 12:34:56Z")); + EXPECT_FALSE(parse_zone_offset("1970-01-01")); + EXPECT_FALSE(parse_zone_offset("12:34:56")); +} + +} // namespace takatori::datetime diff --git a/test/takatori/datetime/parser/datetime_parser_test.cpp b/test/takatori/datetime/parser/datetime_parser_test.cpp new file mode 100644 index 0000000..f4edca3 --- /dev/null +++ b/test/takatori/datetime/parser/datetime_parser_test.cpp @@ -0,0 +1,270 @@ +#include + +#include + +namespace takatori::datetime::parser { + +using subsecond_type = time_info::subsecond_type; + +class datetime_parser_test : public ::testing::Test { +protected: + std::string_view error(parser::result_type const& result) { + if (result.has_error()) { + return result.error().message; + } + return {}; + } +}; + +TEST_F(datetime_parser_test, date) { + auto r = parse("2024-09-16"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + ASSERT_TRUE(info.date); + EXPECT_EQ(info.date->year, 2024); + EXPECT_EQ(info.date->month, 9); + EXPECT_EQ(info.date->day, 16); + + EXPECT_FALSE(info.time); + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, time) { + auto r = parse("12:34:56"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 12); + EXPECT_EQ(info.time->minute, 34); + EXPECT_EQ(info.time->second, 56); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, time_millis) { + auto r = parse("12:34:56.789"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 12); + EXPECT_EQ(info.time->minute, 34); + EXPECT_EQ(info.time->second, 56); + EXPECT_EQ(info.time->subsecond, subsecond_type(789'000'000)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, time_micros) { + auto r = parse("12:34:56.000789"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 12); + EXPECT_EQ(info.time->minute, 34); + EXPECT_EQ(info.time->second, 56); + EXPECT_EQ(info.time->subsecond, subsecond_type(789'000)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, time_nanos) { + auto r = parse("12:34:56.000000789"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 12); + EXPECT_EQ(info.time->minute, 34); + EXPECT_EQ(info.time->second, 56); + EXPECT_EQ(info.time->subsecond, subsecond_type(789)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, time_offset_plus) { + auto r = parse("1:2:3+4:5", parser().set_debug()); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 1); + EXPECT_EQ(info.time->minute, 2); + EXPECT_EQ(info.time->second, 3); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 4); + EXPECT_EQ(info.offset->minute, 5); +} + +TEST_F(datetime_parser_test, time_offset_minus) { + auto r = parse("01:02:03-04:00", parser().set_debug()); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 1); + EXPECT_EQ(info.time->minute, 2); + EXPECT_EQ(info.time->second, 3); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + ASSERT_TRUE(info.offset); + EXPECT_FALSE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 4); + EXPECT_EQ(info.offset->minute, 00); +} + +TEST_F(datetime_parser_test, time_offset_x) { + auto r = parse("01:02:03Z", parser().set_debug()); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 1); + EXPECT_EQ(info.time->minute, 2); + EXPECT_EQ(info.time->second, 3); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 0); + EXPECT_EQ(info.offset->minute, 0); +} + +TEST_F(datetime_parser_test, datetime) { + auto r = parse("1970-01-02 03:04:05"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + ASSERT_TRUE(info.date); + EXPECT_EQ(info.date->year, 1970); + EXPECT_EQ(info.date->month, 1); + EXPECT_EQ(info.date->day, 2); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 3); + EXPECT_EQ(info.time->minute, 4); + EXPECT_EQ(info.time->second, 5); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, datetime_t) { + auto r = parse("1970-01-02T03:04:05"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + ASSERT_TRUE(info.date); + EXPECT_EQ(info.date->year, 1970); + EXPECT_EQ(info.date->month, 1); + EXPECT_EQ(info.date->day, 2); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 3); + EXPECT_EQ(info.time->minute, 4); + EXPECT_EQ(info.time->second, 5); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + EXPECT_FALSE(info.offset); +} + +TEST_F(datetime_parser_test, datetime_offset) { + auto r = parse("1970-01-02 03:04:05+06:07"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + ASSERT_TRUE(info.date); + EXPECT_EQ(info.date->year, 1970); + EXPECT_EQ(info.date->month, 1); + EXPECT_EQ(info.date->day, 2); + + ASSERT_TRUE(info.time); + EXPECT_EQ(info.time->hour, 3); + EXPECT_EQ(info.time->minute, 4); + EXPECT_EQ(info.time->second, 5); + EXPECT_EQ(info.time->subsecond, subsecond_type(0)); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 6); + EXPECT_EQ(info.offset->minute, 7); +} + +TEST_F(datetime_parser_test, offset) { + auto r = parse("+01:02"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + EXPECT_FALSE(info.time); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 1); + EXPECT_EQ(info.offset->minute, 2); +} + +TEST_F(datetime_parser_test, offset_minus) { + auto r = parse("-01:02"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + EXPECT_FALSE(info.time); + + ASSERT_TRUE(info.offset); + EXPECT_FALSE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 1); + EXPECT_EQ(info.offset->minute, 2); +} + +TEST_F(datetime_parser_test, offset_z) { + auto r = parse("Z"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + EXPECT_FALSE(info.time); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 0); + EXPECT_EQ(info.offset->minute, 0); +} + +TEST_F(datetime_parser_test, offset_hour) { + auto r = parse("+09"); + ASSERT_TRUE(r.has_value()) << error(r); + auto&& info = r.value(); + + EXPECT_FALSE(info.date); + EXPECT_FALSE(info.time); + + ASSERT_TRUE(info.offset); + EXPECT_TRUE(info.offset->plus); + EXPECT_EQ(info.offset->hour, 9); + EXPECT_EQ(info.offset->minute, 0); +} + +} // namespace takatori::datetime::parser diff --git a/test/takatori/util/either_test.cpp b/test/takatori/util/either_test.cpp new file mode 100644 index 0000000..dd1fd1a --- /dev/null +++ b/test/takatori/util/either_test.cpp @@ -0,0 +1,172 @@ +#include + +#include + +namespace takatori::util { + +class either_test : public ::testing::Test { +public: + template + static T const& make_const(T const& v) { return v; } +}; + +TEST_F(either_test, empty) { + either e; + + ASSERT_FALSE(e); + EXPECT_FALSE(e.has_value()); + EXPECT_TRUE(e.is_error()); + EXPECT_EQ(e.error(), false); +} + +TEST_F(either_test, normal) { + either e { make_const(100) }; + + ASSERT_TRUE(e); + EXPECT_TRUE(e.has_value()); + EXPECT_FALSE(e.is_error()); + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, normal_rvalue) { + either e { 100 }; + + ASSERT_TRUE(e); + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, assign_normal) { + either e; + e = make_const(100); + + ASSERT_TRUE(e); + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, assign_normal_rvalue) { + either e; + e = 100; + + ASSERT_TRUE(e); + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, error) { + either e { make_const(true) }; + + ASSERT_FALSE(e); + EXPECT_FALSE(e.has_value()); + EXPECT_TRUE(e.is_error()); + EXPECT_EQ(e.error(), true); +} + +TEST_F(either_test, error_rvalue) { + either e { true }; + + ASSERT_FALSE(e); + EXPECT_EQ(e.error(), true); +} + +TEST_F(either_test, assign_error) { + either e; + e = make_const(true); + + ASSERT_FALSE(e); + EXPECT_EQ(e.error(), true); +} + +TEST_F(either_test, assign_error_rvalue) { + either e; + e = true; + + ASSERT_FALSE(e); + EXPECT_EQ(e.error(), true); +} + +TEST_F(either_test, inplace_normal) { + either e { std::in_place_type, 100 }; + + ASSERT_TRUE(e); + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, inplace_error) { + either e { std::in_place_type, true }; + + ASSERT_FALSE(e); + EXPECT_EQ(e.error(), true); +} + +TEST_F(either_test, emplace_normal) { + either e; + e.emplace(100); + + ASSERT_TRUE(e); + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, emplace_error) { + either e; + e.emplace(false); + + ASSERT_FALSE(e); + EXPECT_EQ(e.error(), false); +} + +TEST_F(either_test, value_normal) { + either e { 100 }; + EXPECT_EQ(e.value(), 100); +} + +TEST_F(either_test, value_error) { + either e { true }; + EXPECT_ANY_THROW((void) e.value()); +} + +TEST_F(either_test, get_normal) { + either e { 100 }; + auto* p = e.get(); + ASSERT_TRUE(p); + EXPECT_EQ(*p, 100); +} + +TEST_F(either_test, get_error) { + either e { true }; + auto* p = e.get(); + EXPECT_FALSE(p); +} + +TEST_F(either_test, error_normal) { + either e { 100 }; + EXPECT_ANY_THROW((void) e.error()); +} + +TEST_F(either_test, error_error) { + either e { true }; + EXPECT_EQ(e.error(), true); +} + +TEST_F(either_test, compare) { + using E = either; + + EXPECT_EQ(E(1), E(1)); + EXPECT_NE(E(1), E(2)); + + EXPECT_EQ(E(true), E(true)); + EXPECT_NE(E(true), E(false)); + + EXPECT_NE(E(0), E(false)); + EXPECT_NE(E(false), E(0)); +} + +TEST_F(either_test, output_normal) { + either e { 100 }; + std::cout << e << std::endl; +} + +TEST_F(either_test, output_error) { + either e { true }; + std::cout << e << std::endl; +} + +} // namespace takatori::util