From 4951f62b69854964222f69d94d89ef0014aa1e24 Mon Sep 17 00:00:00 2001 From: "M. Galib Uludag" Date: Sun, 23 Jun 2024 09:34:52 +0000 Subject: [PATCH] adjust project and make C++11 compability --- .github/actions/build-docs/action.yaml | 79 ++++ .../actions/update-redirect-page/action.yaml | 54 +++ .../update-version-selector/action.yaml | 56 +++ .github/workflows/c-cpp.yml | 4 + .github/workflows/create-git-main-docs.yml | 38 ++ .vscode/settings.json | 54 +++ CMakeLists.txt | 117 +++++- cmake/chrono_parseConfig.cmake.in | 3 + doc/CMakeLists.txt | 14 + doc/header.html | 90 +++++ doc/main.md.in | 5 + doc/version_selector_handler.js | 20 + example/main.cpp | 8 +- include/chrono_parse.hpp | 283 -------------- include/mgutility/_common/definitions.hpp | 85 +++++ include/mgutility/chrono/parse.hpp | 358 ++++++++++++++++++ include/mgutility/std/charconv.hpp | 104 +++++ include/mgutility/std/string_view.hpp | 314 +++++++++++++++ test/CMakeLists.txt | 11 - .../test_chrono_parse.cpp | 7 +- 20 files changed, 1386 insertions(+), 318 deletions(-) create mode 100644 .github/actions/build-docs/action.yaml create mode 100644 .github/actions/update-redirect-page/action.yaml create mode 100644 .github/actions/update-version-selector/action.yaml create mode 100644 .github/workflows/create-git-main-docs.yml create mode 100644 .vscode/settings.json create mode 100644 cmake/chrono_parseConfig.cmake.in create mode 100644 doc/CMakeLists.txt create mode 100644 doc/header.html create mode 100644 doc/main.md.in create mode 100644 doc/version_selector_handler.js delete mode 100644 include/chrono_parse.hpp create mode 100644 include/mgutility/_common/definitions.hpp create mode 100644 include/mgutility/chrono/parse.hpp create mode 100644 include/mgutility/std/charconv.hpp create mode 100644 include/mgutility/std/string_view.hpp delete mode 100644 test/CMakeLists.txt rename test/chrono_parse_test.cpp => tests/test_chrono_parse.cpp (74%) diff --git a/.github/actions/build-docs/action.yaml b/.github/actions/build-docs/action.yaml new file mode 100644 index 0000000..24ac03e --- /dev/null +++ b/.github/actions/build-docs/action.yaml @@ -0,0 +1,79 @@ +name: 'build-docs' +description: 'Builds documentation using Doxygen' +inputs: + cmake_target: + description: 'CMake documentation target' + required: true + docs_dir: + description: 'Path to documentation dir, relative to build_dir' + required: true + github_token: + description: 'GitHub token' + required: true + build_dir: + description: 'Build directory' + required: false + default: 'build' + cmake_configure_args: + description: 'Additional CMake configure arguments' + required: false + default: '' + destination_dir: + description: 'Directory name for deployed docs' + required: false + default: '' + docs_branch: + description: 'Documentation branch' + required: false + default: 'gh-pages' + +outputs: + version: + description: 'Version of the generated docs' + value: ${{ steps.get-docs-version.outputs.version }} + +runs: + using: "composite" + steps: + - name: Install deps + shell: bash + run: | + sudo apt install -y cmake + sudo apt install -y wget + cd ~ + wget -nv https://www.doxygen.nl/files/doxygen-1.10.0.linux.bin.tar.gz + tar -xzf doxygen-1.10.0.linux.bin.tar.gz + echo "$(pwd)/doxygen-1.10.0/bin" >> $GITHUB_PATH + + - name: CMake configuration + shell: bash + run: cmake ${{ inputs.cmake_configure_args }} -B ${{ inputs.build_dir }} -DENUM_NAME_BUILD_DOCS=ON + + - name: CMake build + shell: bash + run: cmake --build ${{ inputs.build_dir }} --target ${{ inputs.cmake_target }} + + - name: Get docs version + id: get-docs-version + shell: bash + run: | + subdir=$(basename $(find ${{ inputs.build_dir }}/${{ inputs.docs_dir }} -mindepth 1 -maxdepth 1 -type d | head -n 1)) + echo "version=$subdir" >> $GITHUB_OUTPUT + + - name: Deploy docs + if: ${{ inputs.destination_dir != '' }} + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ inputs.github_token }} + publish_dir: ${{ inputs.build_dir }}/${{ inputs.docs_dir }}/${{ steps.get-docs-version.outputs.version }} + destination_dir: ${{ inputs.destination_dir }} + publish_branch: ${{ inputs.docs_branch }} + + - name: Deploy docs + if: ${{ inputs.destination_dir == '' }} + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ inputs.github_token }} + publish_dir: ${{ inputs.build_dir }}/${{ inputs.docs_dir }}/${{ steps.get-docs-version.outputs.version }} + destination_dir: ${{ steps.get-docs-version.outputs.version }} + publish_branch: ${{ inputs.docs_branch }} \ No newline at end of file diff --git a/.github/actions/update-redirect-page/action.yaml b/.github/actions/update-redirect-page/action.yaml new file mode 100644 index 0000000..68f11bf --- /dev/null +++ b/.github/actions/update-redirect-page/action.yaml @@ -0,0 +1,54 @@ +name: 'update-redirect-page' +description: 'Updates redirect HTML page' +inputs: + github_token: + description: 'GitHub token' + required: true + target_url: + description: 'Redirect target URL' + temp_dir: + description: 'Directory where redirect file will be generated' + required: false + default: 'redirect-dir' + file_name: + description: 'Generated file name' + required: false + default: 'index.html' + destination_dir: + description: 'Redirect file destination directory' + required: false + default: '' + docs_branch: + description: 'Documentation branch' + required: false + default: 'gh-pages' + +runs: + using: "composite" + steps: + - name: Generate redirect HTML + shell: bash + run: | + mkdir ${{ inputs.temp_dir }} + cat << EOF > ${{ inputs.temp_dir }}/${{ inputs.file_name }} + + + + + + Redirecting... + + +

If you are not redirected automatically, click here.

+ + + EOF + + - name: Deploy redirect page + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ inputs.github_token }} + publish_dir: ${{ inputs.temp_dir }} + destination_dir: ${{ inputs.destination_dir }} + publish_branch: ${{ inputs.docs_branch }} + keep_files: true \ No newline at end of file diff --git a/.github/actions/update-version-selector/action.yaml b/.github/actions/update-version-selector/action.yaml new file mode 100644 index 0000000..0b6db70 --- /dev/null +++ b/.github/actions/update-version-selector/action.yaml @@ -0,0 +1,56 @@ +name: 'update-version-selector' +description: 'Updates version selector' +inputs: + github_token: + description: 'GitHub token' + required: true + temp_dir: + description: 'Directory where version selector file will be generated' + required: false + default: 'selector-dir' + file_name: + description: 'Selector file name' + required: false + default: 'version_selector.html' + selector_id: + description: 'select element id' + required: false + default: 'versionSelector' + docs_branch: + description: 'Documentation branch' + required: false + default: 'gh-pages' +outputs: + versions_counter: + description: "Number of existing versions" + value: ${{ steps.discover-versions.outputs.counter }} + +runs: + using: "composite" + steps: + - name: Discover versions + id: discover-versions + shell: bash + run: | + git fetch origin ${{ inputs.docs_branch }} + dirs=$(git ls-tree --name-only -d origin/${{ inputs.docs_branch }} | sort -rV) + echo "counter=$(echo "$dirs" | wc -l | xargs)" >> $GITHUB_OUTPUT + + mkdir ${{ inputs.temp_dir }} + # Create HTML + echo '' >> ${{ inputs.temp_dir }}/${{ inputs.file_name }} + + - name: Deploy version selector + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ inputs.github_token }} + publish_dir: ${{ inputs.temp_dir }} + publish_branch: ${{ inputs.docs_branch }} + keep_files: true \ No newline at end of file diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index db21329..148cd98 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -16,9 +16,13 @@ jobs: fail-fast: false matrix: config: + - { compiler: gcc, version: 9, build_type: Release, cppstd: 11 } + - { compiler: gcc, version: 9, build_type: Release, cppstd: 14 } - { compiler: gcc, version: 9, build_type: Release, cppstd: 17 } - { compiler: gcc, version: 11, build_type: Debug, cppstd: 20 } - { compiler: gcc, version: 12, build_type: Release, cppstd: 20 } + - { compiler: clang, version: 11, build_type: Release, cppstd: 11 } + - { compiler: clang, version: 11, build_type: Release, cppstd: 14 } - { compiler: clang, version: 11, build_type: Release, cppstd: 17 } - { compiler: clang, version: 11, build_type: Debug, cppstd: 17, asan: OFF } - { compiler: clang, version: 12, build_type: Debug, cppstd: 17, asan: OFF } diff --git a/.github/workflows/create-git-main-docs.yml b/.github/workflows/create-git-main-docs.yml new file mode 100644 index 0000000..ceb1408 --- /dev/null +++ b/.github/workflows/create-git-main-docs.yml @@ -0,0 +1,38 @@ +name: Create git-main docs + +permissions: + contents: write + +on: + push: + branches: + - main + +jobs: + create-git-main-docs: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Build docs + id: build-docs + uses: ./.github/actions/build-docs + with: + cmake_target: 'doc' + docs_dir: 'doc/docs' + destination_dir: git-main + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update version selector + id: update-version-selector + uses: ./.github/actions/update-version-selector + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create redirect page if there are no releases + if: ${{ steps.update-version-selector.outputs.versions_counter == 1}} + uses: ./.github/actions/update-redirect-page + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + target_url: git-main/index.html \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6d1b949 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,54 @@ +{ + "files.associations": { + "charconv": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "iomanip": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index cce3c30..7db341f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,28 +1,107 @@ cmake_minimum_required(VERSION 3.14) -project(chrono_parse_example VERSION 0.1 LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) +project( + chrono_parse + VERSION 1.0 + LANGUAGES CXX) -include(FetchContent) +# Define the library +add_library(chrono_parse INTERFACE) +add_library(mgutility::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) -FetchContent_Declare( - fmt - GIT_REPOSITORY "https://github.com/fmtlib/fmt" - GIT_TAG "9.1.0" -) +# Specify the include directories for the library +target_include_directories( + chrono_parse INTERFACE $ + $) -FetchContent_Declare( - DocTest - GIT_REPOSITORY "https://github.com/onqtam/doctest" - GIT_TAG "v2.4.11" -) +# Set the C++ standard +target_compile_features(chrono_parse INTERFACE cxx_std_11) + +if(CMAKE_SYSTEM_NAME STREQUAL Linux) + include(GNUInstallDirs) + set(include_install_dir ${CMAKE_INSTALL_INCLUDEDIR}) + set(config_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") +else() + set(include_install_dir "include") + set(config_install_dir "lib/cmake/${PROJECT_NAME}") +endif() + +# Create the package configuration files +include(CMakePackageConfigHelpers) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/chrono_parseConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY AnyNewerVersion) + +configure_package_config_file( + ${CMAKE_CURRENT_LIST_DIR}/cmake/chrono_parseConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/chrono_parseConfig.cmake" + INSTALL_DESTINATION lib/cmake/chrono_parse) + +if(NOT ${CHRONO_PARSE_NO_INSTALL}) + # Install the library + install( + TARGETS chrono_parse + EXPORT chrono_parseTargets + INCLUDES + DESTINATION include) + + install( + EXPORT chrono_parseTargets + FILE chrono_parseTargets.cmake + NAMESPACE mgutility:: + DESTINATION lib/cmake/chrono_parse) + + install(DIRECTORY include/ DESTINATION include) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/chrono_parseConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/chrono_parseConfigVersion.cmake" + DESTINATION lib/cmake/chrono_parse) + + export( + EXPORT chrono_parseTargets + FILE "${CMAKE_CURRENT_BINARY_DIR}/chrono_parseTargets.cmake" + NAMESPACE mgutility::) +endif() + +# FetchContent to get the fmt library +include(FetchContent) +FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 8.1.1) # Change the tag to the desired version FetchContent_MakeAvailable(fmt) -FetchContent_MakeAvailable(DocTest) -add_executable(${PROJECT_NAME} example/main.cpp) -target_link_libraries(${PROJECT_NAME} fmt::fmt) -target_include_directories(${PROJECT_NAME} AFTER PUBLIC ${CMAKE_SOURCE_DIR}/include) +# Add example executable +add_executable(example_chrono_parse example/main.cpp) + +# Link the example executable with the library +target_link_libraries(example_chrono_parse PRIVATE mgutility::chrono_parse fmt::fmt) + +# if(NOT ${CHRONO_PARSE_NO_TESTS}) + # Enable testing + enable_testing() + + # FetchContent to get the Doctest testing framework + include(FetchContent) + FetchContent_Declare( + doctest + GIT_REPOSITORY https://github.com/doctest/doctest.git + GIT_TAG v2.4.11) + FetchContent_MakeAvailable(doctest) + + # Add test executable + add_executable(test_chrono_parse tests/test_chrono_parse.cpp) + + # Link the test executable with the library and Doctest + target_link_libraries(test_chrono_parse PRIVATE mgutility::chrono_parse + doctest::doctest) + + # Add tests + add_test(NAME test_chrono_parse COMMAND test_chrono_parse) +# endif() -enable_testing() -add_subdirectory(test) +if(${CHRONO_PARSE_BUILD_DOCS}) + add_subdirectory(doc) +endif() \ No newline at end of file diff --git a/cmake/chrono_parseConfig.cmake.in b/cmake/chrono_parseConfig.cmake.in new file mode 100644 index 0000000..05b74d0 --- /dev/null +++ b/cmake/chrono_parseConfig.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/chrono_parseTargets.cmake") \ No newline at end of file diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 0000000..a562088 --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,14 @@ +find_package(Doxygen REQUIRED) + +configure_file("main.md.in" "main.md" @ONLY) + +set(DOXYGEN_HTML_OUTPUT "${PROJECT_VERSION}") +set(DOXYGEN_OUTPUT_DIRECTORY "docs") +set(DOXYGEN_HTML_HEADER "header.html") +set(DOXYGEN_HTML_EXTRA_FILES "version_selector_handler.js") +set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_CURRENT_BINARY_DIR}/main.md") + +doxygen_add_docs( + doc + "${PROJECT_SOURCE_DIR}" +) \ No newline at end of file diff --git a/doc/header.html b/doc/header.html new file mode 100644 index 0000000..48874f1 --- /dev/null +++ b/doc/header.html @@ -0,0 +1,90 @@ + + + + + + + + + + + $projectname: $title + + $title + + + + + + + + + + + + + + + $treeview + $search + $mathjax + $darkmode + + $extrastylesheet + + + + + + +
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname + +
+ +
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + \ No newline at end of file diff --git a/doc/main.md.in b/doc/main.md.in new file mode 100644 index 0000000..8770e74 --- /dev/null +++ b/doc/main.md.in @@ -0,0 +1,5 @@ +# Main page + +parse date and times with {fmt} style intostd::chrono::time_point {WIP} written in C++>=11 + +Version: @PROJECT_VERSION@ \ No newline at end of file diff --git a/doc/version_selector_handler.js b/doc/version_selector_handler.js new file mode 100644 index 0000000..7f0d1fc --- /dev/null +++ b/doc/version_selector_handler.js @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024, Oleksandr Koval + +$(function () { + var repoName = window.location.pathname.split('/')[1]; + $.get('/' + repoName + '/version_selector.html', function (data) { + // Inject version selector HTML into the page + $('#projectnumber').html(data); + + // Event listener to handle version selection + document.getElementById('versionSelector').addEventListener('change', function () { + var selectedVersion = this.value; + window.location.href = '/' + repoName + '/' + selectedVersion + '/index.html'; + }); + + // Set the selected option based on the current version + var currentVersion = window.location.pathname.split('/')[2]; + $('#versionSelector').val(currentVersion); + }); +}); \ No newline at end of file diff --git a/example/main.cpp b/example/main.cpp index 12dc12f..a01f81c 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -1,11 +1,13 @@ -#include "chrono_parse.hpp" +#include "mgutility/chrono/parse.hpp" // {fmt} for printing #include #include int main() { - const auto chrono_time = mgutility::chrono::parse("{:%FT%T.%f%z}", "2023-04-16T00:05:23.999+0100"); + const auto chrono_time = + mgutility::chrono::parse("{:%FT%T.%f%z}", "2023-04-16T00:05:23.999+0100"); - fmt::print("{:%F %T}\n", chrono_time); // prints 2023-04-15 23:05:23.999000000 ({fmt} trunk version) + fmt::print("{:%F %T}\n", chrono_time); // prints 2023-04-15 23:05:23.999000000 + // ({fmt} trunk version) } \ No newline at end of file diff --git a/include/chrono_parse.hpp b/include/chrono_parse.hpp deleted file mode 100644 index ddd155d..0000000 --- a/include/chrono_parse.hpp +++ /dev/null @@ -1,283 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Muhammed Galib Uludag - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -#ifndef MGUTILITY_CHRONO_PARSE_HPP -#define MGUTILITY_CHRONO_PARSE_HPP - -#include -#include -#include -#include -#include -#include -#include - -namespace mgutility { -namespace chrono{ -namespace detail { - -struct tm : std::tm { - uint32_t tm_ms; -}; - -inline auto parse_integer(std::string_view str, uint32_t len, uint32_t& next, - uint32_t begin_offset = 0) -> int32_t { - int32_t result{0}; - if (str.size() < len + next) - throw std::invalid_argument("value is not convertible!"); - for (auto it{str.begin() + next + begin_offset}; - it != str.begin() + len + next - 2; ++it) { - if (!std::isdigit(*it)) - throw std::invalid_argument("value is not convertible!"); - } - auto error = std::from_chars(str.data() + next + begin_offset, - str.data() + len + next, result); - - next = ++len + next; - - if (error.ec != std::errc()) - throw std::invalid_argument("value is not convertible!"); - return result; -} - -inline constexpr auto check_range(int32_t value, int32_t min, int32_t max) { - if (value < min || value > max) { - throw std::out_of_range("value is out of range!"); - } -} - -// inspired from https://sources.debian.org/src/tdb/1.2.1-2/libreplace/timegm.c/ -inline constexpr auto mktime(std::tm& tm) -> std::time_t { - auto constexpr is_leap_year = [](uint32_t year) { - return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); - }; - - constexpr std::array, 2> num_of_days{ - {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // 365 - {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}}; // 366 - - std::time_t result{0}; - - if (tm.tm_mon > 12 || tm.tm_mon < 0 || tm.tm_mday > 31 || tm.tm_min > 60 || - tm.tm_sec > 60 || tm.tm_hour > 24) { - throw std::out_of_range("value is out of range!"); - } - - tm.tm_year += 1900; - - for (auto i{1970}; i < tm.tm_year; ++i) { - result += is_leap_year(i) ? 366 : 365; - } - - for (auto i{0}; i < tm.tm_mon; ++i) { - result += num_of_days[is_leap_year(tm.tm_year)][i]; - } - - result += tm.tm_mday - 1; // nth day since 1970 - result *= 24; - result += tm.tm_hour; - result *= 60; - result += tm.tm_min; - result *= 60; - result += tm.tm_sec; - - return result; -} - -template -std::enable_if_t, T> constexpr get_time( - std::string_view format, std::string_view date_str) { - int32_t count{0}; - uint32_t beg{0}, end{0}; - for (auto i{0}; i < format.size(); ++i) { - switch (format[i]) { - case '{': - beg = i; - ++count; - break; - case '}': - end = i; - --count; - break; - } - if (beg < end) - break; - else if (count != 0 && end < beg) - break; - } - - if (format[beg + 1] != ':' && end - beg < 3 || count != 0) - throw std::invalid_argument("invalid format string!"); - - T tm{}; - - auto handle_timezone = [&tm](int32_t offset) { - const auto minute = offset % 100; - const auto hour = offset / 100; - if (offset < 0) { - tm.tm_min + minute < 0 ? tm.tm_hour - 1 < 0 ? --tm.tm_mday, - tm.tm_hour = 23 - tm.tm_hour : --tm.tm_hour : 0; - tm.tm_min -= minute; - tm.tm_min %= 60; - - tm.tm_hour + hour < 0 ? --tm.tm_mday, tm.tm_hour = 24 + hour - : tm.tm_hour += hour; - } else { - tm.tm_min + minute > 59 ? tm.tm_hour + 1 > 23 ? ++tm.tm_mday, - ++tm.tm_hour %= 24 : ++tm.tm_hour : 0; - tm.tm_min += minute; - tm.tm_min %= 60; - - tm.tm_hour + hour > 23 ? ++tm.tm_mday, - tm.tm_hour = (tm.tm_hour + hour) % 24, - (tm.tm_mon > 10 ? ++tm.tm_mon : 0) : tm.tm_hour += hour; - } - }; - - uint32_t next{0}; - - for (auto i{beg}; i < end; ++i) { - switch (format[i]) { - case '%': { - if (i + 1 >= format.size()) - throw std::invalid_argument("invalid format string!"); - switch (format[i + 1]) { - case 'Y': - tm.tm_year = parse_integer(date_str, 4, next) % 1900; - break; - case 'm': - tm.tm_mon = parse_integer(date_str, 2, next) - 1; - check_range(tm.tm_mon, 0, 11); - break; - case 'd': - tm.tm_mday = parse_integer(date_str, 2, next); - check_range(tm.tm_mday, 1, 31); - break; - case 'F': { - tm.tm_year = parse_integer(date_str, 4, next) % 1900; - tm.tm_mon = parse_integer(date_str, 2, next) - 1; - tm.tm_mday = parse_integer(date_str, 2, next); - check_range(tm.tm_mon, 0, 11); - check_range(tm.tm_mday, 1, 31); - } break; - case 'H': - tm.tm_hour = parse_integer(date_str, 2, next); - check_range(tm.tm_hour, 0, 23); - break; - case 'M': - tm.tm_min = parse_integer(date_str, 2, next); - check_range(tm.tm_min, 0, 59); - break; - case 'S': - tm.tm_sec = parse_integer(date_str, 2, next); - check_range(tm.tm_sec, 0, 59); - break; - case 'T': { - tm.tm_hour = parse_integer(date_str, 2, next); - tm.tm_min = parse_integer(date_str, 2, next); - - tm.tm_sec = parse_integer(date_str, 2, next); - - check_range(tm.tm_hour, 0, 23); - check_range(tm.tm_min, 0, 59); - - check_range(tm.tm_sec, 0, 59); - } break; - case 'f': { - tm.tm_ms = parse_integer(date_str, 3, next); - check_range(tm.tm_ms, 0, 999); - break; - } - case 'z': { - if (*(date_str.begin() + next - 1) != 'Z') { - char sign{}; - auto diff{0}; - for (auto j{next - 1}; j < date_str.size(); ++j) { - if (date_str[j] == '-' || date_str[j] == '+') { - sign = date_str[j]; - diff = j - next + 1; - break; - } - } - auto hour_offset_str = - std::string_view{date_str.data() + next + diff, - date_str.size() - 1}; - auto pos = hour_offset_str.find(':'); - auto offset{0}; - if (pos < 3 && pos > 0) { - next = 0; - auto hour_offset = - parse_integer(hour_offset_str, 2, next); - auto min_offset = - parse_integer(hour_offset_str, 2, next); - offset = hour_offset * 100 + min_offset; - } else { - if (date_str.size() - next > 4 + diff) - throw std::invalid_argument( - "value is not convertible!"); - offset = parse_integer(date_str, - date_str.size() - next, - next, diff); - } - check_range(offset, 0, 1200); - switch (sign) { - case '+': - handle_timezone(offset * -1); - break; - case '-': - handle_timezone(offset); - break; - } - } - } break; - } - } break; - case ' ': - case '-': - case '/': - case '.': - case ':': - if (i > 1 && format[i] != date_str[next - 1]) - throw std::invalid_argument("value is not convertible!"); - break; - } - } - - return tm; -} - -} // namespace detail - -auto parse(std::string_view format, std::string_view date_str) - -> std::chrono::system_clock::time_point { - auto tm = detail::get_time(format, date_str); - auto time_t = detail::mktime(tm); - std::chrono::system_clock::time_point clock = - std::chrono::system_clock::from_time_t(time_t); - clock += std::chrono::milliseconds(tm.tm_ms); - return clock; -} -} // namespace chrono -} // namespace mgutility - -#endif // MGUTILITY_CHRONO_PARSE_HPP diff --git a/include/mgutility/_common/definitions.hpp b/include/mgutility/_common/definitions.hpp new file mode 100644 index 0000000..021dcea --- /dev/null +++ b/include/mgutility/_common/definitions.hpp @@ -0,0 +1,85 @@ +/* +MIT License + +Copyright (c) 2024 mguludag + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef MGUTILITY_COMMON_DEFINITIONS_HPP +#define MGUTILITY_COMMON_DEFINITIONS_HPP + +/** + * @file definitions.hpp + * @brief Defines macros for compiler and standard support detection. + */ + +/** + * @brief Defines the MGUTILITY_CPLUSPLUS macro for MSVC and other compilers. + * + * For MSVC, it uses _MSVC_LANG. For other compilers, it uses __cplusplus. + */ +#ifdef _MSC_VER +#define MGUTILITY_CPLUSPLUS _MSVC_LANG +#else +#define MGUTILITY_CPLUSPLUS __cplusplus +#endif + +/** + * @brief Defines the MGUTILITY_CNSTXPR macro based on the C++ standard. + * + * If the C++ standard is C++11, MGUTILITY_CNSTXPR is defined as empty. + * If the C++ standard is newer than C++11, MGUTILITY_CNSTXPR is defined as + * constexpr. If the C++ standard is older than C++11, an error is raised. + */ +#if MGUTILITY_CPLUSPLUS == 201103L +#define MGUTILITY_CNSTXPR +#elif MGUTILITY_CPLUSPLUS > 201103L +#define MGUTILITY_CNSTXPR constexpr +#elif MGUTILITY_CPLUSPLUS < 201103L +#error "Standards older than C++11 is not supported!" +#endif + +/** + * @brief Defines the MGUTILITY_CNSTXPR_CLANG_WA macro based on the C++ + * standard. + * + * If the C++ standard is newer than C++17 and the compiler is not Clang, + * MGUTILITY_CNSTXPR_CLANG_WA is defined as constexpr. Otherwise, it is defined + * as empty. + */ +#if MGUTILITY_CPLUSPLUS > 201703L +#define MGUTILITY_CNSTXPR_CLANG_WA constexpr +#else +#define MGUTILITY_CNSTXPR_CLANG_WA +#endif + +/** + * @brief Defines the MGUTILITY_CNSTEVL macro based on the C++ standard. + * + * If the C++ standard is newer than C++17, MGUTILITY_CNSTEVL is defined as + * consteval. Otherwise, it is defined as empty. + */ +#if MGUTILITY_CPLUSPLUS > 201703L +#define MGUTILITY_CNSTEVL consteval +#else +#define MGUTILITY_CNSTEVL +#endif + +#endif // MGUTILITY_COMMON_DEFINITIONS_HPP \ No newline at end of file diff --git a/include/mgutility/chrono/parse.hpp b/include/mgutility/chrono/parse.hpp new file mode 100644 index 0000000..c4ebced --- /dev/null +++ b/include/mgutility/chrono/parse.hpp @@ -0,0 +1,358 @@ +/* + * MIT License + * + * (c) 2023 Muhammed Galib Uludag + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef MGUTILITY_CHRONO_PARSE_HPP +#define MGUTILITY_CHRONO_PARSE_HPP + +#include "mgutility/std/charconv.hpp" +#include "mgutility/std/string_view.hpp" + +#include +#include +#include +#include +#include + +namespace mgutility { +namespace chrono { +namespace detail { + +/** + * @brief Extended tm structure with milliseconds. + */ +struct tm : std::tm { + uint32_t tm_ms; ///< Milliseconds. +}; + +/** + * @brief Parses an integer from a string view. + * + * @param str The string view to parse. + * @param len The length of the string view. + * @param next The position of the next character to parse. + * @param begin_offset The offset to begin parsing from. + * @return int32_t The parsed integer. + * @throws std::invalid_argument if the value is not convertible. + */ +MGUTILITY_CNSTXPR auto parse_integer(mgutility::string_view str, uint32_t len, + uint32_t &next, uint32_t begin_offset = 0) -> int32_t { + int32_t result{0}; + + auto error = mgutility::from_chars(str.data() + next + begin_offset, + str.data() + len + next, result); + + next = ++len + next; + + if (error.ec != std::errc()) { + throw std::invalid_argument("value is not convertible!"); + } + + return result; +} + +/** + * @brief Checks if a value is within a given range. + * + * @param value The value to check. + * @param min The minimum acceptable value. + * @param max The maximum acceptable value. + * @throws std::out_of_range if the value is out of range. + */ +constexpr auto check_range(int32_t value, int32_t min, int32_t max) -> void { + if (value < min || value > max) { + throw std::out_of_range("value is out of range!"); + } +} + +/** + * @brief Determines if a year is a leap year. + * + * @param year The year to check. + * @return bool True if the year is a leap year, false otherwise. + */ +auto MGUTILITY_CNSTXPR is_leap_year(uint32_t year) -> bool { + return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); +} + +/** + * @brief Converts a tm structure to a time_t value. + * + * @param tm The tm structure to convert. + * @return std::time_t The corresponding time_t value. + * @throws std::out_of_range if any tm value is out of valid range. + */ +MGUTILITY_CNSTXPR auto mktime(std::tm &tm) -> std::time_t { + MGUTILITY_CNSTXPR std::array, 2> num_of_days{ + {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // 365 days in a common year + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}}; // 366 days in a leap year + + std::time_t result{0}; + + // Check for out of range values in tm structure + if (tm.tm_mon > 12 || tm.tm_mon < 0 || tm.tm_mday > 31 || tm.tm_min > 60 || + tm.tm_sec > 60 || tm.tm_hour > 24) { + throw std::out_of_range("value is out of range!"); + } + + tm.tm_year += 1900; + + // Calculate the number of days since 1970 + for (auto i{1970}; i < tm.tm_year; ++i) { + result += is_leap_year(i) ? 366 : 365; + } + + // Add the days for the current year + for (auto i{0}; i < tm.tm_mon; ++i) { + result += num_of_days[is_leap_year(tm.tm_year)][i]; + } + + result += tm.tm_mday - 1; // nth day since 1970 + result *= 24; + result += tm.tm_hour; + result *= 60; + result += tm.tm_min; + result *= 60; + result += tm.tm_sec; + + return result; +} + +/** + * @brief Adjusts the tm structure for a given timezone offset. + * + * @param tm The tm structure to adjust. + * @param offset The timezone offset in hours and minutes. + */ +MGUTILITY_CNSTXPR auto handle_timezone(tm &tm, int32_t offset) -> void { + const auto minute = offset % 100; + const auto hour = offset / 100; + + if (offset < 0) { + // Adjust minutes + if (tm.tm_min + minute < 0) { + tm.tm_min += 60 - minute; + tm.tm_hour -= 1; + if (tm.tm_hour < 0) { + tm.tm_hour += 24; + tm.tm_mday -= 1; + } + } else { + tm.tm_min += minute; + } + + // Adjust hours + if (tm.tm_hour + hour < 0) { + tm.tm_hour += 24 + hour; + tm.tm_mday -= 1; + } else { + tm.tm_hour += hour; + } + } else { + // Adjust minutes + if (tm.tm_min + minute >= 60) { + tm.tm_min -= 60 - minute; + tm.tm_hour += 1; + if (tm.tm_hour >= 24) { + tm.tm_hour -= 24; + tm.tm_mday += 1; + } + } else { + tm.tm_min += minute; + } + + // Adjust hours + if (tm.tm_hour + hour >= 24) { + tm.tm_hour += hour - 24; + tm.tm_mday += 1; + if (tm.tm_mon == 11 && tm.tm_mday > 31) { // Handle December overflow + tm.tm_mday = 1; + tm.tm_mon = 0; + } else if (tm.tm_mday > 30) { // Handle month overflow for other months + tm.tm_mday = 1; + tm.tm_mon += 1; + } + } else { + tm.tm_hour += hour; + } + } +} + +/** + * @brief Parses a date and time string according to a specified format. + * + * @param format The format string. + * @param date_str The date and time string to parse. + * @return detail::tm The parsed time structure. + * @throws std::invalid_argument if the format string is invalid or parsing fails. + */ +MGUTILITY_CNSTXPR auto get_time(string_view format, string_view date_str) -> detail::tm { + int32_t count{0}; + uint32_t begin{0}, end{0}; + + // Find the positions of format specifiers + for (auto i{0}; i < format.size(); ++i) { + switch (format[i]) { + case '{': + begin = i; + ++count; + break; + case '}': + end = i; + --count; + break; + } + if (begin < end) break; + else if (count != 0 && end < begin) break; + } + + if (format[begin + 1] != ':' && end - begin < 3 || count != 0) + throw std::invalid_argument("invalid format string!"); + + detail::tm tm{}; + uint32_t next{0}; + + // Parse the date and time string based on the format specifiers + for (auto i{begin}; i < end; ++i) { + switch (format[i]) { + case '%': { + if (i + 1 >= format.size()) + throw std::invalid_argument("invalid format string!"); + switch (format[i + 1]) { + case 'Y': // Year with century (4 digits) + tm.tm_year = parse_integer(date_str, 4, next) % 1900; + break; + case 'm': // Month (01-12) + tm.tm_mon = parse_integer(date_str, 2, next) - 1; + check_range(tm.tm_mon, 0, 11); + break; + case 'd': // Day of the month (01-31) + tm.tm_mday = parse_integer(date_str, 2, next); + check_range(tm.tm_mday, 1, 31); + break; + case 'F': { // Full date (YYYY-MM-DD) + tm.tm_year = parse_integer(date_str, 4, next) % 1900; + tm.tm_mon = parse_integer(date_str, 2, next) - 1; + tm.tm_mday = parse_integer(date_str, 2, next); + check_range(tm.tm_mon, 0, 11); + check_range(tm.tm_mday, 1, 31); + } break; + case 'H': // Hour (00-23) + tm.tm_hour = parse_integer(date_str, 2, next); + check_range(tm.tm_hour, 0, 23); + break; + case 'M': // Minute (00-59) + tm.tm_min = parse_integer(date_str, 2, next); + check_range(tm.tm_min, 0, 59); + break; + case 'S': // Second (00-59) + tm.tm_sec = parse_integer(date_str, 2, next); + check_range(tm.tm_sec, 0, 59); + break; + case 'T': { // Full time (HH:MM:SS) + tm.tm_hour = parse_integer(date_str, 2, next); + tm.tm_min = parse_integer(date_str, 2, next); + tm.tm_sec = parse_integer(date_str, 2, next); + check_range(tm.tm_hour, 0, 23); + check_range(tm.tm_min, 0, 59); + check_range(tm.tm_sec, 0, 59); + } break; + case 'f': // Milliseconds (000-999) + tm.tm_ms = parse_integer(date_str, 3, next); + check_range(tm.tm_ms, 0, 999); + break; + case 'z': { // Timezone offset (+/-HHMM) + if (*(date_str.begin() + next - 1) != 'Z') { + char sign{}; + auto diff{0}; + for (auto j{next - 1}; j < date_str.size(); ++j) { + if (date_str[j] == '-' || date_str[j] == '+') { + sign = date_str[j]; + diff = j - next + 1; + break; + } + } + auto hour_offset_str = string_view{date_str.data() + next + diff, date_str.size() - 1}; + auto pos = hour_offset_str.find(':'); + auto offset{0}; + if (pos < 3 && pos > 0) { + next = 0; + auto hour_offset = parse_integer(hour_offset_str, 2, next); + auto min_offset = parse_integer(hour_offset_str, 2, next); + offset = hour_offset * 100 + min_offset; + } else { + if (date_str.size() - next > 4 + diff) + throw std::invalid_argument("value is not convertible!"); + offset = parse_integer(date_str, date_str.size() - next, next, diff); + } + check_range(offset, 0, 1200); + switch (sign) { + case '+': + handle_timezone(tm, offset * -1); + break; + case '-': + handle_timezone(tm, offset); + break; + } + } + } break; + default: + throw std::invalid_argument("unsupported format specifier!"); + } + } break; + case ' ': // Space separator + case '-': // Dash separator + case '/': // Slash separator + case '.': // Dot separator + case ':': // Colon separator + if (i > 1 && format[i] != date_str[next - 1]) + throw std::invalid_argument("value is not convertible!"); + break; + } + } + + return tm; +} + +} // namespace detail + +/** + * @brief Parses a date and time string into a std::chrono::system_clock::time_point. + * + * @param format The format string. + * @param date_str The date and time string to parse. + * @return std::chrono::system_clock::time_point The parsed time point. + */ +auto parse(string_view format, string_view date_str) -> std::chrono::system_clock::time_point { + auto tm = detail::get_time(format, date_str); + auto time_t = detail::mktime(tm); + std::chrono::system_clock::time_point clock = + std::chrono::system_clock::from_time_t(time_t); + clock += std::chrono::milliseconds(tm.tm_ms); + return clock; +} + +} // namespace chrono +} // namespace mgutility + +#endif // MGUTILITY_CHRONO_PARSE_HPP diff --git a/include/mgutility/std/charconv.hpp b/include/mgutility/std/charconv.hpp new file mode 100644 index 0000000..21031e5 --- /dev/null +++ b/include/mgutility/std/charconv.hpp @@ -0,0 +1,104 @@ +/* +MIT License + +Copyright (c) 2024 mguludag + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#ifndef MGUTILITY_STD_CHARCONV_HPP +#define MGUTILITY_STD_CHARCONV_HPP +#include "mgutility/_common/definitions.hpp" +#include + +namespace mgutility { + +#if MGUTILITY_CPLUSPLUS < 201703L + +/** + * @brief Result structure for from_chars function. + */ +struct from_chars_result { + const char + *ptr; ///< Pointer to the character after the last parsed character. + std::errc ec; ///< Error code indicating success or failure. +}; + +/** + * @brief Converts a character to its integer equivalent. + * + * @param c The character to convert. + * @return int The integer value of the character, or -1 if the character is not + * a digit. + */ +constexpr auto char_to_int(char c) noexcept -> int { + return (c >= '0' && c <= '9') ? c - '0' : -1; +} + +/** + * @brief Parses an integer from a character range. + * + * @param first Pointer to the first character of the range. + * @param last Pointer to one past the last character of the range. + * @param value Reference to an integer where the parsed value will be stored. + * @return from_chars_result The result of the parsing operation. + */ +MGUTILITY_CNSTXPR auto from_chars(const char *first, const char *last, + int &value) noexcept -> from_chars_result { + int result = 0; + bool negative = false; + const char *it = first; + + if (it == last) { + return {first, std::errc::invalid_argument}; + } + + if (*it == '-') { + negative = true; + ++it; + if (it == last) { + return {first, std::errc::invalid_argument}; + } + } + + for (; it != last; ++it) { + int digit = char_to_int(*it); + if (digit == -1) { + break; + } + result = result * 10 + digit; + } + + if (it == first || (negative && it == first + 1)) { + return {first, std::errc::invalid_argument}; + } + + value = negative ? -result : result; + return {it, std::errc{}}; +} +#else +#include + +using from_chars_result = std::from_chars_result; +using from_chars = std::from_chars; + +#endif + +} // namespace mgutility + +#endif // MGUTILITY_STD_CHARCONV_HPP \ No newline at end of file diff --git a/include/mgutility/std/string_view.hpp b/include/mgutility/std/string_view.hpp new file mode 100644 index 0000000..a839770 --- /dev/null +++ b/include/mgutility/std/string_view.hpp @@ -0,0 +1,314 @@ +/* +MIT License + +Copyright (c) 2024 mguludag + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef STRING_STRING_VIEW_HPP +#define STRING_STRING_VIEW_HPP + +#include +#include +#include + +#include "mgutility/_common/definitions.hpp" + +namespace mgutility { + +#if MGUTILITY_CPLUSPLUS < 201703L + +namespace detail { +/** + * @brief Computes the length of a C-string at compile-time. + * + * @param str The C-string. + * @param sz The initial size, default is 0. + * @return The length of the C-string. + */ +constexpr auto strlen_constexpr(const char *str, size_t sz = 0) noexcept + -> size_t { + return str[sz] == '\0' ? sz : strlen_constexpr(str, ++sz); +} +} // namespace detail + +/** + * @brief A basic string view class template. + * + * @tparam Char The character type, default is char. + */ +template class basic_string_view { +public: + /** + * @brief Default constructor. + */ + constexpr inline basic_string_view() noexcept : data_(""), size_(0) {} + + /** + * @brief Constructs a basic_string_view from a C-string. + * + * @param str The C-string. + */ + constexpr inline basic_string_view(const Char *str) noexcept + : data_(str), size_(detail::strlen_constexpr(str)) {} + + /** + * @brief Constructs a basic_string_view from a std::string. + * + * @param str The std::string. + */ + constexpr inline basic_string_view( + const std::basic_string &str) noexcept + : data_(str.c_str()), size_(str.size()) {} + + /** + * @brief Constructs a basic_string_view from a C-string and length. + * + * @param str The C-string. + * @param len The length of the string. + */ + constexpr inline basic_string_view(const Char *str, size_t len) noexcept + : data_(str), size_(len) {} + + /** + * @brief Copy constructor. + * + * @param other The other basic_string_view to copy. + */ + constexpr inline basic_string_view(const basic_string_view &other) + : data_(other.data_), size_(other.size_) {} + + /** + * @brief Move constructor. + * + * @param other The other basic_string_view to move. + */ + constexpr inline basic_string_view(basic_string_view &&other) noexcept + : data_(std::move(other.data_)), size_(std::move(other.size_)) {} + + /** + * @brief Copy assignment operator. + * + * @param other The other basic_string_view to copy. + * @return A reference to this object. + */ + MGUTILITY_CNSTXPR inline basic_string_view & + operator=(const basic_string_view &other) noexcept { + data_ = other.data_; + size_ = other.size_; + return *this; + } + + /** + * @brief Move assignment operator. + * + * @param other The other basic_string_view to move. + * @return A reference to this object. + */ + MGUTILITY_CNSTXPR inline basic_string_view & + operator=(basic_string_view &&other) noexcept { + data_ = std::move(other.data_); + size_ = std::move(other.size_); + return *this; + } + + /** + * @brief Accesses the character at the given index. + * + * @param index The index. + * @return The character at the index. + */ + constexpr inline const Char operator[](size_t index) const noexcept { + return data_[index]; + } + + /** + * @brief Returns an iterator to the beginning of the string. + * + * @return A pointer to the first character. + */ + constexpr inline const Char *begin() const noexcept { return data_; } + + /** + * @brief Returns an iterator to the end of the string. + * + * @return A pointer to one past the last character. + */ + constexpr inline const Char *end() const noexcept { return (data_ + size_); } + + /** + * @brief Checks if the string is empty. + * + * @return True if the string is empty, otherwise false. + */ + constexpr inline bool empty() const noexcept { return size_ < 1; } + + /** + * @brief Returns the size of the string. + * + * @return The size of the string. + */ + constexpr inline size_t size() const noexcept { return size_; } + + /** + * @brief Returns a pointer to the underlying data. + * + * @return A pointer to the data. + */ + constexpr inline const Char *data() const noexcept { return data_; } + + /** + * @brief Returns a substring view. + * + * @param begin The starting position. + * @param len The length of the substring. + * @return A basic_string_view representing the substring. + */ + constexpr inline basic_string_view + substr(size_t begin, size_t len = 0U) const noexcept { + return basic_string_view(data_ + begin, + len == 0U ? size_ - begin : len); + } + + /** + * @brief Finds the last occurrence of a character. + * + * @param c The character to find. + * @param pos The position to start from, default is npos. + * @return The position of the character or npos if not found. + */ + constexpr inline size_t rfind(Char c, size_t pos = npos) const noexcept { + return (pos == npos ? pos = size_ : pos = pos), c == data_[pos] ? pos + : pos == 0U + ? npos + : rfind(c, --pos); + } + + /** + * @brief Finds the first occurrence of a character. + * + * @param c The character to find. + * @param pos The position to start from, default is 0. + * @return The position of the character or npos if not found. + */ + constexpr inline size_t find(Char c, size_t pos = 0) const noexcept { + return c == data_[pos] ? pos : pos < size_ ? find(c, ++pos) : npos; + } + + /** + * @brief Equality operator. + * + * @param lhs The left-hand side basic_string_view. + * @param rhs The right-hand side basic_string_view. + * @return True if the strings are equal, otherwise false. + */ + constexpr friend inline bool + operator==(basic_string_view lhs, + basic_string_view rhs) noexcept { + return (lhs.size_ == rhs.size_) && + std::strncmp(lhs.data_, rhs.data_, lhs.size_) == 0; + } + + /** + * @brief Equality operator. + * + * @param lhs The left-hand side basic_string_view. + * @param rhs The right-hand side C-string. + * @return True if the strings are equal, otherwise false. + */ + constexpr friend inline bool operator==(basic_string_view lhs, + const Char *rhs) noexcept { + return (lhs.size_ == detail::strlen_constexpr(rhs)) && + std::strncmp(lhs.data_, rhs, lhs.size_) == 0; + } + + /** + * @brief Inequality operator. + * + * @param lhs The left-hand side basic_string_view. + * @param rhs The right-hand side basic_string_view. + * @return True if the strings are not equal, otherwise false. + */ + constexpr friend inline bool + operator!=(basic_string_view lhs, + basic_string_view rhs) noexcept { + return !(lhs == rhs); + } + + /** + * @brief Inequality operator. + * + * @param lhs The left-hand side basic_string_view. + * @param rhs The right-hand side C-string. + * @return True if the strings are not equal, otherwise false. + */ + constexpr friend inline bool operator!=(basic_string_view lhs, + const Char *rhs) noexcept { + return !(lhs == rhs); + } + + /** + * @brief Converts the string view to an std::string. + * + * @return An std::string representing the same string. + */ + inline operator std::string() { return std::string(data_, size_); } + + /** + * @brief Converts the string view to an std::string (const version). + * + * @return An std::string representing the same string. + */ + inline operator std::string() const { return std::string(data_, size_); } + + /** + * @brief Stream insertion operator. + * + * @param os The output stream. + * @param sv The basic_string_view. + * @return A reference to the output stream. + */ + friend inline std::ostream &operator<<(std::ostream &os, + const basic_string_view &sv) { + for (auto c : sv) { + os << c; + } + return os; + } + + static constexpr auto npos = -1; + +private: + size_t size_; + const Char *data_; +}; + +using string_view = basic_string_view; + +#else +#include + +using string_view = std::string_view; + +#endif + +} // namespace mgutility + +#endif // STRING_STRING_VIEW_HPP \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index 5f7ad2f..0000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(chrono_parse_test VERSION 0.1 LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) - -include_directories(AFTER PUBLIC ${CMAKE_SOURCE_DIR}/include) - -add_executable(${PROJECT_NAME} chrono_parse_test.cpp) -target_link_libraries(${PROJECT_NAME} doctest) - -add_test(NAME chrono_parse_test COMMAND chrono_parse_test) \ No newline at end of file diff --git a/test/chrono_parse_test.cpp b/tests/test_chrono_parse.cpp similarity index 74% rename from test/chrono_parse_test.cpp rename to tests/test_chrono_parse.cpp index 0ca83aa..26cee8f 100644 --- a/test/chrono_parse_test.cpp +++ b/tests/test_chrono_parse.cpp @@ -1,8 +1,8 @@ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "doctest/doctest.h" -#include "chrono_parse.hpp" +#include "mgutility/chrono/parse.hpp" -#if defined(__GNUC__) && !defined(__clang__) +#if defined(__GNUC__) //&& !defined(__clang__) #define FACTOR 1000 #else #define FACTOR 1 @@ -15,5 +15,8 @@ TEST_CASE("testing the iso8601 parsing") { CHECK(mgutility::chrono::parse("{:%FT%T%z}", "2023-04-30T16:22:18-0200").time_since_epoch().count() == (1682878938000000 * FACTOR)); CHECK(mgutility::chrono::parse("{:%FT%T.%f}", "2023-04-30T16:22:18.500").time_since_epoch().count() == (1682871738500000 * FACTOR)); CHECK(mgutility::chrono::parse("{:%FT%T.%f%z}", "2023-04-30T16:22:18.500+0100").time_since_epoch().count() == (1682868138500000 * FACTOR)); + REQUIRE_THROWS(mgutility::chrono::parse("{:%FT%T", "2023-04-30T16:22:18")); + REQUIRE_THROWS(mgutility::chrono::parse("%FT%T}", "2023-04-30T16:22:18")); + REQUIRE_THROWS(mgutility::chrono::parse("{%F %T}", "2023-04-30T16:22:18")); REQUIRE_THROWS(mgutility::chrono::parse("{:%F %T}", "2023-04-30T16:22:18")); } \ No newline at end of file