diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bcc8d3d..f9384e4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,22 @@ jobs: run: cmake --install build --prefix install - name: Test run: ctest --test-dir build + - name: Show CTest last log + if: always() + run: | + echo "=== build/Testing/Temporary/LastTest.log ===" + cat build/Testing/Temporary/LastTest.log || true + - name: Rerun failed tests verbosely + if: always() + run: ctest --test-dir build --rerun-failed --output-on-failure || true + - name: Upload CTest logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: ctest-logs-${{ runner.os }}-cxx${{ matrix.std }} + path: | + build/Testing/Temporary/LastTest.log + build/Testing/** - name: Configure consumer project run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install -DCMAKE_CXX_STANDARD=${{ matrix.std }} - name: Build consumer project @@ -41,7 +57,26 @@ jobs: - name: Install run: cmake --install build --prefix install --config Release - name: Test + shell: bash run: ctest --test-dir build -C Release + - name: Show CTest last log + if: always() + shell: bash + run: | + echo "=== build/Testing/Temporary/LastTest.log ===" + cat build/Testing/Temporary/LastTest.log || true + - name: Rerun failed tests verbosely + if: always() + shell: bash + run: ctest --test-dir build -C Release --rerun-failed --output-on-failure || true + - name: Upload CTest logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: ctest-logs-${{ runner.os }}-cxx${{ matrix.std }} + path: | + build/Testing/Temporary/LastTest.log + build/Testing/** - name: Configure consumer project run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH="${{ github.workspace }}/install" -DCMAKE_CXX_STANDARD=${{ matrix.std }} - name: Build consumer project @@ -62,6 +97,22 @@ jobs: run: cmake --install build --prefix install - name: Test run: ctest --test-dir build + - name: Show CTest last log + if: always() + run: | + echo "=== build/Testing/Temporary/LastTest.log ===" + cat build/Testing/Temporary/LastTest.log || true + - name: Rerun failed tests verbosely + if: always() + run: ctest --test-dir build --rerun-failed --output-on-failure || true + - name: Upload CTest logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: ctest-logs-${{ runner.os }}-cxx${{ matrix.std }} + path: | + build/Testing/Temporary/LastTest.log + build/Testing/** - name: Configure consumer project run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install -DCMAKE_CXX_STANDARD=${{ matrix.std }} - name: Build consumer project diff --git a/AGENTS.md b/AGENTS.md index 6ce67ff2..65087b4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,3 +39,89 @@ - Keep diffs minimal and focused. - Do not refactor or apply style changes beyond the lines you directly touch. + +## Header-only singleton/service storage rule (C++11–C++20) + +Use this rule to avoid ODR issues while keeping a single instance per program. + +### C++17 and newer +Prefer `static inline` storage inside the class: + +```cpp +class Service { +public: + static Service& instance() noexcept { + return s_instance; + } + +private: + Service() = default; + static inline Service s_instance{}; // single instance (C++17+) +}; +``` + +### C++11/14 (header-only is not possible without one TU) +You must define storage in exactly one translation unit (TU) using a macro. + +Header (`Service.hpp`): + +```cpp +#pragma once + +class Service { +public: + static Service& instance() noexcept; + +private: + Service() = default; +#if __cplusplus >= 201703L + static inline Service s_instance{}; // single instance (C++17+) +#endif +}; + +#if __cplusplus >= 201703L + +inline Service& Service::instance() noexcept { + return s_instance; +} + +#else + +namespace detail { +# if defined(SERVICE_DEFINE_STORAGE) + Service g_service; +# else + extern Service g_service; +# endif +} + +inline Service& Service::instance() noexcept { + return detail::g_service; +} + +#endif +``` + +Usage (C++11/14): define the macro in exactly one `.cpp` file (usually `main.cpp`): + +```cpp +#define SERVICE_DEFINE_STORAGE +#include "Service.hpp" +``` + +All other `.cpp` files should include without the macro: + +```cpp +#include "Service.hpp" +``` + +Notes: +- The macro must be defined in exactly one TU. +- If it is not defined anywhere, you will get an undefined reference error. +- If it is defined in multiple TUs, you will get multiple definition errors. +- In C++17+, the macro is unnecessary (but harmless). +- Use `detail` to keep raw storage out of public API. +- If many services exist in C++11/14, prefer a single TU like `project_singletons.cpp` that defines all `*_DEFINE_STORAGE` macros. + +Naming convention for macros: +- `FOO_DEFINE_STORAGE` (or `FOO_IMPLEMENTATION`) — exactly one TU defines it. diff --git a/CHANGELOG.md b/CHANGELOG.md index 86def29f..d5bb5bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to this project will be documented in this file. +## [v1.0.5] - 2025-12-22 +- Added fast date conversion paths for timestamp-to-calendar helpers, along with unchecked timestamp math to reduce validation overhead in hot paths (legacy fallbacks remain for comparison). +- Added reference tests and micro-benchmarks for fast vs legacy date conversions with averaged performance measurements across pre/post-epoch ranges. +- Averaged performance gains (legacy/fast, 5 runs): `to_date_time` ~5.68×, `date_to_unix_day` ~1.25×, `to_timestamp` ~1.14×, `to_timestamp_unchecked` ~1.53× (range-dependent). +- Added a `DateTime` value type with fixed UTC offset storage, parsing, formatting, arithmetic helpers, and examples/tests. +- Added OA date conversions and astronomy helpers (JD/MJD/JDN, lunar phase/age) with docs and examples. +- Added ISO week-date conversions, formatting, and parsing utilities. +- Added geocentric MoonPhase calculator with quarter timings, documentation, and tests. +- Added continuous lunar phase sin/cos helpers, structured quarter instants with event windows, documentation, and tests. +- Split `time_conversions.hpp` into modular headers while keeping the umbrella include, preserving APIs with compatibility aliases and refreshed docs. +- Added short-form weekday and timestamp conversion aliases alongside new constexpr timezone offset helpers. +- Expanded conversion coverage tests to exercise the renamed helpers and new wrappers across C++11/14/17 builds. +- Documented first/last workday boundary helpers and their UTC semantics in README and Doxygen mainpage. +- Renamed `uday_t` to `dse_t` and added `unix_day_t`/`unixday_t` aliases for backward compatibility. +- Corrected unix-day millisecond alias helpers to reuse `days_since_epoch_ms` logic. +- Added workday boundary coverage and alias verification to `time_conversions_test`. +- Introduced UTC offset helpers (`to_utc`/`to_local`, seconds and milliseconds) and TimeZoneStruct offset extraction. +- Expanded time parsing and workday/date conversion APIs: new month/workday inspectors, `unix_day_to_ts*` aliases, `days_since_epoch*`/`min_since_epoch`, and renamed year accessors to `years_since_epoch`/`year_of*`. +- Added POSIX `NtpClient` implementation with Unix test coverage and refreshed NTP documentation. +- Unified `now_realtime_us()` precision across Windows and Unix by combining realtime anchors with monotonic clocks. +- Added MQL5 counterparts for recent workday boundary and ISO parsing helpers. +- Introduced templated NTP client pools, offline/fake testing paths, background runner helpers, and a singleton NTP time service with convenience wrappers. +- Added US Eastern Time (ET/NY) to GMT (UTC) conversion helpers with DST rules. +- Added US Central Time (CT) to GMT (UTC) conversion helpers for America/Chicago. + ## [v1.0.4] - 2025-09-20 - fix ODR violations in headers diff --git a/CMakeLists.txt b/CMakeLists.txt index 923e1872..d696f4c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(TimeShield VERSION 1.0.4 LANGUAGES CXX) +project(TimeShield VERSION 1.0.5 LANGUAGES CXX) if(NOT DEFINED CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 11) @@ -21,11 +21,7 @@ target_include_directories( include(GNUInstallDirs) include(CMakePackageConfigHelpers) -if(WIN32) - set(TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT ON) -else() - set(TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT OFF) -endif() +set(TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT ON) option(TIME_SHIELD_ENABLE_NTP_CLIENT "Enable NTP client" ${TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT}) target_compile_definitions( @@ -110,6 +106,9 @@ if(TIME_SHIELD_CPP_BUILD_TESTS) get_filename_component(test_name ${test_src} NAME_WE) add_executable(${test_name} ${test_src}) target_link_libraries(${test_name} PRIVATE time_shield::time_shield) + if(WIN32) + target_link_libraries(${test_name} PRIVATE ws2_32) + endif() if(COMMON_WARN_FLAGS) target_compile_options(${test_name} PRIVATE ${COMMON_WARN_FLAGS}) endif() diff --git a/MQL5/Include/time_shield.mqh b/MQL5/Include/time_shield.mqh index 7c8b81b4..b6536b0c 100644 --- a/MQL5/Include/time_shield.mqh +++ b/MQL5/Include/time_shield.mqh @@ -37,6 +37,9 @@ // Structure representing date and time combinations #include +// Value-type wrapper for date-time with offset +#include + // Functions for validation of time-related values #include diff --git a/MQL5/Include/time_shield/DateTime.mqh b/MQL5/Include/time_shield/DateTime.mqh new file mode 100644 index 00000000..7db488c3 --- /dev/null +++ b/MQL5/Include/time_shield/DateTime.mqh @@ -0,0 +1,144 @@ +//+------------------------------------------------------------------+ +//| DateTime.mqh | +//| Time Shield - MQL5 DateTime Type | +//| Copyright 2025, NewYaroslav | +//| https://github.com/NewYaroslav/time-shield-cpp | +//+------------------------------------------------------------------+ +#ifndef __TIME_SHIELD_DATE_TIME_MQH__ +#define __TIME_SHIELD_DATE_TIME_MQH__ + +/// \file DateTime.mqh +/// \ingroup mql5 +/// \brief Lightweight date-time wrapper for MQL5 with fixed UTC offset. + +#property copyright "Copyright 2025, NewYaroslav" +#property link "https://github.com/NewYaroslav/time-shield-cpp" +#property strict + +#include "time_conversions.mqh" +#include "time_formatting.mqh" +#include "time_parser.mqh" +#include "time_zone_struct.mqh" +#include "validation.mqh" + +namespace time_shield { + + /// \brief Represents a UTC timestamp with an optional fixed offset. + class DateTime { + public: + /// \brief Default constructor initializes epoch with zero offset. + DateTime(): m_utc_ms(0), m_offset(0) {} + + /// \brief Create instance from UTC milliseconds. + /// \param utc_ms Timestamp in milliseconds since the Unix epoch (UTC). + /// \param offset Fixed UTC offset in seconds. + /// \return Constructed DateTime. + static DateTime from_unix_ms(const long utc_ms, const int offset = 0) { + return DateTime(utc_ms, offset); + } + + /// \brief Construct instance for current UTC time. + /// \param offset Fixed UTC offset in seconds. + /// \return DateTime set to now. + static DateTime now_utc(const int offset = 0) { + return DateTime(ts_ms(), offset); + } + + /// \brief Try to build from calendar components interpreted in provided offset. + /// \param year Year component. + /// \param month Month component. + /// \param day Day component. + /// \param hour Hour component. + /// \param min Minute component. + /// \param sec Second component. + /// \param ms Millisecond component. + /// \param offset Fixed UTC offset in seconds. + /// \param out Output DateTime on success. + /// \return True when components form a valid date-time and offset. + static bool try_from_components( + const long year, + const int month, + const int day, + const int hour, + const int min, + const int sec, + const int ms, + const int offset, + DateTime &out) { + if (!is_valid_date_time(year, month, day, hour, min, sec, ms)) + return false; + + TimeZoneStruct tz = to_time_zone_struct(offset); + if (!is_valid_time_zone_offset(tz)) + return false; + + const long local_ms = to_timestamp_ms(year, month, day, hour, min, sec, ms); + out = DateTime(local_ms - offset_to_ms(offset), offset); + return true; + } + + /// \brief Try to build from DateTimeStruct interpreted in provided offset. + /// \param local_dt Local date-time structure. + /// \param offset Fixed UTC offset in seconds. + /// \param out Output DateTime on success. + /// \return True when structure and offset are valid. + static bool try_from_date_time_struct( + const DateTimeStruct &local_dt, + const int offset, + DateTime &out) { + if (!is_valid_date_time(local_dt)) + return false; + + TimeZoneStruct tz = to_time_zone_struct(offset); + if (!is_valid_time_zone_offset(tz)) + return false; + + const long local_ms = dt_to_timestamp_ms(local_dt); + out = DateTime(local_ms - offset_to_ms(offset), offset); + return true; + } + + /// \brief Try to parse ISO8601 string to DateTime. + /// \param str Input ISO8601 string. + /// \param out Output DateTime when parsing succeeds. + /// \return True on success. + static bool try_parse_iso8601(const string str, DateTime &out) { + DateTimeStruct dt = create_date_time_struct(0); + TimeZoneStruct tz = create_time_zone_struct(0, 0, true); + if (!parse_iso8601(str, dt, tz)) + return false; + + out = DateTime(dt_to_timestamp_ms(dt) - offset_to_ms(time_zone_struct_to_offset(tz)), + time_zone_struct_to_offset(tz)); + return true; + } + + /// \brief Format to ISO8601 string with stored offset. + string to_iso8601() const { return to_iso8601_ms(local_ms(), m_offset); } + + /// \brief Access UTC milliseconds. + long unix_ms() const { return m_utc_ms; } + + /// \brief Access stored UTC offset. + int utc_offset() const { return m_offset; } + + /// \brief Return copy with new offset preserving instant. + DateTime with_offset(const int new_offset) const { return DateTime(m_utc_ms, new_offset); } + + /// \brief Return copy with zero offset. + DateTime to_utc() const { return with_offset(0); } + + private: + DateTime(const long utc_ms, const int offset): m_utc_ms(utc_ms), m_offset(offset) {} + + static long offset_to_ms(const int offset) { return (long)offset * MS_PER_SEC; } + + long local_ms() const { return m_utc_ms + offset_to_ms(m_offset); } + + long m_utc_ms; + int m_offset; + }; + +} // namespace time_shield + +#endif // __TIME_SHIELD_DATE_TIME_MQH__ diff --git a/MQL5/Include/time_shield/time_conversions.mqh b/MQL5/Include/time_shield/time_conversions.mqh index 7db0c686..6cb73cf3 100644 --- a/MQL5/Include/time_shield/time_conversions.mqh +++ b/MQL5/Include/time_shield/time_conversions.mqh @@ -826,6 +826,248 @@ double sec_to_fhour(long sec) { /// \copydoc num_days_in_month_ts int days_in_month(long ts) { return num_days_in_month_ts(ts); } + //---------------------------------------------------------------------- + // Workday boundary helpers + //---------------------------------------------------------------------- + + int first_workday_day(long year, int month) { + int days = num_days_in_month(year, month); + if(days <= 0) return 0; + for(int day = 1; day <= days; ++day) { + if(is_workday(year, month, day)) return day; + } + return 0; + } + + int last_workday_day(long year, int month) { + int days = num_days_in_month(year, month); + if(days <= 0) return 0; + for(int day = days; day >= 1; --day) { + if(is_workday(year, month, day)) return day; + } + return 0; + } + + int count_workdays_in_month(long year, int month) { + int days = num_days_in_month(year, month); + if(days <= 0) return 0; + int total = 0; + for(int day = 1; day <= days; ++day) { + if(is_workday(year, month, day)) ++total; + } + return total; + } + + /// \brief Get start timestamp of the first workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 00:00:00 of the first workday or ERROR_TIMESTAMP. + long start_of_first_workday_month(long year, int month) { + int day = first_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return to_timestamp(year, month, day); + } + + /// \brief Get start timestamp in milliseconds of the first workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 00:00:00.000 of the first workday or ERROR_TIMESTAMP. + long start_of_first_workday_month_ms(long year, int month) { + int day = first_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return sec_to_ms(to_timestamp(year, month, day)); + } + + /// \brief Get start timestamp of the first workday of the month containing timestamp. + /// \param ts Timestamp in seconds. + /// \return Timestamp at 00:00:00 of the first workday or ERROR_TIMESTAMP. + long start_of_first_workday_month(long ts) { + return start_of_first_workday_month(get_year(ts), (int)month_of_year(ts)); + } + + /// \brief Get start timestamp in milliseconds of the first workday of the month containing timestamp. + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at 00:00:00.000 of the first workday or ERROR_TIMESTAMP. + long start_of_first_workday_month_ms(long ts_ms) { + return start_of_first_workday_month_ms(get_year_ms(ts_ms), (int)month_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Get end timestamp of the first workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 23:59:59 of the first workday or ERROR_TIMESTAMP. + long end_of_first_workday_month(long year, int month) { + int day = first_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return end_of_day(to_timestamp(year, month, day)); + } + + /// \brief Get end timestamp in milliseconds of the first workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 23:59:59.999 of the first workday or ERROR_TIMESTAMP. + long end_of_first_workday_month_ms(long year, int month) { + int day = first_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return end_of_day_ms(sec_to_ms(to_timestamp(year, month, day))); + } + + /// \brief Get end timestamp of the first workday of the month containing timestamp. + /// \param ts Timestamp in seconds. + /// \return Timestamp at 23:59:59 of the first workday or ERROR_TIMESTAMP. + long end_of_first_workday_month(long ts) { + return end_of_first_workday_month(get_year(ts), (int)month_of_year(ts)); + } + + /// \brief Get end timestamp in milliseconds of the first workday of the month containing timestamp. + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at 23:59:59.999 of the first workday or ERROR_TIMESTAMP. + long end_of_first_workday_month_ms(long ts_ms) { + return end_of_first_workday_month_ms(get_year_ms(ts_ms), (int)month_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Get start timestamp of the last workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 00:00:00 of the last workday or ERROR_TIMESTAMP. + long start_of_last_workday_month(long year, int month) { + int day = last_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return to_timestamp(year, month, day); + } + + /// \brief Get start timestamp in milliseconds of the last workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 00:00:00.000 of the last workday or ERROR_TIMESTAMP. + long start_of_last_workday_month_ms(long year, int month) { + int day = last_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return sec_to_ms(to_timestamp(year, month, day)); + } + + /// \brief Get start timestamp of the last workday of the month containing timestamp. + /// \param ts Timestamp in seconds. + /// \return Timestamp at 00:00:00 of the last workday or ERROR_TIMESTAMP. + long start_of_last_workday_month(long ts) { + return start_of_last_workday_month(get_year(ts), (int)month_of_year(ts)); + } + + /// \brief Get start timestamp in milliseconds of the last workday of the month containing timestamp. + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at 00:00:00.000 of the last workday or ERROR_TIMESTAMP. + long start_of_last_workday_month_ms(long ts_ms) { + return start_of_last_workday_month_ms(get_year_ms(ts_ms), (int)month_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Get end timestamp of the last workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 23:59:59 of the last workday or ERROR_TIMESTAMP. + long end_of_last_workday_month(long year, int month) { + int day = last_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return end_of_day(to_timestamp(year, month, day)); + } + + /// \brief Get end timestamp in milliseconds of the last workday of a month. + /// \param year Year value. + /// \param month Month value. + /// \return Timestamp at 23:59:59.999 of the last workday or ERROR_TIMESTAMP. + long end_of_last_workday_month_ms(long year, int month) { + int day = last_workday_day(year, month); + if(day <= 0) return ERROR_TIMESTAMP; + return end_of_day_ms(sec_to_ms(to_timestamp(year, month, day))); + } + + /// \brief Get end timestamp of the last workday of the month containing timestamp. + /// \param ts Timestamp in seconds. + /// \return Timestamp at 23:59:59 of the last workday or ERROR_TIMESTAMP. + long end_of_last_workday_month(long ts) { + return end_of_last_workday_month(get_year(ts), (int)month_of_year(ts)); + } + + /// \brief Get end timestamp in milliseconds of the last workday of the month containing timestamp. + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at 23:59:59.999 of the last workday or ERROR_TIMESTAMP. + long end_of_last_workday_month_ms(long ts_ms) { + return end_of_last_workday_month_ms(get_year_ms(ts_ms), (int)month_of_year(ms_to_sec(ts_ms))); + } + + int workday_index_in_month(long year, int month, int day) { + if(!is_workday(year, month, day)) return 0; + int days = num_days_in_month(year, month); + if(days <= 0) return 0; + int index = 0; + for(int current = 1; current <= days; ++current) { + if(is_workday(year, month, current)) { + ++index; + if(current == day) return index; + } + } + return 0; + } + + //---------------------------------------------------------------------- + // Workday boundary predicates + //---------------------------------------------------------------------- + + bool is_first_workday_of_month(long year, int month, int day) { + return is_workday(year, month, day) && first_workday_day(year, month) == day; + } + + bool is_within_first_workdays_of_month(long year, int month, int day, int count) { + if(count <= 0) return false; + int total = count_workdays_in_month(year, month); + if(count > total) return false; + int index = workday_index_in_month(year, month, day); + return index > 0 && index <= count; + } + + bool is_last_workday_of_month(long year, int month, int day) { + return is_workday(year, month, day) && last_workday_day(year, month) == day; + } + + bool is_within_last_workdays_of_month(long year, int month, int day, int count) { + if(count <= 0) return false; + int total = count_workdays_in_month(year, month); + if(count > total) return false; + int index = workday_index_in_month(year, month, day); + return index > 0 && index >= (total - count + 1); + } + + bool is_first_workday_of_month(const long ts) { + return is_first_workday_of_month(get_year(ts), (int)month_of_year(ts), day_of_month(ts)); + } + + bool is_within_first_workdays_of_month(const long ts, int count) { + return is_within_first_workdays_of_month(get_year(ts), (int)month_of_year(ts), day_of_month(ts), count); + } + + bool is_last_workday_of_month(const long ts) { + return is_last_workday_of_month(get_year(ts), (int)month_of_year(ts), day_of_month(ts)); + } + + bool is_within_last_workdays_of_month(const long ts, int count) { + return is_within_last_workdays_of_month(get_year(ts), (int)month_of_year(ts), day_of_month(ts), count); + } + + bool is_first_workday_of_month_ms(const long ts_ms) { + return is_first_workday_of_month(ms_to_sec(ts_ms)); + } + + bool is_within_first_workdays_of_month_ms(const long ts_ms, int count) { + return is_within_first_workdays_of_month(ms_to_sec(ts_ms), count); + } + + bool is_last_workday_of_month_ms(const long ts_ms) { + return is_last_workday_of_month(ms_to_sec(ts_ms)); + } + + bool is_within_last_workdays_of_month_ms(const long ts_ms, int count) { + return is_within_last_workdays_of_month(ms_to_sec(ts_ms), count); + } + /// \brief Get number of days in a year. /// \param year Year value. /// \return Days in the year. @@ -1066,6 +1308,21 @@ double sec_to_fhour(long sec) { // UNIX day and minute helpers //---------------------------------------------------------------------- + /// \brief Convert calendar date to UNIX day. + /// \param year Year component. + /// \param month Month component. + /// \param day Day component. + /// \return Number of days since UNIX epoch. + long date_to_unix_day(const long year, const int month, const int day) { + const long adj_y = year - (month <= 2 ? 1 : 0); + const long adj_m = month <= 2 ? month + 9 : month - 3; + const long era = (adj_y >= 0 ? adj_y : adj_y - 399) / 400; + const long yoe = adj_y - era * 400; + const long doy = (153 * adj_m + 2) / 5 + day - 1; + const long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; + } + /// \brief Get UNIX day from timestamp. /// \param ts Timestamp in seconds. /// \return Number of days since UNIX epoch. diff --git a/MQL5/Include/time_shield/time_parser.mqh b/MQL5/Include/time_shield/time_parser.mqh index 610c2c1c..682838f2 100644 --- a/MQL5/Include/time_shield/time_parser.mqh +++ b/MQL5/Include/time_shield/time_parser.mqh @@ -138,6 +138,9 @@ namespace time_shield { dt.mon =(int)StringToInteger(parts[1]); dt.day =(int)StringToInteger(parts[2]); + if(!is_valid_date(dt)) + return false; + if (StringLen(time_part)>0) { string tz_str = ""; int zpos = StringFind(time_part,"Z"); @@ -194,6 +197,198 @@ namespace time_shield { return true; } + /// \brief Parse ISO8601 string and check for workday using second precision. + /// \param str ISO8601 formatted string. + /// \return true when parsed timestamp is Monday through Friday. + bool is_workday(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return false; + return is_workday(ts); + } + + /// \brief Alias for is_workday. + /// \copydoc is_workday(string) + bool workday(string str) { return is_workday(str); } + + /// \brief Parse ISO8601 string and check for workday using millisecond precision. + /// \param str ISO8601 formatted string. + /// \return true when parsed timestamp is Monday through Friday. + bool is_workday_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return false; + return is_workday_ms(ts); + } + + /// \brief Alias for is_workday_ms. + /// \copydoc is_workday_ms(string) + bool workday_ms(string str) { return is_workday_ms(str); } + + /// \brief Parse ISO8601 string and check for the first workday of the month using second precision. + /// \param str ISO8601 formatted string. + /// \return true when the parsed timestamp is the first workday of its month. + bool is_first_workday_of_month(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return false; + return is_first_workday_of_month(ts); + } + + /// \brief Parse ISO8601 string and check for the first workday of the month using millisecond precision. + /// \param str ISO8601 formatted string. + /// \return true when the parsed timestamp is the first workday of its month. + bool is_first_workday_of_month_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return false; + return is_first_workday_of_month_ms(ts); + } + + /// \brief Parse ISO8601 string and check if it falls within the first N workdays of the month using second precision. + /// \param str ISO8601 formatted string. + /// \param count Number of leading workdays to test. + /// \return true when the parsed timestamp is within the requested range. + bool is_within_first_workdays_of_month(string str, int count) { + long ts = 0; + if(!str_to_ts(str, ts)) + return false; + return is_within_first_workdays_of_month(ts, count); + } + + /// \brief Parse ISO8601 string and check if it falls within the first N workdays of the month using millisecond precision. + /// \param str ISO8601 formatted string. + /// \param count Number of leading workdays to test. + /// \return true when the parsed timestamp is within the requested range. + bool is_within_first_workdays_of_month_ms(string str, int count) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return false; + return is_within_first_workdays_of_month_ms(ts, count); + } + + /// \brief Parse ISO8601 string and check for the last workday of the month using second precision. + /// \param str ISO8601 formatted string. + /// \return true when the parsed timestamp is the last workday of its month. + bool is_last_workday_of_month(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return false; + return is_last_workday_of_month(ts); + } + + /// \brief Parse ISO8601 string and check for the last workday of the month using millisecond precision. + /// \param str ISO8601 formatted string. + /// \return true when the parsed timestamp is the last workday of its month. + bool is_last_workday_of_month_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return false; + return is_last_workday_of_month_ms(ts); + } + + /// \brief Parse ISO8601 string and check if it falls within the last N workdays of the month using second precision. + /// \param str ISO8601 formatted string. + /// \param count Number of trailing workdays to test. + /// \return true when the parsed timestamp is within the requested range. + bool is_within_last_workdays_of_month(string str, int count) { + long ts = 0; + if(!str_to_ts(str, ts)) + return false; + return is_within_last_workdays_of_month(ts, count); + } + + /// \brief Parse ISO8601 string and check if it falls within the last N workdays of the month using millisecond precision. + /// \param str ISO8601 formatted string. + /// \param count Number of trailing workdays to test. + /// \return true when the parsed timestamp is within the requested range. + bool is_within_last_workdays_of_month_ms(string str, int count) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return false; + return is_within_last_workdays_of_month_ms(ts, count); + } + + /// \brief Parse ISO8601 string and return start of the first workday of that month in seconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 00:00:00 of the first workday or ERROR_TIMESTAMP on failure. + long start_of_first_workday_month(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return ERROR_TIMESTAMP; + return start_of_first_workday_month(ts); + } + + /// \brief Parse ISO8601 string and return start of the first workday of that month in milliseconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 00:00:00.000 of the first workday or ERROR_TIMESTAMP on failure. + long start_of_first_workday_month_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return ERROR_TIMESTAMP; + return start_of_first_workday_month_ms(ts); + } + + /// \brief Parse ISO8601 string and return end of the first workday of that month in seconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 23:59:59 of the first workday or ERROR_TIMESTAMP on failure. + long end_of_first_workday_month(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return ERROR_TIMESTAMP; + return end_of_first_workday_month(ts); + } + + /// \brief Parse ISO8601 string and return end of the first workday of that month in milliseconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 23:59:59.999 of the first workday or ERROR_TIMESTAMP on failure. + long end_of_first_workday_month_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return ERROR_TIMESTAMP; + return end_of_first_workday_month_ms(ts); + } + + /// \brief Parse ISO8601 string and return start of the last workday of that month in seconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 00:00:00 of the last workday or ERROR_TIMESTAMP on failure. + long start_of_last_workday_month(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return ERROR_TIMESTAMP; + return start_of_last_workday_month(ts); + } + + /// \brief Parse ISO8601 string and return start of the last workday of that month in milliseconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 00:00:00.000 of the last workday or ERROR_TIMESTAMP on failure. + long start_of_last_workday_month_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return ERROR_TIMESTAMP; + return start_of_last_workday_month_ms(ts); + } + + /// \brief Parse ISO8601 string and return end of the last workday of that month in seconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 23:59:59 of the last workday or ERROR_TIMESTAMP on failure. + long end_of_last_workday_month(string str) { + long ts = 0; + if(!str_to_ts(str, ts)) + return ERROR_TIMESTAMP; + return end_of_last_workday_month(ts); + } + + /// \brief Parse ISO8601 string and return end of the last workday of that month in milliseconds. + /// \param str ISO8601 formatted string. + /// \return Timestamp at 23:59:59.999 of the last workday or ERROR_TIMESTAMP on failure. + long end_of_last_workday_month_ms(string str) { + long ts = 0; + if(!str_to_ts_ms(str, ts)) + return ERROR_TIMESTAMP; + return end_of_last_workday_month_ms(ts); + } + /// \brief Convert an ISO8601 string to a floating-point timestamp (fts_t). /// \param str The ISO8601 string. /// \param ts The floating-point timestamp to be filled. diff --git a/MQL5/Include/time_shield/validation.mqh b/MQL5/Include/time_shield/validation.mqh index 1324b474..d454d49b 100644 --- a/MQL5/Include/time_shield/validation.mqh +++ b/MQL5/Include/time_shield/validation.mqh @@ -158,6 +158,7 @@ namespace time_shield { bool is_valid_date(const long year, const int month, const int day) { if (day > 31 && year <= 31) return is_valid_date((long)day, month, (int)year); + if (year < MIN_YEAR) return false; if (year > MAX_YEAR) return false; if (month < 1 || month > 12) return false; if (day < 1 || day > 31) return false; @@ -246,6 +247,58 @@ namespace time_shield { return is_day_off_unix_day(unix_day); } + /// \brief Check if the timestamp corresponds to a workday. + /// \param ts Timestamp in seconds. + /// \return true when the date is Monday through Friday. + bool is_workday(const long ts) { + return !is_day_off(ts); + } + + /// \brief Alias for is_workday. + /// \copydoc is_workday(const long) + bool workday(const long ts) { + return is_workday(ts); + } + + /// \brief Check if the millisecond timestamp corresponds to a workday. + /// \param ts_ms Timestamp in milliseconds. + /// \return true when the date is Monday through Friday. + bool is_workday_ms(const long ts_ms) { + return is_workday(ts_ms / MS_PER_SEC); + } + + /// \brief Alias for is_workday_ms. + /// \copydoc is_workday_ms(const long) + bool workday_ms(const long ts_ms) { + return is_workday_ms(ts_ms); + } + + /// \brief Check if the provided date represents a workday. + /// \param year Year component. + /// \param month Month component. + /// \param day Day component. + /// \return true when the date is valid and is Monday through Friday. + bool is_workday(const long year, const int month, const int day) { + if (!is_valid_date(year, month, day)) + return false; + + const long adj_y = year - (month <= 2 ? 1 : 0); + const long adj_m = month <= 2 ? month + 9 : month - 3; + const long era = (adj_y >= 0 ? adj_y : adj_y - 399) / 400; + const long yoe = adj_y - era * 400; + const long doy = (153 * adj_m + 2) / 5 + day - 1; + const long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + const long unix_day = era * 146097 + doe - 719468; + + return !is_day_off_unix_day(unix_day); + } + + /// \brief Alias for is_workday. + /// \copydoc is_workday(const long, const int, const int) + bool workday(const long year, const int month, const int day) { + return is_workday(year, month, day); + } + /// \} }; // namespace time_shield diff --git a/README-RU.md b/README-RU.md index 4714e29f..bd7256c8 100644 --- a/README-RU.md +++ b/README-RU.md @@ -3,7 +3,7 @@ Логотип ![MIT License](https://img.shields.io/badge/license-MIT-green.svg) -![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20MQL5-blue) +![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20MQL5-blue) ![C++ Standard](https://img.shields.io/badge/C++-11--17-orange) ![CI Windows](https://img.shields.io/github/actions/workflow/status/newyaroslav/time-shield-cpp/ci.yml?branch=main&label=Windows&logo=windows) ![CI Linux](https://img.shields.io/github/actions/workflow/status/newyaroslav/time-shield-cpp/ci.yml?branch=main&label=Linux&logo=linux) @@ -12,26 +12,46 @@ **Time Shield** — это header-only C++-библиотека для работы со временем. Она включает функции для конвертации временных значений, форматирования дат и множество утилит для задач с таймстампами. > Названа в честь «временного щита» Хомуры Акэми. +## Быстрый старт + +```cpp +#include + +using namespace time_shield; + +ts_t now = ts(); +std::string iso = to_iso8601(now); +ts_t gmt = cet_to_gmt(now); +bool monday = is_workday(now); +``` + +Используйте `#include ` для полного API или подключайте +отдельные заголовки для минимальной сборки. + ## Зачем Time Shield? **Time Shield** создавался как практичный инструмент для работы с временем в C++, ориентированный на прикладные и инженерные задачи. В отличие от стандартной `std::chrono` или более академичных решений вроде `HowardHinnant/date`, библиотека: - использует простые типы (`int64_t`, `double`) для представления времени (`ts_t`, `fts_t`) — их легко логировать, сериализовать и передавать через JSON, RPC и базы данных; без перегруженных классов `std::chrono`; - поддерживает **разные временные представления** — Unix‑время, дробные секунды, милли- и микросекунды, OLE Automation (Excel), джулианские даты; -- включает **утилиты для округления, форматирования, парсинга ISO8601**, работы с частями времени и вычисления границ периодов; +- включает **утилиты для округления, форматирования, парсинга ISO 8601**, работы с частями времени и вычисления границ периодов; - имеет **расширяемую архитектуру** — новые форматы (Julian, OLE, UTC-offset) добавляются как отдельные типы и модули; -- **работает даже в ограниченных средах**, таких как MQL5/MetaTrader — не требует исключений, STL-контейнеров или динамической памяти; +- есть адаптированные заголовки для MQL5/MetaTrader в каталоге `MQL5`, покрывающие ключевые части API; - поставляется как **header-only** — подключается одной строкой, без сборки и внешних зависимостей; -- использует только **стандартные заголовки STL и системные API**; модули, зависящие от WinAPI (например, `NtpClient`), изолированы и не мешают кроссплатформенной сборке. +- использует только **стандартные заголовки STL и системные API**; платформенные модули (например, сокетный `NtpClient`) изолированы и не мешают кроссплатформенной сборке. ## Возможности -- **Проверка дат** — валидация дат с учётом високосных лет и выходных. +- **Календарные и датовые хелперы** — валидация, будни/выходные/рабочие дни (включая строки ISO 8601). - **Форматирование времени** — преобразование таймстампов в строки по стандартным и пользовательским шаблонам. -- **Конвертации** — перевод между секундными, миллисекундными и плавающими представлениями времени, структуры `DateTimeStruct` и временные зоны. +- **Конвертации** — перевод между секундными, миллисекундными и плавающими представлениями времени, структуры `DateTimeStruct`, OLE Automation (Excel) и временные зоны. +- **Быстрые конвертации дат** — часть функций `timestamp -> календарь` использует ускоренный алгоритм, вдохновлённый https://www.benjoffe.com/fast-date-64 и реализованный с нуля. +- **Тип `DateTime`** — обёртка, хранящая UTC миллисекунды и фиксированное смещение, поддерживает ISO 8601, локальные/UTC компоненты и арифметику. +- **ISO week date** — конвертация, форматирование и парсинг ISO 8601 для недельного счёта. +- **Астрономические утилиты** — расчёт Julian Date/MJD/JDN и оценка лунной фазы/возраста по Unix‑времени. - **Утилиты** — получение текущих меток времени, вычисления начала/конца периодов, работа с частями секунды. -- **Преобразование часовых поясов** — функции для CET/EET в GMT. -- **NTP‑клиент** — получение точного времени по сети (только Windows). +- **Преобразование часовых поясов** — функции для CET/EET/ET/CT в GMT. +- **NTP‑клиент и пул** — одиночные запросы и конфигурируемый пул/раннер/сервис с возможностью офлайн‑тестов (Windows и Unix). - **Поддержка MQL5** — адаптированные заголовки в каталоге `MQL5` позволяют использовать библиотеку в MetaTrader. - Совместимость с `C++11` – `C++17`. @@ -41,11 +61,28 @@ - `TIME_SHIELD_PLATFORM_WINDOWS` / `TIME_SHIELD_PLATFORM_UNIX` — определение целевой платформы. - `TIME_SHIELD_HAS_WINSOCK` — наличие WinSock API. -- `TIME_SHIELD_ENABLE_NTP_CLIENT` — включает модуль `NtpClient` (по умолчанию `1` на Windows). +- `TIME_SHIELD_ENABLE_NTP_CLIENT` — включает модуль `NtpClient` (по умолчанию `1` на поддерживаемых платформах). Все заголовки библиотеки используют пространство имён `time_shield`. Для доступа к API можно писать `time_shield::` или подключать `using namespace time_shield;`. -> Часть функций зависит от WinAPI и будет работать только под Windows (например, `NtpClient` или получение realtime через `QueryPerformanceCounter`). +> Часть функций зависит от системных API и может быть ограничена платформой (например, получение realtime через `QueryPerformanceCounter` под Windows). + +## Инварианты API + +- `ts_t` — Unix-время в секундах (signed 64-bit). Представляет целые секунды. +- `ts_ms_t` / `ts_us_t` — Unix-время в милли/микросекундах (signed 64-bit). +- `fts_t` — Unix-время в секундах как `double`. Точность дробной части зависит от + величины значения; около современной эпохи обычно сохраняет микросекунды, на + очень больших |ts| младшие разряды могут теряться. +- `year_t` — signed 64-bit год. +- `dse_t` / `unix_day_t` / `unixday_t` — число суток с 1970-01-01 (signed 64-bit), + подходит для дат до эпохи. +- ISO 8601 утилиты используют пролептический григорианский календарь и не учитывают + високосные секунды. +- Базовые конверсии и “горячие” функции ориентированы на `noexcept` и отсутствие + динамических аллокаций; строковые/парсинговые и часть высокоуровневых хелперов + могут выделять память и/или бросать исключения (это указано в документации + конкретных функций). ## Установка и настройка @@ -84,7 +121,7 @@ std::string mql5 = to_mql5_date_time(now); // 2024.06.21 12:00:00 std::string filename = to_windows_filename(now); ``` -### Парсинг ISO8601 +### Парсинг ISO 8601 ```cpp #include @@ -96,6 +133,84 @@ if (parse_iso8601("2024-11-25T14:30:00-05:30", dt, tz)) { } ``` +### Класс DateTime + +`DateTime` хранит UTC миллисекунды и фиксированный offset, что позволяет +сохранять смещение при форматировании и получать локальные компоненты. + +```cpp +#include + +using namespace time_shield; + +DateTime dt = DateTime::parse_iso8601("2025-12-16T10:20:30.123+02:30"); +std::string local = dt.to_iso8601(); // сохраняет +02:30 +std::string utc = dt.to_iso8601_utc(); // выводит Z +DateTime tomorrow = dt.add_days(1).start_of_day(); +int hour_local = dt.hour(); // локальный час с учётом offset +int hour_utc = dt.utc_hour(); // час в UTC +``` + +### Преобразование дат OLE Automation (OA) + +Преобразования OA совместимы с Excel/COM (базовая дата 1899-12-30), выполняются в UTC и сохраняют округление к нулю, характерное для серийных OA дат. + +```cpp +#include + +using namespace time_shield; + +oadate_t oa = ts_to_oadate(1714608000); // 2024-05-02 00:00:00Z +ts_t ts_from_oa = oadate_to_ts(oa); // преобразование обратно в Unix-время +oadate_t from_parts = to_oadate(2024, Month::MAY, 2, 12, 0); // 2024-05-02 12:00:00Z +``` + +### Джулианские даты и лунные вычисления + +Астрономические хелперы ориентированы на аналитические оценки (JD, MJD, JDN, фазу и возраст Луны), а не на высокоточные эфемериды. + +```cpp +#include + +using namespace time_shield; + +jd_t jd = ts_to_jd(1714608000); // Julian Date для переданного таймстампа +mjd_t mjd = ts_to_mjd(1714608000); // Modified Julian Date +jdn_t jdn = gregorian_to_jdn(2, 5, 2024); // Julian Day Number (целое значение) +double phase = moon_phase(fts()); // фаза Луны [0..1) +double age_days = moon_age_days(fts()); // примерный возраст Луны в днях +MoonPhaseSineCosine signal = moon_phase_sincos(fts()); // sin/cos фазового угла без скачка в 0/1 +MoonQuarterInstants quarters = moon_quarters(fts()); // ближайшие четверти (Unix секунды в double) +bool is_near_new = is_new_moon_window(fts()); // попадание в окно новолуния +/-12ч +``` + +### Геоцентрический калькулятор фаз Луны + +`MoonPhaseCalculator` (`time_shield::astronomy::MoonPhase`) выдаёт расширенный набор показателей (освещённость, угловые диаметры, расстояние, фазовый угол), sin/cos для непрерывного фазового сигнала, моменты четвертей и проверки «окон» вокруг фаз. Текущая математика геоцентрическая: положения Солнца/Луны рассчитываются без топоцентрических поправок. Поэтому освещённость и фазовый угол — «глобальные» в данный момент времени, а локально отличается: + +- дата/время из-за часового пояса, +- ориентация освещённой части (в южном полушарии картинка отражена), +- наблюдаемость (первый серп, высота над горизонтом, атмосфера). + +> Текущая математика — геоцентрическая (относительно центра Земли), без топоцентрических поправок/параллакса. Освещённость/фазовый угол глобальны для Земли как целого. По месту реально меняются: +> - локальная дата/время (часовой пояс), +> - ориентация освещённой части (в северном/южном полушарии «картинка» перевёрнута), +> - видимость (первый серп/наблюдаемость) — уже про атмосферу/высоту над горизонтом и т.п. + +```cpp +#include + +using namespace time_shield; + +MoonPhaseCalculator calculator{}; +const double ts = 1704067200.0; // 2024-01-01T00:00:00Z +MoonPhaseResult res = calculator.compute(ts); +MoonPhase::quarters_unix_s_t quarters = calculator.quarter_times_unix(ts); // Unix секунды (double) +MoonQuarterInstants around = moon_quarters(ts); +MoonPhaseSineCosine signal = moon_phase_sincos(ts); +bool is_new = calculator.is_new_moon_window(ts); // по умолчанию окно +/-12ч +``` + ### Конвертация часовых поясов ```cpp @@ -105,23 +220,35 @@ ts_t cet = to_ts(2024, Month::JUN, 21, 12, 0, 0); ts_t gmt = cet_to_gmt(cet); ``` -### NTP‑клиент (Windows) +### NTP‑клиент, пул и сервис времени ```cpp -#include +#include +#include -NtpClient client; -if (client.query()) { - int64_t offset = client.get_offset_us(); - int64_t utc_ms = client.get_utc_time_ms(); -} +using namespace time_shield; + +NtpClientPool pool; +pool.set_default_servers(); +pool.measure(); +int64_t pool_offset = pool.offset_us(); + +// Фоновый runner + ленивый сервис через функции-обёртки: +ntp::init(std::chrono::seconds(30)); +int64_t utc_ms = ntp::utc_time_ms(); +int64_t offset_us = ntp::offset_us(); +int64_t utc_sec = ntp::utc_time_sec(); +bool ok = ntp::last_measure_ok(); +uint64_t attempts = ntp::measure_count(); +ntp::shutdown(); ``` ## Документация Полное описание API и дополнительные примеры доступны по адресу: +HTML-документация Doxygen публикуется через GitHub Pages. + ## Лицензия Проект распространяется по лицензии [MIT](LICENSE). - diff --git a/README.md b/README.md index 87824e91..777fa931 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Logo ![MIT License](https://img.shields.io/badge/license-MIT-green.svg) -![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20MQL5-blue) +![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20MQL5-blue) ![C++ Standard](https://img.shields.io/badge/C++-11--17-orange) ![CI Windows](https://img.shields.io/github/actions/workflow/status/newyaroslav/time-shield-cpp/ci.yml?branch=main&label=Windows&logo=windows) ![CI Linux](https://img.shields.io/github/actions/workflow/status/newyaroslav/time-shield-cpp/ci.yml?branch=main&label=Linux&logo=linux) @@ -16,6 +16,22 @@ working with timestamps. See the [Russian version](README-RU.md) for documentation in Russian. +## Quick start + +```cpp +#include + +using namespace time_shield; + +ts_t now = ts(); +std::string iso = to_iso8601(now); +ts_t gmt = cet_to_gmt(now); +bool monday = is_workday(now); +``` + +Use `#include ` for the full API, or include specific headers +for a minimal build. + ## Why Time Shield? **Time Shield** was created as a practical tool for handling time in C++ with a @@ -27,29 +43,39 @@ more academic solutions like `HowardHinnant/date`, the library: no overloaded `std::chrono` classes; - supports **multiple time representations**: Unix time, fractional seconds, milli- and microseconds, OLE Automation (Excel), Julian dates; -- includes **utilities for rounding, formatting, ISO8601 parsing**, working with +- includes **utilities for rounding, formatting, ISO 8601 parsing**, working with parts of a timestamp and calculating period boundaries; - has an **extensible architecture**—new formats (Julian, OLE, UTC offset) can be added as separate types and modules; - **works even in restricted environments** such as MQL5/MetaTrader—no - exceptions, STL containers or dynamic memory are required; + exceptions or dynamic memory are required for the core API; optional helpers + can use `std::string` or throw and can be disabled in restricted builds; - ships as **header-only**—a single include without build steps or external dependencies; -- uses only **standard STL headers and system APIs**; modules that depend on - WinAPI (e.g., `NtpClient`) are isolated and do not hinder cross-platform - builds. +- uses only **standard STL headers and system APIs**; platform-specific modules + (e.g., the socket-based `NtpClient`) are isolated and do not hinder + cross-platform builds. ## Features -- **Date validation**—checks dates for leap years and weekends. +- **Date & calendar helpers**—validation, weekdays/weekends/workdays (including ISO 8601 strings). - **Time formatting**—converts timestamps to strings with standard or custom templates. - **Conversions**—translates between second, millisecond and floating time - representations, `DateTimeStruct` and time zones. + representations, `DateTimeStruct`, OLE Automation dates and time zones. +- **Fast date conversions**—some timestamp-to-calendar helpers use a fast + algorithm inspired by https://www.benjoffe.com/fast-date-64 and implemented from scratch. +- **DateTime value type**—fixed-offset wrapper that stores UTC milliseconds, + parses/prints ISO 8601, exposes local/UTC components, and provides arithmetic + helpers. +- **ISO week dates**—conversion helpers, formatting, and parsing for ISO 8601 + week-numbering years. +- **Astronomy utilities**—computes Julian Date/MJD/JDN values and estimates + lunar phase/age from Unix timestamps. - **Utilities**—fetches current timestamps, computes start/end of periods and works with fractions of a second. -- **Time zone conversion**—functions for CET/EET to GMT. -- **NTP client**—obtains accurate time over the network (Windows only). +- **Time zone conversion**—functions for CET/EET/ET/CT to GMT. +- **NTP client and pool**—single-client queries plus a configurable pool/runner/service pipeline with offline testing hooks (Windows and Unix). - **MQL5 support**—adapted headers in the `MQL5` directory allow using the library in MetaTrader. - Compatible with `C++11`–`C++17`. @@ -64,24 +90,29 @@ library and report platform capabilities: target platform. - `TIME_SHIELD_HAS_WINSOCK` — set when WinSock APIs are available. - `TIME_SHIELD_ENABLE_NTP_CLIENT` — enables the optional `NtpClient` module - (defaults to `1` on Windows). + (defaults to `1` on supported platforms). All public headers place their declarations inside the `time_shield` namespace. Use `time_shield::` or `using namespace time_shield;` to access the API. -> Some functions depend on WinAPI and work only on Windows (for example, -> `NtpClient` or obtaining realtime via `QueryPerformanceCounter`). +> Some functions depend on platform APIs and may be limited (for example, +> obtaining realtime via `QueryPerformanceCounter` on Windows). ## API Invariants -- `ts_t` stores Unix seconds in a signed 64-bit integer and provides - microsecond precision when converted to other types. -- `fts_t` uses double precision floating seconds. Conversions between - representations preserve microsecond accuracy. -- ISO8601 utilities assume the proleptic Gregorian calendar and do not account +- `ts_t` — Unix time in seconds (signed 64-bit). Represents whole seconds. +- `ts_ms_t` / `ts_us_t` — Unix time in milli/microseconds (signed 64-bit). +- `fts_t` — Unix time in seconds as `double`. Fractional precision depends on + magnitude; near the modern epoch it typically preserves microseconds, while + very large |ts| values can lose lower bits. +- `year_t` — signed 64-bit year. +- `dse_t` / `unix_day_t` / `unixday_t` — count of days since 1970-01-01. The + signedness of the type determines correctness for dates before the epoch. +- ISO 8601 utilities use the proleptic Gregorian calendar and do not account for leap seconds. -- Core functions avoid throwing exceptions and do not allocate dynamic memory; - helpers returning `std::string` rely on the caller to manage allocations. +- Core conversions and “hot” functions aim for `noexcept` and no dynamic + allocations; string/parsing routines and some high-level helpers may allocate + and/or throw (as documented per function). ## Versioning @@ -183,7 +214,7 @@ std::string mql5 = to_mql5_date_time(now); // 2024.06.21 12:00:00 std::string filename = to_windows_filename(now); ``` -### ISO8601 parsing +### ISO 8601 parsing ```cpp #include @@ -195,6 +226,115 @@ if (parse_iso8601("2024-11-25T14:30:00-05:30", dt, tz)) { } ``` +### DateTime value type + +`DateTime` stores UTC milliseconds plus an optional fixed offset for local +component access and round-trip formatting. + +```cpp +#include + +using namespace time_shield; + +DateTime dt = DateTime::parse_iso8601("2025-12-16T10:20:30.123+02:30"); +std::string local = dt.to_iso8601(); // preserves +02:30 +std::string utc = dt.to_iso8601_utc(); // prints Z +DateTime tomorrow = dt.add_days(1).start_of_day(); +int hour_local = dt.hour(); // local hour (offset applied) +int hour_utc = dt.utc_hour(); // UTC hour +``` + +### Checking workdays + +```cpp +#include +#include + +using namespace time_shield; + +bool monday = is_workday(1710720000); // seconds timestamp +bool monday_ms = is_workday_ms("2024-03-18T09:00:00.500Z"); // ISO 8601 with milliseconds +bool from_date = is_workday(2024, 3, 18); // year, month, day components +bool from_str = is_workday("2024-03-18T09:00:00Z"); // ISO 8601 string +``` + +The string helpers accept the same ISO 8601 formats as `str_to_ts` / `str_to_ts_ms` and evaluate to `false` when parsing fails or the date falls on a weekend. + +### Locating first and last workdays + +```cpp +#include + +using namespace time_shield; + +ts_t first_open = start_of_first_workday_month(2024, 6); // 2024-06-03T00:00:00Z +ts_t first_close = end_of_first_workday_month(2024, Month::JUN); +ts_t last_open = start_of_last_workday_month(2024, 3); // 2024-03-29T00:00:00Z +ts_ms_t last_close_ms = end_of_last_workday_month_ms(2024, Month::MAR); +``` + +The helpers reuse the `start_of_day` / `end_of_day` semantics and therefore return UTC timestamps. Invalid months or months without workdays produce `ERROR_TIMESTAMP` to simplify downstream validation. + +### OLE Automation (OA) date conversions + +OA conversions are Excel/COM compatible (base date 1899-12-30), operate in UTC, and mirror the round-toward-zero semantics used by OA serials. + +```cpp +#include + +using namespace time_shield; + +oadate_t oa = ts_to_oadate(1714608000); // 2024-05-02 00:00:00Z +ts_t ts_from_oa = oadate_to_ts(oa); // round toward zero +oadate_t from_parts = to_oadate(2024, Month::MAY, 2, 12, 0); // 2024-05-02 12:00:00Z +``` + +### Julian date and lunar helpers + +The astronomy helpers provide lightweight analytics-oriented values (JD, MJD, JDN, phase, age) rather than high-precision ephemerides. + +```cpp +#include + +using namespace time_shield; + +jd_t jd = ts_to_jd(1714608000); // Julian Date for the timestamp +mjd_t mjd = ts_to_mjd(1714608000); // Modified Julian Date +jdn_t jdn = gregorian_to_jdn(2, 5, 2024); // Julian Day Number +double phase = moon_phase(fts()); // lunar phase fraction [0..1) +double age_days = moon_age_days(fts()); // approximate lunar age +MoonPhaseSineCosine signal = moon_phase_sincos(fts()); // sin/cos of the phase angle (continuous) +MoonQuarterInstants quarters = moon_quarters(fts()); // nearest quarter instants (Unix seconds, double) +bool is_near_new = is_new_moon_window(fts()); // inside +/-12h new moon window +``` + +### Geocentric Moon phase calculator + +`MoonPhaseCalculator` (`time_shield::astronomy::MoonPhase`) exposes richer geocentric outputs (illumination, angular diameters, distance, phase angle), sin/cos for a continuous phase signal, quarter instants, and phase event windows. The current math is geocentric (Earth-centered) without topocentric corrections, so phase and illumination are “global” for a given moment. What varies locally: + +- local date/time (timezone conversion), +- visual orientation of the lit part (inverted between hemispheres), +- visibility/observability, which depends on atmosphere and altitude above the horizon. + +> Current math is geocentric (Sun/Moon positions relative to Earth’s center, without topocentric parallax). This means illumination and the phase angle are primarily “global” at a given instant for the Earth as a whole. Locally, what actually differs is: +> - calendar date/time via timezone shifts, +> - apparent orientation of the illuminated side (flipped between northern/southern hemispheres), +> - visibility (e.g., first crescent) driven by atmosphere/horizon/altitude rather than the geocentric phase itself. + +```cpp +#include + +using namespace time_shield; + +MoonPhaseCalculator calculator{}; +const double ts = 1704067200.0; // 2024-01-01T00:00:00Z +MoonPhaseResult res = calculator.compute(ts); +MoonPhase::quarters_unix_s_t quarters = calculator.quarter_times_unix(ts); // Unix seconds (double) +MoonQuarterInstants around = moon_quarters(ts); +MoonPhaseSineCosine signal = moon_phase_sincos(ts); +bool is_new = calculator.is_new_moon_window(ts); // +/-12h window by default +``` + ### Time zone conversion ```cpp @@ -204,16 +344,27 @@ ts_t cet = to_ts(2024, Month::JUN, 21, 12, 0, 0); ts_t gmt = cet_to_gmt(cet); ``` -### NTP client (Windows) +### NTP client, pool, and time service ```cpp -#include +#include +#include -NtpClient client; -if (client.query()) { - int64_t offset = client.get_offset_us(); - int64_t utc_ms = client.get_utc_time_ms(); -} +using namespace time_shield; + +NtpClientPool pool; +pool.set_default_servers(); +pool.measure(); +int64_t pool_offset = pool.offset_us(); + +// Background runner + lazy singleton service via wrapper functions: +ntp::init(std::chrono::seconds(30)); +int64_t utc_ms = ntp::utc_time_ms(); +int64_t offset_us = ntp::offset_us(); +int64_t utc_sec = ntp::utc_time_sec(); +bool ok = ntp::last_measure_ok(); +uint64_t attempts = ntp::measure_count(); +ntp::shutdown(); ``` ## Documentation @@ -221,6 +372,8 @@ if (client.query()) { Full API description and additional examples are available at +Doxygen HTML is published via GitHub Pages. + ## License The project is distributed under the [MIT](LICENSE) license. diff --git a/docs/groups.dox b/docs/groups.dox index f442d1aa..8b2a4657 100644 --- a/docs/groups.dox +++ b/docs/groups.dox @@ -161,22 +161,45 @@ ts_t gmt_ts = time_shield::cet_to_gmt(cet_ts); \defgroup ntp NTP Client \brief Facilities for retrieving time using the Network Time Protocol. -This module contains a minimal client capable of querying remote NTP -servers to measure the offset between local and network time. It uses -WinSock via the `WsaGuard` helper and therefore works only on Windows. +This module contains a minimal client and optional pool/service pipeline +capable of querying remote NTP servers to measure the offset between local +realtime and network time. It uses WinSock via the `WsaGuard` helper on +Windows and POSIX sockets on Unix platforms. + +### Architecture: +- `NtpClient` performs a single request/response exchange with one server. +- `NtpClientPool` samples multiple servers with rate limiting and backoff. +- `NtpClientPoolRunner` runs the pool periodically in a background thread. +- `NtpTimeService` wraps the runner in a singleton interface for easy access. + +### Offset computation: +For each response, offsets and delays are computed using the standard NTP +four-timestamp method: +- `t1` — client transmit time +- `t2` — server receive time +- `t3` — server transmit time +- `t4` — client receive time + +Offset and delay are derived as: +`offset = ((t2 - t1) + (t3 - t4)) / 2`, +`delay = (t4 - t1) - (t3 - t2)`. + +Pools aggregate per-server offsets using the configured strategy (median, +best-delay sample, or median with MAD trimming) and optional exponential +smoothing. ### Features: - Query NTP servers and obtain the time offset. - Retrieve corrected UTC time in microseconds and milliseconds. -- Automatic WinSock initialization through `WsaGuard`. +- Automatic WinSock initialization through `WsaGuard` (Windows build). - Requires network connectivity. ### Example Usage: ```cpp time_shield::NtpClient client; if (client.query()) { - int64_t offset = client.get_offset_us(); - int64_t utc_ms = client.get_utc_time_ms(); + int64_t offset = client.offset_us(); + int64_t utc_ms = client.utc_time_ms(); } ``` */ diff --git a/docs/mainpage.md b/docs/mainpage.md index 415669d6..673cadb2 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -21,11 +21,17 @@ portable, and suitable for scenarios like logging, serialization, MQL5 usage, an \section features_sec Features -- Validation of dates and times +- Validation of dates and times (including weekend and workday predicates) - Time and date formatting (standard and custom) - Time zone conversion functions +- Some timestamp-to-calendar conversions use a fast algorithm inspired by + https://www.benjoffe.com/fast-date-64 (implemented from scratch). +- `DateTime` value type storing UTC milliseconds with a fixed offset for + ISO8601 round-trips, local/UTC components, arithmetic, and boundaries - ISO8601 string parsing - Utilities for time manipulation and conversion +- ISO 8601 week-date helpers for conversion, formatting, and parsing +- OLE Automation date conversions and astronomy helpers (JD/MJD/JDN, lunar phase, geocentric MoonPhase calculator) \section config_sec Configuration @@ -80,9 +86,141 @@ Additional example files are located in the `examples/` folder: - `time_utils_example.cpp` — get timestamps and parts - `time_formatting_example.cpp` — to_string, ISO8601, MQL5 - `time_parser_example.cpp` — parse ISO8601 +- `date_time_example.cpp` — fixed-offset `DateTime` parsing, formatting, and arithmetic helpers - `time_conversions_example.cpp` — convert between formats - `time_zone_conversions_example.cpp` — CET/EET ↔ GMT -- `ntp_client_example.cpp` — NTP sync (Windows-only) +- `ntp_client_example.cpp` — NTP sync (sockets) + +\section ntp_sec NTP client, pool, and time service + +Time Shield provides an optional NTP stack (`TIME_SHIELD_ENABLE_NTP_CLIENT`) +that can query remote servers and compute a local offset for UTC time. + +### Components +- `NtpClient` performs a single NTP request to one server. +- `NtpClientPool` samples multiple servers with rate limiting and backoff. +- `NtpClientPoolRunner` runs the pool periodically in a background thread. +- `NtpTimeService` exposes a singleton interface with cached offset/UTC time. + +### Offset computation +Each response is parsed using the standard four-timestamp method: +`offset = ((t2 - t1) + (t3 - t4)) / 2`, +`delay = (t4 - t1) - (t3 - t2)`, +where `t1` is client transmit, `t2` is server receive, `t3` is server transmit, +and `t4` is client receive time. Pool aggregation uses median or best-delay +selection with optional MAD trimming and exponential smoothing. + +### Basic usage +\code{.cpp} +#include +#include + +using namespace time_shield; + +NtpClient client("pool.ntp.org"); +if (client.query()) { + int64_t offset = client.offset_us(); + int64_t utc_ms = client.utc_time_ms(); +} + +auto& service = NtpTimeService::instance(); +service.init(std::chrono::seconds(30)); +int64_t now_ms = service.utc_time_ms(); +service.shutdown(); +\endcode + +\subsection oa_and_astronomy OA date and astronomy helpers + +Convert between Unix timestamps and Excel/COM OA dates, or derive basic +astronomical values from calendar inputs: + +\code{.cpp} +#include +#include + +using namespace time_shield; + +oadate_t oa = ts_to_oadate(1714608000); // OA date for 2024-05-02 +ts_t ts_from_oa = oadate_to_ts(oa); + +jd_t jd = gregorian_to_jd(2, 5, 2024, 12, 0); // Julian Date with time +mjd_t mjd = ts_to_mjd(1714608000); // Modified Julian Date +double phase = moon_phase(fts()); // lunar phase [0..1) +double age = moon_age_days(fts()); // lunar age in days +MoonPhaseSineCosine signal = moon_phase_sincos(fts()); // sin/cos of the phase angle +MoonQuarterInstants quarters = moon_quarters(fts()); // nearest quarter instants (Unix seconds, double) +bool near_new = is_new_moon_window(fts()); // +/-12h new moon window +\endcode + +The `MoonPhaseCalculator` class (`time_shield::astronomy::MoonPhase`) builds on these helpers to return illumination, distances, phase angles, continuous sin/cos for the phase, quarter instants, and “event windows” around new/full/quarter phases. Calculations are geocentric (no observer latitude/longitude or parallax corrections). Phase and illumination are therefore global for a given moment; what changes locally are timezone-adjusted date/time, the apparent orientation of the lit part (flipped between hemispheres), and visibility, which depends on horizon/atmosphere. + +Basic class usage for bespoke calculations: + +\code{.cpp} +#include + +using namespace time_shield; + +MoonPhase calculator{}; +double ts = 1704067200.0; // 2024-01-01T00:00:00Z + +MoonPhaseResult res = calculator.compute(ts); // illumination, distance, angles, sin/cos +MoonPhase::quarters_unix_s_t quarters = calculator.quarter_times_unix(ts); // Unix seconds as double +MoonQuarterInstants structured = calculator.quarter_instants_unix(ts); // structured view +bool near_new = calculator.is_new_moon_window(ts, 3600.0); // +/-1h window around the event +bool near_full = calculator.is_full_moon_window(ts, 3600.0); +\endcode + +\subsection workday_helpers Workday helpers + +Check whether a moment falls on a business day using timestamps, calendar components, or ISO8601 strings: + +\code{.cpp} +#include +#include + +using namespace time_shield; + +bool monday = is_workday(1710720000); // Unix seconds (2024-03-18) +bool monday_ms = is_workday_ms("2024-03-18T09:00:00.250Z"); // ISO8601 with milliseconds +bool from_date = is_workday(2024, 3, 18); // year, month, day components + +// Parsing failure or a weekend evaluates to false +bool saturday = is_workday("2024-03-16T10:00:00Z"); +bool invalid = is_workday("not-a-date"); +\endcode + +Locate the first and last trading days for a month and constrain schedules to the opening or closing workdays: + +\code{.cpp} +using namespace time_shield; + +ts_t june28 = to_timestamp(2024, 6, 28); +bool is_last = is_last_workday_of_month(june28); // true (Friday before a weekend) +bool in_last_two = is_within_last_workdays_of_month(june28, 2); // true for the final two workdays +bool in_first_two = is_within_first_workdays_of_month(june28, 2);// false, trailing end of month + +bool first_from_str = is_first_workday_of_month("2024-09-02T09:00:00Z"); +bool window_from_str = is_within_first_workdays_of_month_ms( + "2024-09-03T09:00:00.250Z", 2); +\endcode + +The string overloads recognise the same ISO8601 formats handled by \ref time_shield::str_to_ts "str_to_ts" and \ref time_shield::str_to_ts_ms "str_to_ts_ms". + +Locate the boundaries of the first and last workdays when preparing trading windows or settlement cutoffs: + +\code{.cpp} +#include + +using namespace time_shield; + +ts_t first_open = start_of_first_workday_month(2024, Month::JUN); // 2024-06-03T00:00:00Z +ts_t first_close = end_of_first_workday_month(2024, 6); // 2024-06-03T23:59:59Z +ts_t last_open = start_of_last_workday_month(2024, 3); // 2024-03-29T00:00:00Z +ts_ms_t last_close_ms = end_of_last_workday_month_ms(2024, Month::MAR); // 2024-03-29T23:59:59.999Z +\endcode + +These helpers follow the same semantics as \ref time_shield::start_of_day "start_of_day" and \ref time_shield::end_of_day "end_of_day", returning UTC timestamps. Invalid month inputs or months without workdays yield \ref time_shield::ERROR_TIMESTAMP "ERROR_TIMESTAMP" to simplify validation. \section install_sec Installation \subsection install_pkg Install and `find_package` @@ -162,4 +300,4 @@ The latest generated API reference is available at \section license_sec License -This library is licensed under the MIT License. See the LICENSE file for more details. \ No newline at end of file +This library is licensed under the MIT License. See the LICENSE file for more details. diff --git a/examples/date_time_example.cpp b/examples/date_time_example.cpp new file mode 100644 index 00000000..935f9481 --- /dev/null +++ b/examples/date_time_example.cpp @@ -0,0 +1,29 @@ +#include + +#include +#include + +/// \brief Demonstrates DateTime parsing, formatting, arithmetic, and ISO week-date usage. +int main() { + using namespace time_shield; + + const std::string input = "2025-12-16T10:20:30.123+02:30"; + DateTime dt; + if (!DateTime::try_parse_iso8601(input, dt)) { + std::cout << "Failed to parse input" << std::endl; + return 1; + } + + std::cout << "unix_ms: " << dt.unix_ms() << '\n'; + std::cout << "iso8601 utc: " << dt.to_iso8601_utc() << '\n'; + std::cout << "formatted: " << dt.format("%F %T") << '\n'; + + const DateTime tomorrow = dt.add_days(1); + std::cout << "tomorrow: " << tomorrow.to_iso8601() << '\n'; + std::cout << "start_of_day: " << dt.start_of_day().to_iso8601() << '\n'; + + const IsoWeekDateStruct iso_week = dt.iso_week_date(); + std::cout << "ISO week-date: " << format_iso_week_date(iso_week) << '\n'; + + return 0; +} diff --git a/examples/ntp_client_example.cpp b/examples/ntp_client_example.cpp index 3d36e7a9..80516380 100644 --- a/examples/ntp_client_example.cpp +++ b/examples/ntp_client_example.cpp @@ -9,7 +9,8 @@ #include #include #include -#if defined(_WIN32) +#include +#if TIME_SHIELD_ENABLE_NTP_CLIENT # include # include @@ -23,11 +24,11 @@ int main() { std::cout << "Querying NTP server..." << std::endl; if (!client.query()) { std::cerr << "Failed to query NTP server. Error code: " - << client.get_last_error_code() << std::endl; + << client.last_error_code() << std::endl; return 1; } - const int64_t offset_us = client.get_offset_us(); + const int64_t offset_us = client.offset_us(); // Current local system time auto now = std::chrono::system_clock::now(); @@ -38,7 +39,7 @@ int main() { // Corrected time using the offset from the NTP server auto corrected = std::chrono::system_clock::time_point( - std::chrono::microseconds(client.get_utc_time_us())); + std::chrono::microseconds(client.utc_time_us())); auto corrected_time_t = std::chrono::system_clock::to_time_t(corrected); std::cout << "Corrected time: " << std::put_time(std::gmtime(&corrected_time_t), "%Y-%m-%d %H:%M:%S") @@ -53,7 +54,7 @@ int main() { #else int main() { - std::cout << "NtpClient is supported only on Windows." << std::endl; + std::cout << "NtpClient is disabled in this build." << std::endl; return 0; } #endif diff --git a/examples/time_utils_example.cpp b/examples/time_utils_example.cpp index f0efd8c2..2cf0d8b2 100644 --- a/examples/time_utils_example.cpp +++ b/examples/time_utils_example.cpp @@ -6,8 +6,6 @@ #include -/// \def _WIN32 -#if defined(_WIN32) #include int main() { @@ -24,15 +22,9 @@ int main() { std::cout << "Millisecond part: " << ms_of_sec() << '\n'; std::cout << "CPU time used: " << get_cpu_time() << " s" << '\n'; - std::cout << "Realtime (us) via QPC: " << now_realtime_us() << '\n'; - + std::cout << "Realtime (us): " << now_realtime_us() << '\n'; + std::cout << "Press Enter to exit..." << std::endl; std::cin.get(); return 0; } -#else -int main() { - std::cout << "time_utils.hpp requires Windows for now_realtime_us()" << std::endl; - return 0; -} -#endif diff --git a/include/time_shield.hpp b/include/time_shield.hpp index 58895a3a..729d5917 100644 --- a/include/time_shield.hpp +++ b/include/time_shield.hpp @@ -5,10 +5,12 @@ /// \file time_shield.hpp /// \brief Main header file for the Time Shield library. +/// \details +/// Includes all public headers of the Time Shield library, so the entire API +/// can be used via a single include directive. /// -/// This header file includes all the components of the Time Shield library, -/// making it easy to include the entire library in your projects with a single -/// include directive. +/// \note This header is intended for convenience. If you care about compile time, +/// include only the specific headers you need. #include "time_shield/config.hpp" ///< Configuration settings for the Time Shield library. #include "time_shield/types.hpp" ///< Type definitions used throughout the library. @@ -18,17 +20,25 @@ #include "time_shield/date_struct.hpp" ///< Structures representing date components. #include "time_shield/time_zone_struct.hpp" ///< Structure representing a time zone. #include "time_shield/date_time_struct.hpp" ///< Structure representing date and time components. +#include "time_shield/DateTime.hpp" ///< Value-type wrapper for timestamp with fixed UTC offset. +#include "time_shield/iso_week_struct.hpp" ///< Structure representing ISO week date components. #include "time_shield/validation.hpp" ///< Functions for validation of time-related values. #include "time_shield/time_utils.hpp" ///< Utility functions for time manipulation. #include "time_shield/time_conversions.hpp" ///< Functions for converting between different time representations. +#include "time_shield/iso_week_conversions.hpp" ///< Functions for ISO week date conversions and formatting. #include "time_shield/time_conversion_aliases.hpp" ///< Convenient conversion aliases. +#include "time_shield/MoonPhase.hpp" ///< Geocentric lunar phase calculator. #include "time_shield/time_zone_conversions.hpp" ///< Functions for converting between time zones. +#include "time_shield/time_zone_offset.hpp" ///< UTC offset arithmetic helpers (UTC <-> local) and offset extraction. #include "time_shield/time_formatting.hpp" ///< Functions for formatting time in various standard formats. #include "time_shield/time_parser.hpp" ///< Functions for parsing time in various standard formats. #if TIME_SHIELD_ENABLE_NTP_CLIENT -# include "time_shield/ntp_client.hpp" ///< NTP client for time offset queries. +# include "time_shield/ntp_client.hpp" ///< NTP client for time offset queries. #endif #include "time_shield/initialization.hpp" ///< Library initialization helpers. +#include "time_shield/TimerScheduler.hpp" ///< Timer scheduler utilities. +#include "time_shield/DeadlineTimer.hpp" ///< Monotonic deadline timer helper. +#include "time_shield/ElapsedTimer.hpp" ///< Monotonic elapsed time measurement helper. /// \namespace tsh /// \brief Alias for the namespace time_shield. @@ -40,9 +50,15 @@ namespace tshield = time_shield; /// \namespace time_shield /// \brief Main namespace for the Time Shield library. -/// -/// The time_shield namespace contains all the core components and functions of the Time Shield library. -/// It includes various utilities for working with time and dates, their formatting, conversion, and validation. +/// \details +/// Contains all public types, constants, and functions of the library. +/// The API provides: +/// - time and date structures +/// - parsing and formatting +/// - conversions between representations (seconds/milliseconds, calendar fields, etc.) +/// - timezone utilities (named zones, UTC offsets) +/// - validation helpers +/// - timers and scheduling utilities namespace time_shield {}; #endif // _TIME_SHIELD_HPP_INCLUDED diff --git a/include/time_shield/CpuTickTimer.hpp b/include/time_shield/CpuTickTimer.hpp new file mode 100644 index 00000000..1d20e20e --- /dev/null +++ b/include/time_shield/CpuTickTimer.hpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_CPU_TICK_TIMER_HPP_INCLUDED +#define _TIME_SHIELD_CPU_TICK_TIMER_HPP_INCLUDED + +/// \file CpuTickTimer.hpp +/// \brief Helper class for measuring CPU time using get_cpu_time(). + +#include "time_utils.hpp" + +#include +#include + +namespace time_shield { + + /// \ingroup time_utils + /// \brief Timer that measures CPU time ticks using get_cpu_time(). + /// \details Class is intended for single-threaded use and assumes that + /// get_cpu_time() is monotonic within the current process or thread. For + /// long-running measurements (for example, durations longer than a day) it + /// is recommended to periodically call record_sample() or restart() to + /// limit floating-point precision loss. + /// \note All reported durations are expressed in CPU tick units provided + /// by get_cpu_time(). + class CpuTickTimer { + public: + /// \brief Construct timer and optionally start it immediately. + /// \param is_start_immediately Indicates whether the timer should start right away. + explicit CpuTickTimer(bool is_start_immediately = true) noexcept { + if (is_start_immediately) { + start(); + } + } + + /// \brief Start measuring CPU time. + void start() noexcept { + m_start_ticks = get_cpu_time(); + m_end_ticks = m_start_ticks; + m_is_running = true; + } + + /// \brief Restart timer and reset collected statistics. + void restart() noexcept { + reset_samples(); + start(); + } + + /// \brief Stop measuring CPU time and freeze elapsed ticks. + void stop() noexcept { + if (m_is_running) { + m_end_ticks = get_cpu_time(); + m_is_running = false; + } + } + + /// \brief Get elapsed CPU ticks since the last start. + /// \return Elapsed CPU tick units produced by get_cpu_time(). + TIME_SHIELD_NODISCARD double elapsed() const noexcept { + const double final_ticks = m_is_running ? get_cpu_time() : m_end_ticks; + return final_ticks - m_start_ticks; + } + + /// \brief Record sample using elapsed ticks and restart timer. + /// \return Collected sample value in CPU tick units or 0.0 when the + /// timer was not previously running. + double record_sample() noexcept { + if (!m_is_running) { + start(); + m_last_sample_ticks = 0.0; + return 0.0; + } + + const double now_ticks = get_cpu_time(); + m_last_sample_ticks = now_ticks - m_start_ticks; + m_start_ticks = now_ticks; + + accumulate_ticks(m_last_sample_ticks); + ++m_sample_count; + + return m_last_sample_ticks; + } + + /// \brief Reset collected samples without touching running state. + void reset_samples() noexcept { + m_total_ticks = 0.0; + m_total_compensation = 0.0; + m_last_sample_ticks = 0.0; + m_sample_count = 0; + } + + /// \brief Get the number of recorded samples. + /// \return Count of recorded samples. + TIME_SHIELD_NODISCARD std::size_t sample_count() const noexcept { + return m_sample_count; + } + + /// \brief Get total recorded CPU ticks across samples. + /// \return Sum of recorded CPU tick units. + TIME_SHIELD_NODISCARD double total_ticks() const noexcept { + return m_total_ticks; + } + + /// \brief Get average CPU ticks per sample. + /// \return Average CPU tick units or NaN if there are no samples. + TIME_SHIELD_NODISCARD double average_ticks() const noexcept { + if (m_sample_count == 0U) { + return std::numeric_limits::quiet_NaN(); + } + return m_total_ticks / static_cast(m_sample_count); + } + + /// \brief Get ticks collected during the last recorded sample. + /// \return Ticks from the most recent sample in CPU tick units. + TIME_SHIELD_NODISCARD double last_sample_ticks() const noexcept { + return m_last_sample_ticks; + } + + private: + void accumulate_ticks(double sample_ticks) noexcept { + const double compensated = sample_ticks - m_total_compensation; + const double updated_total = m_total_ticks + compensated; + m_total_compensation = (updated_total - m_total_ticks) - compensated; + m_total_ticks = updated_total; + } + + double m_start_ticks { 0.0 }; + double m_end_ticks { 0.0 }; + double m_total_ticks { 0.0 }; + double m_total_compensation { 0.0 }; + double m_last_sample_ticks { 0.0 }; + std::size_t m_sample_count { 0 }; + bool m_is_running { false }; + }; + +} // namespace time_shield + +#endif // _TIME_SHIELD_CPU_TICK_TIMER_HPP_INCLUDED diff --git a/include/time_shield/DateTime.hpp b/include/time_shield/DateTime.hpp new file mode 100644 index 00000000..ebe118a0 --- /dev/null +++ b/include/time_shield/DateTime.hpp @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DATE_TIME_HPP_INCLUDED +#define _TIME_SHIELD_DATE_TIME_HPP_INCLUDED + +/// \file DateTime.hpp +/// \brief Value-type wrapper for timestamps with fixed UTC offset. + +#include "config.hpp" +#include "constants.hpp" +#include "date_conversions.hpp" +#include "date_time_conversions.hpp" +#include "date_time_struct.hpp" +#include "iso_week_conversions.hpp" +#include "time_formatting.hpp" +#include "time_parser.hpp" +#include "time_struct.hpp" +#include "time_utils.hpp" +#include "time_zone_struct.hpp" +#include "types.hpp" +#include "validation.hpp" + +#include +#include +#include +#include +#ifdef TIME_SHIELD_CPP17 +#include +#endif + +namespace time_shield { + + /// \brief Represents a moment in time with optional fixed UTC offset. + /// + /// Equality and ordering compare the UTC instant only and ignore the stored offset. + class DateTime { + public: + /// \brief Default constructor sets epoch with zero offset. + DateTime() noexcept + : m_utc_ms(0) + , m_offset(0) {} + + /// \brief Create instance from UTC milliseconds. + /// \param utc_ms Timestamp in milliseconds since Unix epoch (UTC). + /// \param offset Fixed UTC offset in seconds. + /// \return Constructed DateTime. + static DateTime from_unix_ms(ts_ms_t utc_ms, tz_t offset = 0) noexcept { + return DateTime(utc_ms, offset); + } + + /// \brief Create instance from UTC seconds. + /// \param utc_s Timestamp in seconds since Unix epoch (UTC). + /// \param offset Fixed UTC offset in seconds. + /// \return Constructed DateTime. + static DateTime from_unix_s(ts_t utc_s, tz_t offset = 0) noexcept { + return DateTime(sec_to_ms(utc_s), offset); + } + + /// \brief Construct instance for current UTC time. + /// \param offset Fixed UTC offset in seconds. + /// \return DateTime set to now. + static DateTime now_utc(tz_t offset = 0) noexcept { + return DateTime(ts_ms(), offset); + } + + /// \brief Build from calendar components interpreted in provided offset. + static DateTime from_components( + year_t year, + int month, + int day, + int hour = 0, + int min = 0, + int sec = 0, + int ms = 0, + tz_t offset = 0) { + const ts_ms_t local_ms = to_timestamp_ms(year, month, day, hour, min, sec, ms); + const ts_ms_t utc_ms = local_ms - offset_to_ms(offset); + return DateTime(utc_ms, offset); + } + + /// \brief Try to build from calendar components interpreted in provided offset. + /// \param year Year component. + /// \param month Month component. + /// \param day Day component. + /// \param hour Hour component. + /// \param min Minute component. + /// \param sec Second component. + /// \param ms Millisecond component. + /// \param offset Fixed UTC offset in seconds. + /// \param out Output DateTime on success. + /// \return True when components form a valid date-time and offset. + static bool try_from_components( + year_t year, + int month, + int day, + int hour, + int min, + int sec, + int ms, + tz_t offset, + DateTime& out) noexcept { + if (!is_valid_date_time(year, month, day, hour, min, sec, ms)) { + return false; + } + const TimeZoneStruct tz = to_time_zone_struct(offset); + if (!is_valid_time_zone_offset(tz)) { + return false; + } + const ts_ms_t local_ms = to_timestamp_ms(year, month, day, hour, min, sec, ms); + out = DateTime(local_ms - offset_to_ms(offset), offset); + return true; + } + + /// \brief Build from DateTimeStruct interpreted in provided offset. + static DateTime from_date_time_struct(const DateTimeStruct& local_dt, tz_t offset = 0) { + const ts_ms_t local_ms = dt_to_timestamp_ms(local_dt); + const ts_ms_t utc_ms = local_ms - offset_to_ms(offset); + return DateTime(utc_ms, offset); + } + + /// \brief Try to build from DateTimeStruct interpreted in provided offset. + /// \param local_dt Local date-time structure. + /// \param offset Fixed UTC offset in seconds. + /// \param out Output DateTime on success. + /// \return True when structure and offset are valid. + static bool try_from_date_time_struct( + const DateTimeStruct& local_dt, + tz_t offset, + DateTime& out) noexcept { + if (!is_valid_date_time(local_dt)) { + return false; + } + const TimeZoneStruct tz = to_time_zone_struct(offset); + if (!is_valid_time_zone_offset(tz)) { + return false; + } + const ts_ms_t local_ms = dt_to_timestamp_ms(local_dt); + out = DateTime(local_ms - offset_to_ms(offset), offset); + return true; + } + + /// \brief Convert to date-time structure using stored offset. + DateTimeStruct to_date_time_struct_local() const { + return to_date_time_ms(local_ms()); + } + + /// \brief Convert to UTC date-time structure. + DateTimeStruct to_date_time_struct_utc() const { + return to_date_time_ms(m_utc_ms); + } + + /// \brief Build instance from ISO week date interpreted in provided offset. + static DateTime from_iso_week_date( + const IsoWeekDateStruct& iso, + int hour = 0, + int min = 0, + int sec = 0, + int ms = 0, + tz_t offset = 0) { + const DateStruct date = iso_week_date_to_date(iso); + return from_components(date.year, date.mon, date.day, hour, min, sec, ms, offset); + } + + /// \brief Try to parse ISO8601 string to DateTime. + /// \param str Input ISO8601 string. + /// \param out Output DateTime when parsing succeeds. + /// \return True on success. + static bool try_parse_iso8601(const std::string& str, DateTime& out) noexcept { + return try_parse_iso8601_buffer(str.data(), str.size(), out); + } + + #ifdef TIME_SHIELD_CPP17 + /// \brief Try to parse ISO8601 string_view to DateTime. + /// \param str Input ISO8601 string_view. + /// \param out Output DateTime when parsing succeeds. + /// \return True on success. + static bool try_parse_iso8601(std::string_view str, DateTime& out) noexcept { + return try_parse_iso8601_buffer(str.data(), str.size(), out); + } + #endif + + /// \brief Try to parse ISO8601 C-string to DateTime. + /// \param str Null-terminated ISO8601 string. + /// \param out Output DateTime when parsing succeeds. + /// \return True on success. + static bool try_parse_iso8601(const char* str, DateTime& out) noexcept { + if (str == nullptr) { + return false; + } + return try_parse_iso8601_buffer(str, std::strlen(str), out); + } + + /// \brief Parse ISO8601 string, throws on failure. + /// \param str Input ISO8601 string. + /// \return Parsed DateTime. + static DateTime parse_iso8601(const std::string& str) { + return parse_iso8601_buffer(str.data(), str.size()); + } + + #ifdef TIME_SHIELD_CPP17 + /// \brief Parse ISO8601 string_view, throws on failure. + /// \param str Input ISO8601 view. + /// \return Parsed DateTime. + static DateTime parse_iso8601(std::string_view str) { + return parse_iso8601_buffer(str.data(), str.size()); + } + #endif + + /// \brief Parse ISO8601 C-string, throws on failure. + /// \param str Null-terminated ISO8601 string. + /// \return Parsed DateTime. + static DateTime parse_iso8601(const char* str) { + if (str == nullptr) { + throw std::invalid_argument("Invalid ISO8601 datetime"); + } + return parse_iso8601_buffer(str, std::strlen(str)); + } + + /// \brief Try to parse ISO week-date string. + /// \param str Input ISO week-date string. + /// \param iso Output ISO week-date structure. + /// \return True on success. + static bool try_parse_iso_week_date(const std::string& str, IsoWeekDateStruct& iso) noexcept { + return parse_iso_week_date(str.data(), str.size(), iso); + } + + #ifdef TIME_SHIELD_CPP17 + /// \brief Try to parse ISO week-date string_view. + static bool try_parse_iso_week_date(std::string_view str, IsoWeekDateStruct& iso) noexcept { + return parse_iso_week_date(str.data(), str.size(), iso); + } + #endif + + /// \brief Try to parse ISO week-date C-string. + static bool try_parse_iso_week_date(const char* str, IsoWeekDateStruct& iso) noexcept { + if (str == nullptr) { + return false; + } + return parse_iso_week_date(str, std::strlen(str), iso); + } + + /// \brief Format to ISO8601 string with stored offset. + std::string to_iso8601() const { + return to_iso8601_ms(local_ms(), m_offset); + } + + /// \brief Format to ISO8601 string in UTC. + std::string to_iso8601_utc() const { + return to_iso8601_utc_ms(m_utc_ms); + } + + /// \brief Format using custom pattern. + std::string format(const std::string& fmt) const { + return to_string_ms(fmt, local_ms(), m_offset); + } + + #ifdef TIME_SHIELD_CPP17 + /// \brief Format using custom string_view pattern. + std::string format(std::string_view fmt) const { + return to_string_ms(std::string(fmt), local_ms(), m_offset); + } + #endif + + /// \brief Format using C-string pattern. + std::string format(const char* fmt) const { + if (fmt == nullptr) { + return std::string(); + } + return to_string_ms(std::string(fmt), local_ms(), m_offset); + } + + /// \brief Format to MQL5 date-time string. + std::string to_mql5_date_time() const { + return time_shield::to_mql5_date_time(ms_to_sec(local_ms())); + } + + /// \brief Access UTC milliseconds. + ts_ms_t unix_ms() const noexcept { + return m_utc_ms; + } + + /// \brief Access UTC seconds. + ts_t unix_s() const noexcept { + return ms_to_sec(m_utc_ms); + } + + /// \brief Access stored UTC offset. + tz_t utc_offset() const noexcept { + return m_offset; + } + + /// \brief Get timezone structure from offset. + TimeZoneStruct time_zone() const { + return to_time_zone_struct(m_offset); + } + + /// \brief Local year component. + year_t year() const { + return to_date_time_struct_local().year; + } + + /// \brief Local month component. + int month() const { + return to_date_time_struct_local().mon; + } + + /// \brief Local day component. + int day() const { + return to_date_time_struct_local().day; + } + + /// \brief Local hour component. + int hour() const { + return to_date_time_struct_local().hour; + } + + /// \brief Local minute component. + int minute() const { + return to_date_time_struct_local().min; + } + + /// \brief Local second component. + int second() const { + return to_date_time_struct_local().sec; + } + + /// \brief Local millisecond component. + int millisecond() const { + return to_date_time_struct_local().ms; + } + + /// \brief Local date components. + DateStruct date() const { + const DateTimeStruct local_dt = to_date_time_struct_local(); + return create_date_struct(local_dt.year, local_dt.mon, local_dt.day); + } + + /// \brief Local time-of-day components. + TimeStruct time_of_day() const { + const DateTimeStruct local_dt = to_date_time_struct_local(); + return create_time_struct( + static_cast(local_dt.hour), + static_cast(local_dt.min), + static_cast(local_dt.sec), + static_cast(local_dt.ms)); + } + + /// \brief UTC date components. + DateStruct utc_date() const { + const DateTimeStruct utc_dt = to_date_time_struct_utc(); + return create_date_struct(utc_dt.year, utc_dt.mon, utc_dt.day); + } + + /// \brief UTC time-of-day components. + TimeStruct utc_time_of_day() const { + const DateTimeStruct utc_dt = to_date_time_struct_utc(); + return create_time_struct( + static_cast(utc_dt.hour), + static_cast(utc_dt.min), + static_cast(utc_dt.sec), + static_cast(utc_dt.ms)); + } + + /// \brief Local weekday. + Weekday weekday() const { + const DateStruct local_date = date(); + return weekday_of_date(local_date); + } + + /// \brief Local ISO weekday number (1..7). + int iso_weekday() const { + const DateStruct local_date = date(); + return iso_weekday_of_date(local_date.year, local_date.mon, local_date.day); + } + + /// \brief Local ISO week date. + IsoWeekDateStruct iso_week_date() const { + const DateStruct local_date = date(); + return to_iso_week_date(local_date.year, local_date.mon, local_date.day); + } + + /// \brief Check if local date is a workday. + bool is_workday() const noexcept { + return is_workday_ms(local_ms()); + } + + /// \brief Check if local date is a weekend. + bool is_weekend() const noexcept { + return time_shield::is_weekend(ms_to_sec(local_ms())); + } + + /// \brief Compare equality by UTC instant. + bool operator==(const DateTime& other) const noexcept { + return m_utc_ms == other.m_utc_ms; + } + + /// \brief Compare inequality by UTC instant. + bool operator!=(const DateTime& other) const noexcept { + return !(*this == other); + } + + /// \brief Less-than comparison by UTC instant. + bool operator<(const DateTime& other) const noexcept { + return m_utc_ms < other.m_utc_ms; + } + + /// \brief Less-than-or-equal comparison by UTC instant. + bool operator<=(const DateTime& other) const noexcept { + return m_utc_ms <= other.m_utc_ms; + } + + /// \brief Greater-than comparison by UTC instant. + bool operator>(const DateTime& other) const noexcept { + return m_utc_ms > other.m_utc_ms; + } + + /// \brief Greater-than-or-equal comparison by UTC instant. + bool operator>=(const DateTime& other) const noexcept { + return m_utc_ms >= other.m_utc_ms; + } + + /// \brief Check if local representations match including offset. + bool same_local(const DateTime& other) const noexcept { + return local_ms() == other.local_ms() && m_offset == other.m_offset; + } + + /// \brief Add milliseconds to UTC instant. + DateTime add_ms(int64_t delta_ms) const noexcept { + return DateTime(m_utc_ms + delta_ms, m_offset); + } + + /// \brief Add seconds to UTC instant. + DateTime add_seconds(int64_t seconds) const noexcept { + return add_ms(sec_to_ms(seconds)); + } + + /// \brief Add minutes to UTC instant. + DateTime add_minutes(int64_t minutes) const noexcept { + return add_ms(sec_to_ms(minutes * SEC_PER_MIN)); + } + + /// \brief Add hours to UTC instant. + DateTime add_hours(int64_t hours) const noexcept { + return add_ms(sec_to_ms(hours * SEC_PER_HOUR)); + } + + /// \brief Add days to UTC instant. + DateTime add_days(int64_t days) const noexcept { + return add_ms(days * MS_PER_DAY); + } + + /// \brief Difference in milliseconds to another DateTime. + int64_t diff_ms(const DateTime& other) const noexcept { + return m_utc_ms - other.m_utc_ms; + } + + /// \brief Difference in seconds to another DateTime. + double diff_seconds(const DateTime& other) const noexcept { + return static_cast(diff_ms(other)) / static_cast(MS_PER_SEC); + } + + /// \brief Return copy with new offset preserving instant. + DateTime with_offset(tz_t new_offset) const noexcept { + return DateTime(m_utc_ms, new_offset); + } + + /// \brief Return copy with zero offset. + DateTime to_utc() const noexcept { + return with_offset(0); + } + + /// \brief Start of local day. + DateTime start_of_day() const { + const DateTimeStruct local_dt = to_date_time_struct_local(); + const ts_ms_t local_start_ms = to_timestamp_ms(local_dt.year, local_dt.mon, local_dt.day); + return from_unix_ms(local_start_ms - offset_to_ms(m_offset), m_offset); + } + + /// \brief End of local day. + DateTime end_of_day() const { + const DateTimeStruct local_dt = to_date_time_struct_local(); + const ts_ms_t local_end_ms = to_timestamp_ms( + local_dt.year, + local_dt.mon, + local_dt.day, + 23, + 59, + 59, + static_cast(MS_PER_SEC - 1)); + return from_unix_ms(local_end_ms - offset_to_ms(m_offset), m_offset); + } + + /// \brief Start of local month. + DateTime start_of_month() const { + const DateTimeStruct local_dt = to_date_time_struct_local(); + const ts_ms_t local_start_ms = to_timestamp_ms(local_dt.year, local_dt.mon, 1); + return from_unix_ms(local_start_ms - offset_to_ms(m_offset), m_offset); + } + + /// \brief End of local month. + DateTime end_of_month() const { + const DateTimeStruct local_dt = to_date_time_struct_local(); + const int days = num_days_in_month(local_dt.year, local_dt.mon); + const ts_ms_t local_end_ms = to_timestamp_ms( + local_dt.year, + local_dt.mon, + days, + 23, + 59, + 59, + static_cast(MS_PER_SEC - 1)); + return from_unix_ms(local_end_ms - offset_to_ms(m_offset), m_offset); + } + + /// \brief Start of local year. + DateTime start_of_year() const { + const year_t local_year = year(); + const ts_ms_t local_start_ms = to_timestamp_ms(local_year, 1, 1); + return from_unix_ms(local_start_ms - offset_to_ms(m_offset), m_offset); + } + + /// \brief End of local year. + DateTime end_of_year() const { + const year_t local_year = year(); + const ts_ms_t local_end_ms = to_timestamp_ms( + local_year, + 12, + 31, + 23, + 59, + 59, + static_cast(MS_PER_SEC - 1)); + return from_unix_ms(local_end_ms - offset_to_ms(m_offset), m_offset); + } + + private: + static bool try_parse_iso8601_buffer(const char* data, std::size_t size, DateTime& out) noexcept { + if (data == nullptr) { + return false; + } + DateTimeStruct dt = create_date_time_struct(0); + TimeZoneStruct tz = create_time_zone_struct(0, 0, true); + if (!time_shield::parse_iso8601(data, size, dt, tz)) { + return false; + } + const tz_t offset = time_zone_struct_to_offset(tz); + const ts_ms_t utc_ms = dt_to_timestamp_ms(dt) - offset_to_ms(offset); + out = from_unix_ms(utc_ms, offset); + return true; + } + + static DateTime parse_iso8601_buffer(const char* data, std::size_t size) { + DateTime result; + if (!try_parse_iso8601_buffer(data, size, result)) { + throw std::invalid_argument("Invalid ISO8601 datetime"); + } + return result; + } + + DateTime(ts_ms_t utc_ms, tz_t offset) noexcept + : m_utc_ms(utc_ms) + , m_offset(offset) {} + + static constexpr ts_ms_t offset_to_ms(tz_t offset) noexcept { + return static_cast(offset) * MS_PER_SEC; + } + + ts_ms_t local_ms() const noexcept { + return m_utc_ms + offset_to_ms(m_offset); + } + + ts_ms_t m_utc_ms; + tz_t m_offset; + }; + +} // namespace time_shield + +#endif // _TIME_SHIELD_DATE_TIME_HPP_INCLUDED diff --git a/include/time_shield/DeadlineTimer.hpp b/include/time_shield/DeadlineTimer.hpp new file mode 100644 index 00000000..9faf2ada --- /dev/null +++ b/include/time_shield/DeadlineTimer.hpp @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DEADLINE_TIMER_HPP_INCLUDED +#define _TIME_SHIELD_DEADLINE_TIMER_HPP_INCLUDED + +/// \file DeadlineTimer.hpp +/// \brief Monotonic deadline timer utility similar to Qt's QDeadlineTimer. +/// +/// DeadlineTimer provides a lightweight helper around std::chrono::steady_clock +/// that tracks an absolute deadline and exposes helpers to query the remaining +/// time or whether the deadline has already passed. + +#include "config.hpp" +#include "types.hpp" + +#include + +namespace time_shield { + + /// \brief Helper that models a monotonic deadline for timeout management. + /// + /// DeadlineTimer invariants: + /// * Not thread-safe. Access must stay within a single thread. + /// * `start(timeout <= 0)` marks the deadline as already due. + /// * Use `set_forever()` to represent "no timeout" semantics. + class DeadlineTimer { + public: + using clock = std::chrono::steady_clock; + using duration = clock::duration; + using time_point = clock::time_point; + + /// \brief Constructs an inactive timer. + DeadlineTimer() noexcept = default; + + /// \brief Constructs a timer with the specified absolute deadline. + explicit DeadlineTimer(time_point deadline) noexcept { + start(deadline); + } + + /// \brief Constructs a timer that expires after the given timeout. + template + explicit DeadlineTimer(std::chrono::duration timeout) noexcept { + start(timeout); + } + + /// \brief Constructs a timer that expires after the given number of milliseconds. + explicit DeadlineTimer(ts_ms_t timeout_ms) noexcept { + start_ms(timeout_ms); + } + + /// \brief Creates a timer that expires after the specified timeout. + static DeadlineTimer from_timeout(duration timeout) noexcept { + DeadlineTimer timer; + timer.start(timeout); + return timer; + } + + /// \brief Creates a timer that expires after the specified timeout. + template + static DeadlineTimer from_timeout(std::chrono::duration timeout) noexcept { + DeadlineTimer timer; + timer.start(timeout); + return timer; + } + + /// \brief Creates a timer that expires after the specified number of seconds. + static DeadlineTimer from_timeout_sec(ts_t timeout_sec) noexcept { + DeadlineTimer timer; + timer.start_sec(timeout_sec); + return timer; + } + + /// \brief Creates a timer that expires after the specified number of milliseconds. + static DeadlineTimer from_timeout_ms(ts_ms_t timeout_ms) noexcept { + DeadlineTimer timer; + timer.start_ms(timeout_ms); + return timer; + } + + /// \brief Sets the absolute deadline and marks the timer as active. + void start(time_point deadline) noexcept { + m_deadline = deadline; + m_is_running = true; + } + + /// \brief Starts the timer so it expires after the specified timeout. + /// + /// Negative durations result in an immediate expiration. Durations that + /// are shorter than the steady clock tick are rounded up to a single + /// tick to preserve the monotonic nature of the timer. + template + void start(std::chrono::duration timeout) noexcept { + const time_point now = clock::now(); + if (timeout <= decltype(timeout)::zero()) { + start(now); + return; + } + + duration safe_duration = std::chrono::duration_cast(timeout); + if (safe_duration <= duration::zero()) { + safe_duration = duration(1); + } + + const time_point max_time = (time_point::max)(); + const duration max_offset = max_time - now; + if (safe_duration >= max_offset) { + start(max_time); + return; + } + + start(now + safe_duration); + } + + /// \brief Starts the timer so it expires after the specified number of seconds. + void start_sec(ts_t timeout_sec) noexcept { + start(std::chrono::seconds(timeout_sec)); + } + + /// \brief Starts the timer so it expires after the specified number of milliseconds. + void start_ms(ts_ms_t timeout_ms) noexcept { + start(std::chrono::milliseconds(timeout_ms)); + } + + /// \brief Stops the timer and invalidates the stored deadline. + void stop() noexcept { + m_is_running = false; + m_deadline = time_point{}; + } + + /// \brief Marks the timer as running forever (no timeout). + void set_forever() noexcept { + m_is_running = true; + m_deadline = (time_point::max)(); + } + + /// \brief Checks whether the timer currently tracks a deadline. + TIME_SHIELD_NODISCARD bool is_running() const noexcept { + return m_is_running; + } + + /// \brief Checks whether the timer is configured for an infinite timeout. + TIME_SHIELD_NODISCARD bool is_forever() const noexcept { + return m_is_running && m_deadline == (time_point::max)(); + } + + /// \brief Returns stored deadline. + TIME_SHIELD_NODISCARD time_point deadline() const noexcept { + return m_deadline; + } + + /// \brief Returns stored deadline as milliseconds since the steady epoch. + TIME_SHIELD_NODISCARD ts_ms_t deadline_ms() const noexcept { + return std::chrono::duration_cast(m_deadline.time_since_epoch()).count(); + } + + /// \brief Returns stored deadline as seconds since the steady epoch. + TIME_SHIELD_NODISCARD ts_t deadline_sec() const noexcept { + return std::chrono::duration_cast(m_deadline.time_since_epoch()).count(); + } + + /// \brief Checks if the deadline has already expired. + TIME_SHIELD_NODISCARD bool has_expired() const noexcept { + return has_expired(clock::now()); + } + + /// \brief Checks if the deadline has expired relative to the provided millisecond timestamp. + TIME_SHIELD_NODISCARD bool has_expired_ms(ts_ms_t now_ms) const noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::milliseconds(now_ms)); + return has_expired(time_point(since_epoch)); + } + + /// \brief Checks if the deadline has expired relative to the provided second timestamp. + TIME_SHIELD_NODISCARD bool has_expired_sec(ts_t now_sec) const noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::seconds(now_sec)); + return has_expired(time_point(since_epoch)); + } + + /// \brief Checks if the deadline has expired relative to the provided time point. + TIME_SHIELD_NODISCARD bool has_expired(time_point now) const noexcept { + return m_is_running && now >= m_deadline; + } + + /// \brief Returns time remaining until the deadline. + TIME_SHIELD_NODISCARD duration remaining_time() const noexcept { + return remaining_time(clock::now()); + } + + /// \brief Returns remaining time in milliseconds until the deadline. + TIME_SHIELD_NODISCARD ts_ms_t remaining_time_ms() const noexcept { + return std::chrono::duration_cast(remaining_time()).count(); + } + + /// \brief Returns remaining time in seconds until the deadline. + TIME_SHIELD_NODISCARD ts_t remaining_time_sec() const noexcept { + return std::chrono::duration_cast(remaining_time()).count(); + } + + /// \brief Returns remaining time relative to the provided time point. + /// + /// Non-running timers and already expired timers report zero duration. + TIME_SHIELD_NODISCARD duration remaining_time(time_point now) const noexcept { + if (!m_is_running || now >= m_deadline) { + return duration::zero(); + } + return m_deadline - now; + } + + /// \brief Extends deadline by the specified duration while preventing overflow. + void add(duration extend_by) noexcept { + if (!m_is_running || extend_by <= duration::zero()) { + return; + } + + const time_point now = clock::now(); + const time_point base = m_deadline > now ? m_deadline : now; + const duration max_offset = (time_point::max)() - base; + const duration safe_offset = extend_by < max_offset ? extend_by : max_offset; + m_deadline = base + safe_offset; + } + + /// \brief Extends deadline by the specified number of seconds while preventing overflow. + void add_sec(ts_t extend_by_sec) noexcept { + if (extend_by_sec <= 0) { + return; + } + add(std::chrono::seconds(extend_by_sec)); + } + + /// \brief Extends deadline by the specified number of milliseconds while preventing overflow. + void add_ms(ts_ms_t extend_by_ms) noexcept { + if (extend_by_ms <= 0) { + return; + } + add(std::chrono::milliseconds(extend_by_ms)); + } + + private: + time_point m_deadline{}; + bool m_is_running{false}; + }; + +} // namespace time_shield + +#endif // _TIME_SHIELD_DEADLINE_TIMER_HPP_INCLUDED diff --git a/include/time_shield/ElapsedTimer.hpp b/include/time_shield/ElapsedTimer.hpp new file mode 100644 index 00000000..203f596c --- /dev/null +++ b/include/time_shield/ElapsedTimer.hpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_ELAPSED_TIMER_HPP_INCLUDED +#define _TIME_SHIELD_ELAPSED_TIMER_HPP_INCLUDED + +/// \file ElapsedTimer.hpp +/// \brief High-precision elapsed time measurement helper similar to Qt's QElapsedTimer. +/// +/// ElapsedTimer provides lightweight access to std::chrono::steady_clock for +/// precise interval measurements without being affected by system clock +/// adjustments. + +#include "config.hpp" +#include "types.hpp" + +#include +#include + +namespace time_shield { + + /// \brief Helper that measures elapsed monotonic time spans. + /// + /// Instances are expected to be used from a single thread without + /// additional synchronization. + class ElapsedTimer { + public: + using clock = std::chrono::steady_clock; + using duration = clock::duration; + using time_point = clock::time_point; + + /// \brief Constructs an invalid timer. + ElapsedTimer() noexcept = default; + + /// \brief Constructs a timer that starts immediately when requested. + explicit ElapsedTimer(bool start_immediately) noexcept { + if (start_immediately) { + start(); + } + } + + /// \brief Starts the timer using the current steady clock time. + void start() noexcept { + m_start_time = clock::now(); + m_is_running = true; + } + + /// \brief Restarts the timer and returns the elapsed duration so far. + TIME_SHIELD_NODISCARD duration restart() noexcept { + const time_point now = clock::now(); + const duration delta = m_is_running ? now - m_start_time : duration::zero(); + m_start_time = now; + m_is_running = true; + return delta; + } + + /// \brief Restarts the timer using a millisecond timestamp and returns elapsed milliseconds. + TIME_SHIELD_NODISCARD ts_ms_t restart_ms(ts_ms_t now_ms) noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::milliseconds(now_ms)); + const time_point now(since_epoch); + const duration delta = m_is_running ? now - m_start_time : duration::zero(); + m_start_time = now; + m_is_running = true; + return std::chrono::duration_cast(delta).count(); + } + + /// \brief Restarts the timer using a second timestamp and returns elapsed seconds. + TIME_SHIELD_NODISCARD ts_t restart_sec(ts_t now_sec) noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::seconds(now_sec)); + const time_point now(since_epoch); + const duration delta = m_is_running ? now - m_start_time : duration::zero(); + m_start_time = now; + m_is_running = true; + return std::chrono::duration_cast(delta).count(); + } + + /// \brief Invalidates the timer so subsequent elapsed() calls return zero. + void invalidate() noexcept { + m_is_running = false; + } + + /// \brief Checks whether the timer currently measures elapsed time. + TIME_SHIELD_NODISCARD bool is_running() const noexcept { + return m_is_running; + } + + /// \brief Alias for is_running() to match Qt naming conventions. + TIME_SHIELD_NODISCARD bool is_valid() const noexcept { + return m_is_running; + } + + /// \brief Returns start time stored by the timer. + TIME_SHIELD_NODISCARD time_point start_time() const noexcept { + return m_start_time; + } + + /// \brief Returns elapsed duration since the timer was started. + TIME_SHIELD_NODISCARD duration elapsed() const noexcept { + return elapsed(clock::now()); + } + + /// \brief Returns elapsed duration relative to the provided time point. + TIME_SHIELD_NODISCARD duration elapsed(time_point now) const noexcept { + if (!m_is_running) { + return duration::zero(); + } + return now - m_start_time; + } + + /// \brief Returns elapsed nanoseconds since the timer was started. + TIME_SHIELD_NODISCARD std::int64_t elapsed_ns() const noexcept { + return elapsed_count(); + } + + /// \brief Returns elapsed nanoseconds relative to the provided timestamp in nanoseconds. + TIME_SHIELD_NODISCARD std::int64_t elapsed_ns(std::int64_t now_ns) const noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::nanoseconds(now_ns)); + const time_point now(since_epoch); + return std::chrono::duration_cast(elapsed(now)).count(); + } + + /// \brief Returns elapsed milliseconds since the timer was started. + TIME_SHIELD_NODISCARD ts_ms_t elapsed_ms() const noexcept { + return elapsed_count(); + } + + /// \brief Returns elapsed milliseconds relative to the provided timestamp in milliseconds. + TIME_SHIELD_NODISCARD ts_ms_t elapsed_ms(ts_ms_t now_ms) const noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::milliseconds(now_ms)); + const time_point now(since_epoch); + return std::chrono::duration_cast(elapsed(now)).count(); + } + + /// \brief Returns elapsed seconds since the timer was started. + TIME_SHIELD_NODISCARD ts_t elapsed_sec() const noexcept { + return elapsed_count(); + } + + /// \brief Returns elapsed seconds relative to the provided timestamp in seconds. + TIME_SHIELD_NODISCARD ts_t elapsed_sec(ts_t now_sec) const noexcept { + const duration since_epoch = std::chrono::duration_cast(std::chrono::seconds(now_sec)); + const time_point now(since_epoch); + return std::chrono::duration_cast(elapsed(now)).count(); + } + + /// \brief Returns elapsed duration in the desired chrono duration type. + template + TIME_SHIELD_NODISCARD typename Duration::rep elapsed_count() const noexcept { + return std::chrono::duration_cast(elapsed()).count(); + } + + /// \brief Checks if the given timeout in milliseconds has expired. + TIME_SHIELD_NODISCARD bool has_expired(ts_ms_t timeout_ms) const noexcept { + if (!m_is_running) { + return false; + } + if (timeout_ms <= 0) { + return true; + } + return elapsed_ms() >= timeout_ms; + } + + /// \brief Checks if the given timeout in seconds has expired. + TIME_SHIELD_NODISCARD bool has_expired_sec(ts_t timeout_sec) const noexcept { + if (!m_is_running) { + return false; + } + if (timeout_sec <= 0) { + return true; + } + return elapsed() >= std::chrono::seconds(timeout_sec); + } + + /// \brief Returns milliseconds since the internal clock reference. + TIME_SHIELD_NODISCARD std::int64_t ms_since_reference() const noexcept { + if (!m_is_running) { + return 0; + } + return std::chrono::duration_cast(m_start_time.time_since_epoch()).count(); + } + + private: + time_point m_start_time{}; + bool m_is_running{false}; + }; + +} // namespace time_shield + +#endif // _TIME_SHIELD_ELAPSED_TIMER_HPP_INCLUDED diff --git a/include/time_shield/MoonPhase.hpp b/include/time_shield/MoonPhase.hpp new file mode 100644 index 00000000..2950d959 --- /dev/null +++ b/include/time_shield/MoonPhase.hpp @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_MOON_PHASE_HPP_INCLUDED +#define _TIME_SHIELD_MOON_PHASE_HPP_INCLUDED + +/// \file MoonPhase.hpp +/// \brief Geocentric Moon phase calculator and result helpers. +/// \ingroup time_conversions + +#include "date_time_conversions.hpp" +#include "types.hpp" + +#include +#include +#include + +namespace time_shield { + +namespace astronomy { + + /// \brief Result of Moon phase computation (geocentric approximation). + struct MoonPhaseResult { + double phase = 0.0; ///< Phase fraction in [0..1). 0=new moon, 0.5=full moon. + double illumination = 0.0; ///< Illuminated fraction in [0..1]. + double age_days = 0.0; ///< Age of the Moon in days since new moon (approx). + double distance_km = 0.0; ///< Distance to Moon in km (approx). + double diameter_deg = 0.0; ///< Angular diameter of Moon in degrees (approx). + double age_deg = 0.0; ///< Phase angle in degrees (0..360). + double phase_angle_rad = 0.0; ///< Phase angle in radians (0..2*pi). + double phase_sin = 0.0; ///< sin(phase_angle_rad) helper for continuous signal. + double phase_cos = 0.0; ///< cos(phase_angle_rad) helper for continuous signal. + double sun_distance_km = 0.0; + double sun_diameter_deg = 0.0; + }; + + /// \brief Lunar quarter instants (Unix UTC seconds, floating). + struct MoonQuarterInstants { + double previous_new_unix_s = 0.0; ///< Previous new moon instant (Unix UTC seconds, double). + double previous_first_quarter_unix_s = 0.0; ///< Previous first quarter instant (Unix UTC seconds, double). + double previous_full_unix_s = 0.0; ///< Previous full moon instant (Unix UTC seconds, double). + double previous_last_quarter_unix_s = 0.0; ///< Previous last quarter instant (Unix UTC seconds, double). + double next_new_unix_s = 0.0; ///< Next new moon instant (Unix UTC seconds, double). + double next_first_quarter_unix_s = 0.0; ///< Next first quarter instant (Unix UTC seconds, double). + double next_full_unix_s = 0.0; ///< Next full moon instant (Unix UTC seconds, double). + double next_last_quarter_unix_s = 0.0; ///< Next last quarter instant (Unix UTC seconds, double). + }; + + /// \brief Moon phase calculator (geocentric approximation). + /// + /// References: + /// - John Walker, "moontool" (Fourmilab). See: https://www.fourmilab.ch/moontoolw/ + /// - solarissmoke/php-moon-phase (port). See: https://github.com/solarissmoke/php-moon-phase/blob/master/Solaris/MoonPhase.php + /// + /// Notes: + /// - Input timestamps are assumed to be UTC Unix seconds (can be floating). + /// - Computation is geocentric (no observer latitude/longitude corrections). + /// + /// Example usage: + /// \code{.cpp} + /// using namespace time_shield; + /// MoonPhase calculator{}; + /// double ts = 1704067200.0; // 2024-01-01T00:00:00Z + /// MoonPhaseResult res = calculator.compute(ts); // illumination, angles, sin/cos + /// MoonPhase::quarters_unix_s_t quarters = calculator.quarter_times_unix(ts); // Unix seconds as double + /// MoonQuarterInstants mapped = calculator.quarter_instants_unix(ts); // structured view + /// bool near_full = calculator.is_full_moon_window(ts, 3600.0); // +/-1h window check + /// \endcode + class MoonPhase { + public: + using quarters_unix_s_t = std::array; ///< Quarter instants as Unix UTC seconds ({new, firstQ, full, lastQ} for previous and next cycles). + static constexpr double kDefaultQuarterWindow_s = 43200.0; ///< Default window around phase events (12h). + + /// \brief Compute full set of Moon phase parameters for given UTC timestamp. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \return Geocentric Moon phase parameters for the given instant. + MoonPhaseResult compute(double unix_utc_s) const noexcept { + MoonPhaseResult out{}; + const double jd = julian_day_from_unix_seconds(unix_utc_s); + + // --- Sun position --- + const double day = jd - kEpochJd; + const double N = fix_angle((360.0 / 365.2422) * day); + const double M = fix_angle(N + kElonge - kElongp); + + double Ec = kepler(M, kEccent); + Ec = std::sqrt((1.0 + kEccent) / (1.0 - kEccent)) * std::tan(Ec / 2.0); + Ec = 2.0 * rad2deg(std::atan(Ec)); + const double lambda_sun = fix_angle(Ec + kElongp); + + const double F = ((1.0 + kEccent * std::cos(deg2rad(Ec))) / (1.0 - kEccent * kEccent)); + const double sun_dist = kSunSmax / F; + const double sun_ang = F * kSunAngSiz; + + // --- Moon position --- + const double ml = fix_angle(13.1763966 * day + kMmLong); + const double MM = fix_angle(ml - 0.1114041 * day - kMmLongp); + + const double Ev = 1.2739 * std::sin(deg2rad(2.0 * (ml - lambda_sun) - MM)); + const double Ae = 0.1858 * std::sin(deg2rad(M)); + const double A3 = 0.37 * std::sin(deg2rad(M)); + const double MmP = MM + Ev - Ae - A3; + + const double mEc = 6.2886 * std::sin(deg2rad(MmP)); + const double A4 = 0.214 * std::sin(deg2rad(2.0 * MmP)); + const double lP = ml + Ev + mEc - Ae + A4; + + const double V = 0.6583 * std::sin(deg2rad(2.0 * (lP - lambda_sun))); + const double lPP = lP + V; + + // --- Phase --- + const double moon_age_deg_wrapped = fix_angle(lPP - lambda_sun); + const double moon_age_rad = deg2rad(moon_age_deg_wrapped); + const double illum = (1.0 - std::cos(moon_age_rad)) / 2.0; + + const double moon_dist = (kMsMax * (1.0 - kMecc * kMecc)) + / (1.0 + kMecc * std::cos(deg2rad(MmP + mEc))); + + const double moon_dfrac = moon_dist / kMsMax; + const double moon_ang = kMAngSiz / moon_dfrac; + + out.phase = moon_age_deg_wrapped / 360.0; + out.illumination = illum; + out.age_days = kSynMonth * out.phase; + out.distance_km = moon_dist; + out.diameter_deg = moon_ang; + out.age_deg = moon_age_deg_wrapped; + out.phase_angle_rad = moon_age_rad; + out.phase_sin = std::sin(moon_age_rad); + out.phase_cos = std::cos(moon_age_rad); + out.sun_distance_km = sun_dist; + out.sun_diameter_deg = sun_ang; + return out; + } + + /// \brief Compute only phase fraction in [0..1) for given UTC timestamp. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \return Phase fraction in the \f$[0, 1)\f$ interval where 0=new moon, 0.5=full moon. + double compute_phase(double unix_utc_s) const noexcept { + return compute(unix_utc_s).phase; + } + + /// \brief Compute quarter/new/full instants around given timestamp. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \return Array of 8 instants (Unix UTC seconds as double): {prev new, prev firstQ, prev full, prev lastQ, next new, next firstQ, next full, next lastQ}. + quarters_unix_s_t quarter_times_unix(double unix_utc_s) const noexcept { + const double sdate = julian_day_from_unix_seconds(unix_utc_s); + double adate = sdate - 45.0; + + const double ats = unix_utc_s - 86400.0 * 45.0; + const int yy = year_from_unix_seconds(ats); + const int mm = month_from_unix_seconds(ats); + + // IMPORTANT: use floating division + double k1 = std::floor((yy + ((mm - 1) * (1.0 / 12.0)) - 1900.0) * 12.3685); + double k2 = 0.0; + + double nt1 = mean_phase_jd(adate, k1); + adate = nt1; + + while (true) { + adate += kSynMonth; + k2 = k1 + 1.0; + + double nt2 = mean_phase_jd(adate, k2); + if (std::abs(nt2 - sdate) < 0.75) { + nt2 = true_phase_jd(k2, 0.0); // new moon correction + } + + if (nt1 <= sdate && nt2 > sdate) { + break; + } + + nt1 = nt2; + k1 = k2; + } + + const double dates_jd[8] = { + true_phase_jd(k1, 0.0), + true_phase_jd(k1, 0.25), + true_phase_jd(k1, 0.5), + true_phase_jd(k1, 0.75), + true_phase_jd(k2, 0.0), + true_phase_jd(k2, 0.25), + true_phase_jd(k2, 0.5), + true_phase_jd(k2, 0.75) + }; + + quarters_unix_s_t out{}; + for (std::size_t i = 0; i < 8; ++i) { + out[i] = jd_to_unix_seconds(dates_jd[i]); + } + return out; + } + + /// \brief Compatibility wrapper returning quarter instants as Unix UTC seconds. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \return Same payload as quarter_times_unix() for compatibility. + quarters_unix_s_t quarter_times(double unix_utc_s) const noexcept { + return quarter_times_unix(unix_utc_s); + } + + /// \brief Quarter instants around the provided timestamp as a structured result (Unix UTC seconds). + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \return Previous/next quarter instants mapped into a structured result with Unix UTC seconds. + MoonQuarterInstants quarter_instants_unix(double unix_utc_s) const noexcept { + const auto quarters = quarter_times_unix(unix_utc_s); + MoonQuarterInstants out{}; + out.previous_new_unix_s = quarters[0]; + out.previous_first_quarter_unix_s = quarters[1]; + out.previous_full_unix_s = quarters[2]; + out.previous_last_quarter_unix_s = quarters[3]; + out.next_new_unix_s = quarters[4]; + out.next_first_quarter_unix_s = quarters[5]; + out.next_full_unix_s = quarters[6]; + out.next_last_quarter_unix_s = quarters[7]; + return out; + } + + /// \brief Check whether timestamp is inside a window around new moon. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \param window_seconds Symmetric window size in seconds around the event time. + /// \return true if the timestamp lies within the window of the previous or next new moon. + bool is_new_moon_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept { + const auto instants = quarter_instants_unix(unix_utc_s); + return is_within_window(unix_utc_s, instants.previous_new_unix_s, instants.next_new_unix_s, window_seconds); + } + + /// \brief Check whether timestamp is inside a window around full moon. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \param window_seconds Symmetric window size in seconds around the event time. + /// \return true if the timestamp lies within the window of the previous or next full moon. + bool is_full_moon_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept { + const auto instants = quarter_instants_unix(unix_utc_s); + return is_within_window(unix_utc_s, instants.previous_full_unix_s, instants.next_full_unix_s, window_seconds); + } + + /// \brief Check whether timestamp is inside a window around first quarter. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \param window_seconds Symmetric window size in seconds around the event time. + /// \return true if the timestamp lies within the window of the previous or next first quarter. + bool is_first_quarter_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept { + const auto instants = quarter_instants_unix(unix_utc_s); + return is_within_window(unix_utc_s, instants.previous_first_quarter_unix_s, instants.next_first_quarter_unix_s, window_seconds); + } + + /// \brief Check whether timestamp is inside a window around last quarter. + /// \param unix_utc_s Timestamp in Unix UTC seconds (can be fractional). + /// \param window_seconds Symmetric window size in seconds around the event time. + /// \return true if the timestamp lies within the window of the previous or next last quarter. + bool is_last_quarter_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept { + const auto instants = quarter_instants_unix(unix_utc_s); + return is_within_window(unix_utc_s, instants.previous_last_quarter_unix_s, instants.next_last_quarter_unix_s, window_seconds); + } + + private: + static double julian_day_from_unix_seconds(double unix_utc_s) noexcept { + return 2440587.5 + unix_utc_s / 86400.0; + } + + static int year_from_unix_seconds(double unix_utc_s) noexcept { + return static_cast(year_of(static_cast(unix_utc_s))); + } + + static int month_from_unix_seconds(double unix_utc_s) noexcept { + return static_cast(month_of_year(static_cast(unix_utc_s))); + } + + static double jd_to_unix_seconds(double julian_day) noexcept { + return (julian_day - 2440587.5) * 86400.0; + } + + // --- Constants (1980 January 0.0) --- + static constexpr double kEpochJd = 2444238.5; + + static constexpr double kElonge = 278.833540; + static constexpr double kElongp = 282.596403; + static constexpr double kEccent = 0.016718; + static constexpr double kSunSmax = 1.495985e8; // km + static constexpr double kSunAngSiz= 0.533128; // deg + + static constexpr double kMmLong = 64.975464; + static constexpr double kMmLongp = 349.383063; + TIME_SHIELD_MAYBE_UNUSED static constexpr double kMNode = 151.950429; + TIME_SHIELD_MAYBE_UNUSED static constexpr double kMInc = 5.145396; + static constexpr double kMecc = 0.054900; + static constexpr double kMAngSiz = 0.5181; // deg + static constexpr double kMsMax = 384401.0; // km + TIME_SHIELD_MAYBE_UNUSED static constexpr double kMParallax= 0.9507; // deg + static constexpr double kSynMonth = 29.53058868; + + static constexpr double kPi = 3.14159265358979323846; + + static double deg2rad(double deg) noexcept { return deg * (kPi / 180.0); } + static double rad2deg(double rad) noexcept { return rad * (180.0 / kPi); } + + static double fix_angle(double a) noexcept { + a = std::fmod(a, 360.0); + if (a < 0.0) a += 360.0; + return a; + } + + static double kepler(double m_deg, double ecc) noexcept { + constexpr double eps = 1e-6; + const double m = deg2rad(m_deg); + double e = m; + for (int i = 0; i < 50; ++i) { + const double delta = e - ecc * std::sin(e) - m; + e -= delta / (1.0 - ecc * std::cos(e)); + if (std::abs(delta) <= eps) break; + } + return e; + } + + double mean_phase_jd(double julian_day, double lunation_index) const noexcept { + const double jt = (julian_day - 2415020.0) / 36525.0; + const double t2 = jt * jt; + const double t3 = t2 * jt; + + return 2415020.75933 + kSynMonth * lunation_index + + 0.0001178 * t2 + - 0.000000155 * t3 + + 0.00033 * std::sin(deg2rad(166.56 + 132.87 * jt - 0.009173 * t2)); + } + + double true_phase_jd(double lunation_index, double phase_fraction) const noexcept { + // This algorithm is designed for phase_fraction in {0, 0.25, 0.5, 0.75}. + const double kx = lunation_index + phase_fraction; + const double t = kx / 1236.85; + const double t2 = t * t; + const double t3 = t2 * t; + + double pt = 2415020.75933 + + kSynMonth * kx + + 0.0001178 * t2 + - 0.000000155 * t3 + + 0.00033 * std::sin(deg2rad(166.56 + 132.87 * t - 0.009173 * t2)); + + const double m = 359.2242 + 29.10535608 * kx - 0.0000333 * t2 - 0.00000347 * t3; + const double mprime = 306.0253 + 385.81691806 * kx + 0.0107306 * t2 + 0.00001236 * t3; + const double f = 21.2964 + 390.67050646 * kx - 0.0016528 * t2 - 0.00000239 * t3; + + // Corrections (same structure as common ports of moontool) + if (phase_fraction < 0.01 || std::abs(phase_fraction - 0.5) < 0.01) { + pt += (0.1734 - 0.000393 * t) * std::sin(deg2rad(m)) + + 0.0021 * std::sin(deg2rad(2 * m)) + - 0.4068 * std::sin(deg2rad(mprime)) + + 0.0161 * std::sin(deg2rad(2 * mprime)) + - 0.0004 * std::sin(deg2rad(3 * mprime)) + + 0.0104 * std::sin(deg2rad(2 * f)) + - 0.0051 * std::sin(deg2rad(m + mprime)) + - 0.0074 * std::sin(deg2rad(m - mprime)) + + 0.0004 * std::sin(deg2rad(2 * f + m)) + - 0.0004 * std::sin(deg2rad(2 * f - m)) + - 0.0006 * std::sin(deg2rad(2 * f + mprime)) + + 0.0010 * std::sin(deg2rad(2 * f - mprime)) + + 0.0005 * std::sin(deg2rad(m + 2 * mprime)); + return pt; + } + + if (std::abs(phase_fraction - 0.25) < 0.01 || std::abs(phase_fraction - 0.75) < 0.01) { + pt += (0.1721 - 0.0004 * t) * std::sin(deg2rad(m)) + + 0.0021 * std::sin(deg2rad(2 * m)) + - 0.6280 * std::sin(deg2rad(mprime)) + + 0.0089 * std::sin(deg2rad(2 * mprime)) + - 0.0004 * std::sin(deg2rad(3 * mprime)) + + 0.0079 * std::sin(deg2rad(2 * f)) + - 0.0119 * std::sin(deg2rad(m + mprime)) + - 0.0047 * std::sin(deg2rad(m - mprime)) + + 0.0003 * std::sin(deg2rad(2 * f + m)) + - 0.0004 * std::sin(deg2rad(2 * f - m)) + - 0.0006 * std::sin(deg2rad(2 * f + mprime)) + + 0.0021 * std::sin(deg2rad(2 * f - mprime)) + + 0.0003 * std::sin(deg2rad(m + 2 * mprime)) + + 0.0004 * std::sin(deg2rad(m - 2 * mprime)) + - 0.0003 * std::sin(deg2rad(2 * m + mprime)); + + if (phase_fraction < 0.5) { + pt += 0.0028 - 0.0004 * std::cos(deg2rad(m)) + 0.0003 * std::cos(deg2rad(mprime)); + } else { + pt += -0.0028 + 0.0004 * std::cos(deg2rad(m)) - 0.0003 * std::cos(deg2rad(mprime)); + } + return pt; + } + + // Fallback: return uncorrected estimate + return pt; + } + + static bool is_within_window(double unix_utc_s, double previous_instant, double next_instant, double window_seconds) noexcept { + const double prev_delta = std::abs(unix_utc_s - previous_instant); + const double next_delta = std::abs(unix_utc_s - next_instant); + return (prev_delta <= window_seconds) || (next_delta <= window_seconds); + } + }; + +} // namespace astronomy + + /// \brief Convenience alias for the geocentric Moon phase calculator. + using MoonPhaseCalculator = astronomy::MoonPhase; + +} // namespace time_shield + +#endif // _TIME_SHIELD_MOON_PHASE_HPP_INCLUDED diff --git a/include/time_shield/TimerScheduler.hpp b/include/time_shield/TimerScheduler.hpp new file mode 100644 index 00000000..62e795d8 --- /dev/null +++ b/include/time_shield/TimerScheduler.hpp @@ -0,0 +1,650 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_TIMER_SCHEDULER_HPP_INCLUDED +#define _TIME_SHIELD_TIMER_SCHEDULER_HPP_INCLUDED + +/// \file TimerScheduler.hpp +/// \brief Timer scheduler that provides Qt-like timer functionality. +/// +/// TimerScheduler manages timers that can be processed either by a dedicated +/// worker thread or manually via process/update calls. Timers are rescheduled +/// using fixed-rate semantics, meaning the next activation time is based on the +/// previously scheduled fire time. Cancelled timers are removed lazily from the +/// internal queue, which can temporarily increase the queue size under frequent +/// start/stop cycles. + +#include "config.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace time_shield { + + class TimerScheduler; + class Timer; + + namespace detail { + + using TimerClock = std::chrono::steady_clock; + using TimerCallback = std::function; + + /// \brief Internal state shared between Timer and TimerScheduler. + struct TimerState { + TimerScheduler* m_scheduler = nullptr; + std::mutex m_callback_mutex; + TimerCallback m_callback; + std::atomic m_interval_ms{0}; + std::atomic m_is_single_shot{false}; + std::atomic m_is_active{false}; + std::atomic m_is_running{false}; + std::size_t m_id{0}; + std::atomic m_generation{0}; + std::atomic m_has_external_owner{false}; + }; + + inline TimerState*& current_timer_state() { + static TIME_SHIELD_THREAD_LOCAL TimerState* state = nullptr; + return state; + } + + struct RunningTimerScope { + explicit RunningTimerScope(TimerState* state) + : m_previous(current_timer_state()) { + current_timer_state() = state; + } + + ~RunningTimerScope() { + current_timer_state() = m_previous; + } + + private: + TimerState* m_previous; + }; + + /// \brief Data stored in the priority queue of scheduled timers. + struct ScheduledTimer { + ScheduledTimer() = default; + + ScheduledTimer(TimerClock::time_point fire_time, std::size_t timer_id, std::uint64_t generation) + : m_fire_time(fire_time), m_timer_id(timer_id), m_generation(generation) {} + + TimerClock::time_point m_fire_time{}; + std::size_t m_timer_id{0}; + std::uint64_t m_generation{0}; + }; + + /// \brief Comparator that orders timers by earliest fire time. + struct ScheduledComparator { + bool operator()(const ScheduledTimer& lhs, const ScheduledTimer& rhs) const { + return lhs.m_fire_time > rhs.m_fire_time; + } + }; + + /// \brief Helper structure that represents a timer ready to run. + struct DueTimer { + DueTimer() = default; + + DueTimer(TimerClock::time_point fire_time, + std::uint64_t generation, + std::shared_ptr state) + : m_fire_time(fire_time), m_generation(generation), m_state(std::move(state)) {} + + TimerClock::time_point m_fire_time{}; + std::uint64_t m_generation{0}; + std::shared_ptr m_state; + }; + + } // namespace detail + + using timer_state_ptr = std::shared_ptr; + + /// \brief Scheduler that manages timer execution. + class TimerScheduler { + public: + using clock = detail::TimerClock; + + TimerScheduler(); + ~TimerScheduler(); + + TimerScheduler(const TimerScheduler&) = delete; + TimerScheduler& operator=(const TimerScheduler&) = delete; + TimerScheduler(TimerScheduler&&) = delete; + TimerScheduler& operator=(TimerScheduler&&) = delete; + + /// \brief Starts a dedicated worker thread that processes timers. + /// + /// This method is non-blocking. It spawns a background thread that + /// waits for timers to fire and executes their callbacks. While the + /// worker thread is active, manual processing via process() or update() + /// must not be used. + void run(); + + /// \brief Requests the worker thread to stop and waits for it to exit. + void stop(); + + /// \brief Processes all timers that are ready to fire at the moment of the call. + /// + /// The method is non-blocking: it does not wait for future timers. + /// It must not be called while the worker thread started by run() is + /// active. + void process(); + + /// \brief Alias for process() for compatibility with update-based loops. + void update(); + + /// \brief Returns number of timer states that are still alive. + /// + /// Method is intended for tests to verify resource cleanup. + std::size_t active_timer_count_for_testing(); + + private: + friend class Timer; + + timer_state_ptr create_timer_state(); + void destroy_timer_state(const timer_state_ptr& state); + void start_timer(const timer_state_ptr& state, clock::time_point when); + void stop_timer(const timer_state_ptr& state); + + void worker_loop(); + void collect_due_timers_locked(std::vector& due, clock::time_point now); + void execute_due_timers(std::vector& due); + void finalize_timer(const detail::DueTimer& due_timer); + + std::mutex m_mutex; + std::condition_variable m_cv; + std::thread m_thread; + bool m_is_worker_running{false}; + bool m_stop_requested{false}; + std::priority_queue, detail::ScheduledComparator> m_queue; + std::unordered_map> m_timers; + std::size_t m_next_id{1}; + }; + + /// \brief Timer that mimics the behavior of Qt timers. + class Timer { + public: + using Callback = detail::TimerCallback; + + explicit Timer(TimerScheduler& scheduler); + ~Timer(); + + Timer(const Timer&) = delete; + Timer& operator=(const Timer&) = delete; + Timer(Timer&&) = delete; + Timer& operator=(Timer&&) = delete; + + /// \brief Sets the interval used by the timer. + /// + /// Negative durations are clamped to zero. An interval of zero means + /// the timer is rescheduled immediately after firing. + template + void set_interval(std::chrono::duration interval) noexcept; + + /// \brief Returns the currently configured interval. + std::chrono::milliseconds interval() const noexcept; + + /// \brief Starts the timer using the previously configured interval. + void start(); + + /// \brief Starts the timer with the specified interval. + template + void start(std::chrono::duration interval); + + /// \brief Stops the timer. + /// + /// The operation is non-blocking: the method does not wait for a + /// running callback to finish. Use stop_and_wait() to synchronously + /// wait for completion. + void stop(); + + /// \brief Stops the timer and waits until an active callback finishes. + /// + /// Must not be called from inside the timer callback itself. + void stop_and_wait(); + + /// \brief Sets whether the timer should fire only once. + void set_single_shot(bool is_single_shot) noexcept; + + /// \brief Returns true if the timer fires only once. + bool is_single_shot() const noexcept; + + /// \brief Returns true if the timer is active. + bool is_active() const noexcept; + + /// \brief Returns true if the timer callback is being executed. + bool is_running() const noexcept; + + /// \brief Sets the callback that should be invoked when the timer fires. + void set_callback(Callback callback); + + /// \brief Creates a single-shot timer that invokes the callback once. + /// + /// The helper keeps the timer alive until the callback finishes. + template + static void single_shot(TimerScheduler& scheduler, + std::chrono::duration interval, + Callback callback); + + private: + TimerScheduler& m_scheduler; + timer_state_ptr m_state; + }; + + // --------------------------------------------------------------------- + // TimerScheduler inline implementation + // --------------------------------------------------------------------- + + inline TimerScheduler::TimerScheduler() = default; + + inline TimerScheduler::~TimerScheduler() { + stop(); + std::lock_guard lock(m_mutex); + for (auto& entry : m_timers) { + if (auto state = entry.second.lock()) { + std::lock_guard callback_lock(state->m_callback_mutex); + state->m_callback = {}; + } + } + m_timers.clear(); + while (!m_queue.empty()) { + m_queue.pop(); + } + } + + inline void TimerScheduler::run() { + std::lock_guard lock(m_mutex); + if (m_is_worker_running) { + return; + } + m_stop_requested = false; + m_is_worker_running = true; + m_thread = std::thread(&TimerScheduler::worker_loop, this); + } + + inline void TimerScheduler::stop() { + std::vector orphan_states; + std::thread worker_to_join; + + { + std::unique_lock lock(m_mutex); + if (m_is_worker_running) { + m_stop_requested = true; + m_cv.notify_all(); + worker_to_join = std::move(m_thread); + } else { + m_stop_requested = false; + } + + for (auto it = m_timers.begin(); it != m_timers.end();) { + auto state = it->second.lock(); + if (!state) { + it = m_timers.erase(it); + continue; + } + + if (!state->m_has_external_owner.load(std::memory_order_relaxed)) { + orphan_states.push_back(state); + it = m_timers.erase(it); + } else { + ++it; + } + } + } + + if (worker_to_join.joinable()) { + worker_to_join.join(); + } + + { + std::lock_guard lock(m_mutex); + m_is_worker_running = false; + m_stop_requested = false; + } + + for (auto& state : orphan_states) { + if (!state) { + continue; + } + std::lock_guard callback_lock(state->m_callback_mutex); + state->m_callback = {}; + state->m_is_active.store(false, std::memory_order_relaxed); + } + } + + inline void TimerScheduler::process() { + std::vector due; + { + std::lock_guard lock(m_mutex); + assert(!m_is_worker_running && "process() must not be called while the worker thread is active"); + const auto now = clock::now(); + collect_due_timers_locked(due, now); + } + execute_due_timers(due); + } + + inline void TimerScheduler::update() { + process(); + } + + inline std::size_t TimerScheduler::active_timer_count_for_testing() { + std::lock_guard lock(m_mutex); + std::size_t count = 0; + for (const auto& entry : m_timers) { + if (!entry.second.expired()) { + ++count; + } + } + return count; + } + + inline timer_state_ptr TimerScheduler::create_timer_state() { + auto state = std::make_shared(); + state->m_scheduler = this; + std::lock_guard lock(m_mutex); + state->m_id = m_next_id++; + m_timers[state->m_id] = state; + return state; + } + + inline void TimerScheduler::destroy_timer_state(const timer_state_ptr& state) { + if (!state) { + return; + } + { + std::lock_guard callback_lock(state->m_callback_mutex); + state->m_callback = {}; + } + std::lock_guard lock(m_mutex); + state->m_is_active.store(false, std::memory_order_relaxed); + state->m_generation.fetch_add(1, std::memory_order_relaxed); + if (state->m_id != 0) { + m_timers.erase(state->m_id); + } + state->m_scheduler = nullptr; + } + + inline void TimerScheduler::start_timer(const timer_state_ptr& state, clock::time_point when) { + if (!state) { + return; + } + std::lock_guard lock(m_mutex); + state->m_is_active.store(true, std::memory_order_relaxed); + const auto generation = state->m_generation.fetch_add(1, std::memory_order_relaxed) + 1; + m_queue.push(detail::ScheduledTimer{when, state->m_id, generation}); + m_cv.notify_all(); + } + + inline void TimerScheduler::stop_timer(const timer_state_ptr& state) { + if (!state) { + return; + } + std::lock_guard lock(m_mutex); + state->m_is_active.store(false, std::memory_order_relaxed); + state->m_generation.fetch_add(1, std::memory_order_relaxed); + m_cv.notify_all(); + } + + inline void TimerScheduler::worker_loop() { + std::vector due; + std::unique_lock lock(m_mutex); + while (!m_stop_requested) { + if (m_queue.empty()) { + m_cv.wait(lock, [this] { return m_stop_requested || !m_queue.empty(); }); + continue; + } + + const auto next_fire_time = m_queue.top().m_fire_time; + const bool woke_by_condition = m_cv.wait_until( + lock, + next_fire_time, + [this, next_fire_time] { + return m_stop_requested || m_queue.empty() || m_queue.top().m_fire_time < next_fire_time; + } + ); + + if (m_stop_requested) { + break; + } + + if (woke_by_condition) { + continue; + } + + const auto now = clock::now(); + collect_due_timers_locked(due, now); + + lock.unlock(); + execute_due_timers(due); + due.clear(); + lock.lock(); + } + } + + inline void TimerScheduler::collect_due_timers_locked(std::vector& due, clock::time_point now) { + while (!m_queue.empty()) { + const auto& top = m_queue.top(); + if (top.m_fire_time > now) { + break; + } + + detail::ScheduledTimer item = top; + m_queue.pop(); + + auto it = m_timers.find(item.m_timer_id); + if (it == m_timers.end()) { + continue; + } + + auto state = it->second.lock(); + if (!state) { + m_timers.erase(it); + continue; + } + + if (!state->m_is_active.load(std::memory_order_relaxed) || + state->m_generation.load(std::memory_order_relaxed) != item.m_generation) { + continue; + } + + state->m_is_running.store(true, std::memory_order_release); + due.push_back(detail::DueTimer{item.m_fire_time, item.m_generation, std::move(state)}); + } + } + + inline void TimerScheduler::execute_due_timers(std::vector& due) { + for (auto& timer : due) { + detail::TimerCallback callback; + if (timer.m_state) { + std::lock_guard callback_lock(timer.m_state->m_callback_mutex); + callback = timer.m_state->m_callback; + } + if (callback) { + detail::RunningTimerScope running_scope(timer.m_state.get()); + try { + callback(); + } catch (...) { + // TODO: integrate with logging once a logging facility is available. + } + } + finalize_timer(timer); + } + } + + inline void TimerScheduler::finalize_timer(const detail::DueTimer& due_timer) { + auto state = due_timer.m_state; + if (!state) { + return; + } + + std::unique_lock lock(m_mutex); + state->m_is_running.store(false, std::memory_order_release); + if (!state->m_is_active.load(std::memory_order_relaxed)) { + return; + } + + if (state->m_is_single_shot.load(std::memory_order_relaxed)) { + state->m_is_active.store(false, std::memory_order_relaxed); + state->m_generation.fetch_add(1, std::memory_order_relaxed); + return; + } + + if (state->m_generation.load(std::memory_order_relaxed) != due_timer.m_generation) { + return; + } + + const auto interval_ms = state->m_interval_ms.load(std::memory_order_relaxed); + const auto next_fire_time = due_timer.m_fire_time + std::chrono::milliseconds(interval_ms); + const auto next_generation = state->m_generation.fetch_add(1, std::memory_order_relaxed) + 1; + m_queue.push(detail::ScheduledTimer{next_fire_time, state->m_id, next_generation}); + m_cv.notify_all(); + } + + // --------------------------------------------------------------------- + // Timer inline implementation + // --------------------------------------------------------------------- + + inline Timer::Timer(TimerScheduler& scheduler) + : m_scheduler(scheduler), m_state(scheduler.create_timer_state()) { + if (m_state) { + m_state->m_has_external_owner.store(true, std::memory_order_relaxed); + } + } + + inline Timer::~Timer() { + if (!m_state) { + return; + } + + if (detail::current_timer_state() != m_state.get()) { + stop_and_wait(); + } else { + m_scheduler.stop_timer(m_state); + } + + m_state->m_has_external_owner.store(false, std::memory_order_relaxed); + m_scheduler.destroy_timer_state(m_state); + } + + template + void Timer::set_interval(std::chrono::duration interval) noexcept { + auto milliseconds = std::chrono::duration_cast(interval).count(); + if (milliseconds < 0) { + milliseconds = 0; + } + m_state->m_interval_ms.store(milliseconds, std::memory_order_relaxed); + } + + inline std::chrono::milliseconds Timer::interval() const noexcept { + const auto milliseconds = m_state->m_interval_ms.load(std::memory_order_relaxed); + return std::chrono::milliseconds(milliseconds); + } + + inline void Timer::start() { + const auto milliseconds = m_state->m_interval_ms.load(std::memory_order_relaxed); + const auto delay = TimerScheduler::clock::now() + std::chrono::milliseconds(milliseconds); + m_scheduler.start_timer(m_state, delay); + } + + template + void Timer::start(std::chrono::duration interval) { + set_interval(interval); + start(); + } + + inline void Timer::stop() { + m_scheduler.stop_timer(m_state); + } + + inline void Timer::stop_and_wait() { + assert(detail::current_timer_state() != m_state.get() + && "stop_and_wait() must not be called from inside callback"); + m_scheduler.stop_timer(m_state); + while (m_state->m_is_running.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + } + + inline void Timer::set_single_shot(bool is_single_shot) noexcept { + m_state->m_is_single_shot.store(is_single_shot, std::memory_order_relaxed); + } + + inline bool Timer::is_single_shot() const noexcept { + return m_state->m_is_single_shot.load(std::memory_order_relaxed); + } + + inline bool Timer::is_active() const noexcept { + return m_state->m_is_active.load(std::memory_order_relaxed); + } + + inline bool Timer::is_running() const noexcept { + return m_state->m_is_running.load(std::memory_order_relaxed); + } + + inline void Timer::set_callback(Callback callback) { + std::lock_guard lock(m_state->m_callback_mutex); + m_state->m_callback = std::move(callback); + } + + template + void Timer::single_shot(TimerScheduler& scheduler, + std::chrono::duration interval, + Callback callback) { + auto state = scheduler.create_timer_state(); + if (!state) { + return; + } + + auto milliseconds = std::chrono::duration_cast(interval).count(); + if (milliseconds < 0) { + milliseconds = 0; + } + + state->m_is_single_shot.store(true, std::memory_order_relaxed); + state->m_interval_ms.store(milliseconds, std::memory_order_relaxed); + + auto* scheduler_ptr = state->m_scheduler; + + Callback user_callback_local = std::move(callback); + + { + std::lock_guard lock(state->m_callback_mutex); + state->m_callback = [state, scheduler_ptr, user_callback_local]() mutable { + if (user_callback_local) { + user_callback_local(); + } + + auto state_ptr = state; + if (!state_ptr) { + return; + } + + { + std::lock_guard callback_lock(state_ptr->m_callback_mutex); + state_ptr->m_callback = {}; + } + + if (scheduler_ptr) { + scheduler_ptr->destroy_timer_state(state_ptr); + } + }; + } + + const auto fire_time = TimerScheduler::clock::now() + std::chrono::milliseconds(milliseconds); + scheduler.start_timer(state, fire_time); + } + +} // namespace time_shield + +#endif // _TIME_SHIELD_TIMER_SCHEDULER_HPP_INCLUDED diff --git a/include/time_shield/astronomy_conversions.hpp b/include/time_shield/astronomy_conversions.hpp new file mode 100644 index 00000000..24edb797 --- /dev/null +++ b/include/time_shield/astronomy_conversions.hpp @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_ASTRONOMY_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_ASTRONOMY_CONVERSIONS_HPP_INCLUDED + +/// \file astronomy_conversions.hpp +/// \brief Julian Date / MJD / JDN and simple lunar helpers. +/// \ingroup time_conversions +/// +/// JD epoch used here: +/// - Unix epoch (1970-01-01 00:00:00 UTC) is JD 2440587.5 +/// +/// Notes: +/// - JD and MJD are returned as double (jd_t/mjd_t). +/// - These functions are intended for utility/analytics, not for high-precision astronomy. + +#include "config.hpp" +#include "types.hpp" +#include "constants.hpp" +#include "MoonPhase.hpp" +#include + +namespace time_shield { + + /// \brief Convert Unix timestamp (floating seconds) to Julian Date (JD). + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Julian Date value. + inline jd_t fts_to_jd(fts_t ts) noexcept { + // JD at Unix epoch: + // 1970-01-01 00:00:00 UTC -> 2440587.5 + return static_cast(2440587.5) + + static_cast(ts) / static_cast(SEC_PER_DAY); + } + + /// \brief Convert Unix timestamp (seconds) to Julian Date (JD). + /// \param ts Unix timestamp in seconds since Unix epoch. + /// \return Julian Date value. + inline jd_t ts_to_jd(ts_t ts) noexcept { + return fts_to_jd(static_cast(ts)); + } + + /// \brief Convert Gregorian date (with optional fractional day) to Julian Date (JD). + /// \param day Day of month (may include fractional part). + /// \param month Month [1..12]. + /// \param year Full year (e.g. 2025). + /// \return Julian Date value. + inline jd_t gregorian_to_jd(double day, int64_t month, int64_t year) noexcept { + // Algorithm source (as per your original code): krutov.org Julianday + if (month == 1 || month == 2) { + year -= 1; + month += 12; + } + const double a = std::floor(static_cast(year) / 100.0); + const double b = 2.0 - a + std::floor(a / 4.0); + const double jdn = std::floor(365.25 * (static_cast(year) + 4716.0)) + + std::floor(30.6000001 * (static_cast(month) + 1.0)) + + day + b - 1524.5; + return static_cast(jdn); + } + + /// \brief Convert Gregorian date/time components to Julian Date (JD). + /// \param day Day of month [1..31]. + /// \param month Month [1..12]. + /// \param year Full year (e.g. 2025). + /// \param hour Hour of day [0..23]. + /// \param minute Minute of hour [0..59]. + /// \param second Second of minute [0..59]. + /// \param millisecond Millisecond of second [0..999]. + /// \return Julian Date value. + inline jd_t gregorian_to_jd( + uint32_t day, + uint32_t month, + uint32_t year, + uint32_t hour, + uint32_t minute, + uint32_t second = 0, + uint32_t millisecond = 0) noexcept { + const double frac = + (static_cast(hour) / 24.0) + + (static_cast(minute) / (24.0 * 60.0)) + + ((static_cast(second) + static_cast(millisecond) / 1000.0) + / static_cast(SEC_PER_DAY)); + return gregorian_to_jd(static_cast(day) + frac, + static_cast(month), + static_cast(year)); + } + + /// \brief Convert Unix timestamp (floating seconds) to Modified Julian Date (MJD). + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Modified Julian Date value. + inline mjd_t fts_to_mjd(fts_t ts) noexcept { + return static_cast(fts_to_jd(ts) - 2400000.5); + } + + /// \brief Convert Unix timestamp (seconds) to Modified Julian Date (MJD). + /// \param ts Unix timestamp in seconds since Unix epoch. + /// \return Modified Julian Date value. + inline mjd_t ts_to_mjd(ts_t ts) noexcept { + return static_cast(fts_to_mjd(static_cast(ts))); + } + + /// \brief Convert Gregorian date to Julian Day Number (JDN). + /// \details JDN is an integer day count (no fractional part). + /// \param day Day of month [1..31]. + /// \param month Month [1..12]. + /// \param year Full year (e.g. 2025). + /// \return Julian Day Number value. + inline jdn_t gregorian_to_jdn(uint32_t day, uint32_t month, uint32_t year) noexcept { + const uint64_t a = (14ULL - static_cast(month)) / 12ULL; + const uint64_t y = static_cast(year) + 4800ULL - a; + const uint64_t m = static_cast(month) + 12ULL * a - 3ULL; + const uint64_t jdn = static_cast(day) + + (153ULL * m + 2ULL) / 5ULL + + 365ULL * y + + y / 4ULL + - y / 100ULL + + y / 400ULL + - 32045ULL; + return static_cast(jdn); + } + + /// \brief sin/cos helper for the Moon phase angle. + struct MoonPhaseSineCosine { + double phase_sin = 0.0; ///< sin(phase angle), continuous around 0/2pi. + double phase_cos = 0.0; ///< cos(phase angle), continuous around 0/2pi. + double phase_angle_rad = 0.0; ///< Phase angle in radians [0..2*pi). + constexpr MoonPhaseSineCosine() = default; + constexpr MoonPhaseSineCosine(double phase_sin_value, double phase_cos_value, double phase_angle_rad_value) noexcept + : phase_sin(phase_sin_value), + phase_cos(phase_cos_value), + phase_angle_rad(phase_angle_rad_value) {} + }; + + /// \brief Get lunar phase in range [0..1) using a simple Julian Day approximation. + /// \details This helper mirrors the legacy Julian Day based approximation and is less precise + /// than the geocentric MoonPhase calculator. + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Approximate lunar phase fraction where 0 is new moon. + inline double moon_phase_jd_approx(fts_t ts) noexcept { + double temp = (static_cast(fts_to_jd(ts)) - 2451550.1) / 29.530588853; + temp = temp - std::floor(temp); + if (temp < 0.0) temp += 1.0; + return temp; + } + + /// \brief Get lunar phase in range [0..1) using the geocentric MoonPhase calculator. + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Lunar phase fraction where 0 is new moon. + inline double moon_phase(fts_t ts) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.compute(static_cast(ts)).phase; + } + + /// \brief Get sin/cos of the lunar phase angle (continuous signal without wrap-around). + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Structure containing sin/cos and the angle in radians. + inline MoonPhaseSineCosine moon_phase_sincos(fts_t ts) noexcept { + static const astronomy::MoonPhase calculator{}; + const auto result = calculator.compute(static_cast(ts)); + return MoonPhaseSineCosine{result.phase_sin, result.phase_cos, result.phase_angle_rad}; + } + + /// \brief Get illuminated fraction in range [0..1] using the geocentric MoonPhase calculator. + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Illuminated fraction of the Moon. + inline double moon_illumination(fts_t ts) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.compute(static_cast(ts)).illumination; + } + + /// \brief Get lunar age in days (~0..29.53) using a simple Julian Day approximation. + /// \details This helper mirrors the legacy Julian Day based approximation and is less precise + /// than the geocentric MoonPhase calculator. + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Approximate lunar age in days. + inline double moon_age_days_jd_approx(fts_t ts) noexcept { + return moon_phase_jd_approx(ts) * 29.530588853; + } + + /// \brief Get lunar age in days (~0..29.53). + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Approximate lunar age in days. + inline double moon_age_days(fts_t ts) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.compute(static_cast(ts)).age_days; + } + + /// \brief Quarter instants around the provided timestamp. + /// \param ts Unix timestamp in floating seconds since Unix epoch. + /// \return Quarter windows around the timestamp (Unix seconds as double). + inline astronomy::MoonQuarterInstants moon_quarters(fts_t ts) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.quarter_instants_unix(static_cast(ts)); + } + + /// \brief Check if timestamp falls into the new moon window (default \pm12h). + inline bool is_new_moon_window(fts_t ts, double window_seconds = astronomy::MoonPhase::kDefaultQuarterWindow_s) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.is_new_moon_window(static_cast(ts), window_seconds); + } + + /// \brief Check if timestamp falls into the full moon window (default \pm12h). + inline bool is_full_moon_window(fts_t ts, double window_seconds = astronomy::MoonPhase::kDefaultQuarterWindow_s) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.is_full_moon_window(static_cast(ts), window_seconds); + } + + /// \brief Check if timestamp falls into the first quarter window (default \pm12h). + inline bool is_first_quarter_window(fts_t ts, double window_seconds = astronomy::MoonPhase::kDefaultQuarterWindow_s) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.is_first_quarter_window(static_cast(ts), window_seconds); + } + + /// \brief Check if timestamp falls into the last quarter window (default \pm12h). + inline bool is_last_quarter_window(fts_t ts, double window_seconds = astronomy::MoonPhase::kDefaultQuarterWindow_s) noexcept { + static const astronomy::MoonPhase calculator{}; + return calculator.is_last_quarter_window(static_cast(ts), window_seconds); + } + +} // namespace time_shield + +#endif // _TIME_SHIELD_ASTRONOMY_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/config.hpp b/include/time_shield/config.hpp index d033f0c1..12f6f610 100644 --- a/include/time_shield/config.hpp +++ b/include/time_shield/config.hpp @@ -11,6 +11,8 @@ /// enable or disable parts of the library depending on the target platform or /// user preferences. +#include + #if defined(_MSVC_LANG) # define TIME_SHIELD_CXX_VERSION _MSVC_LANG #else @@ -44,6 +46,40 @@ #endif #endif +// Configure nodiscard attribute support while keeping compatibility with C++11 compilers +#if defined(__has_cpp_attribute) +# if __has_cpp_attribute(nodiscard) && defined(TIME_SHIELD_CPP17) +# define TIME_SHIELD_NODISCARD [[nodiscard]] +# else +# define TIME_SHIELD_NODISCARD +# endif +#else +# if defined(TIME_SHIELD_CPP17) +# define TIME_SHIELD_NODISCARD [[nodiscard]] +# else +# define TIME_SHIELD_NODISCARD +# endif +#endif + +// Attribute helpers +#if defined(TIME_SHIELD_CPP17) +# define TIME_SHIELD_MAYBE_UNUSED [[maybe_unused]] +#else +# define TIME_SHIELD_MAYBE_UNUSED +#endif + +// Configure thread-local storage handling for compilers with partial support +#if defined(__cpp_thread_local) +# define TIME_SHIELD_THREAD_LOCAL thread_local +#elif defined(_MSC_VER) +# define TIME_SHIELD_THREAD_LOCAL __declspec(thread) +#elif defined(__GNUC__) +# define TIME_SHIELD_THREAD_LOCAL __thread +#else +# define TIME_SHIELD_THREAD_LOCAL +#endif + + /// \name Platform detection ///@{ #if defined(_WIN32) @@ -72,7 +108,7 @@ /// \name Optional features ///@{ #ifndef TIME_SHIELD_ENABLE_NTP_CLIENT -# if TIME_SHIELD_HAS_WINSOCK +# if TIME_SHIELD_HAS_WINSOCK || TIME_SHIELD_PLATFORM_UNIX # define TIME_SHIELD_ENABLE_NTP_CLIENT 1 # else # define TIME_SHIELD_ENABLE_NTP_CLIENT 0 diff --git a/include/time_shield/constants.hpp b/include/time_shield/constants.hpp index db49a773..51427904 100644 --- a/include/time_shield/constants.hpp +++ b/include/time_shield/constants.hpp @@ -154,7 +154,10 @@ namespace time_shield { constexpr int64_t MAX_YEAR = 292277022000LL; ///< Maximum representable year constexpr int64_t MIN_YEAR = -2967369602200LL; ///< Minimum representable year constexpr int64_t ERROR_YEAR = 9223372036854770000LL; ///< Error year value - constexpr int64_t MAX_TIMESTAMP = 9223371890843040000LL; ///< Maximum timestamp value + constexpr int64_t MAX_TIMESTAMP = (((std::numeric_limits::max)() - (MS_PER_SEC - 1)) / MS_PER_SEC) - (SEC_PER_YEAR - 1); ///< Maximum timestamp value + constexpr int64_t MIN_TIMESTAMP = -MAX_TIMESTAMP; ///< Minimum timestamp value + constexpr int64_t MAX_TIMESTAMP_MS = MAX_TIMESTAMP * MS_PER_SEC + (MS_PER_SEC - 1); ///< Maximum timestamp value in milliseconds + constexpr int64_t MIN_TIMESTAMP_MS = MIN_TIMESTAMP * MS_PER_SEC; ///< Minimum timestamp value in milliseconds constexpr int64_t ERROR_TIMESTAMP = 9223372036854770000LL; ///< Error timestamp value constexpr double MAX_OADATE = (std::numeric_limits::max)(); ///< Maximum OLE automation date constexpr double AVG_DAYS_PER_YEAR = 365.25; ///< Average days per year diff --git a/include/time_shield/date_conversions.hpp b/include/time_shield/date_conversions.hpp new file mode 100644 index 00000000..cbba4f9b --- /dev/null +++ b/include/time_shield/date_conversions.hpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DATE_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_DATE_CONVERSIONS_HPP_INCLUDED + +/// \file date_conversions.hpp +/// \brief Conversions related to calendar dates and DateStruct helpers. + +#include "config.hpp" +#include "constants.hpp" +#include "types.hpp" +#include "unix_time_conversions.hpp" +#include "validation.hpp" + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + /// \brief Get the year from the timestamp. + /// + /// This function returns the year of the specified timestamp in seconds since the Unix epoch. + /// + /// \tparam T The return type of the function (default is year_t). + /// \param ts Timestamp in seconds (default is current timestamp). + /// \return Year of the specified timestamp. + template + TIME_SHIELD_CONSTEXPR inline T year_of(ts_t ts = time_shield::ts()) { + return years_since_epoch(ts) + static_cast(UNIX_EPOCH); + } + + /// \brief Get the year from the timestamp in milliseconds. + /// + /// This function returns the year of the specified timestamp in milliseconds since the Unix epoch. + /// + /// \tparam T The return type of the function (default is year_t). + /// \param ts_ms Timestamp in milliseconds (default is current timestamp). + /// \return Year of the specified timestamp. + template + TIME_SHIELD_CONSTEXPR inline T year_of_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { + return year_of(ms_to_sec(ts_ms)); + } + + /// \brief Get the number of days in a year. + /// \param year Year. + /// \return Number of days in the given year. + template + TIME_SHIELD_CONSTEXPR inline T1 num_days_in_year(T2 year) noexcept { + if (is_leap_year_date(year)) return DAYS_PER_LEAP_YEAR; + return DAYS_PER_YEAR; + } + + /// \brief Get the number of days in the current year. + /// + /// This function calculates and returns the number of days in the current year based on the provided timestamp. + /// + /// \param ts Timestamp. + /// \return Number of days in the current year. + template + TIME_SHIELD_CONSTEXPR inline T num_days_in_year_ts(ts_t ts = time_shield::ts()) { + if (is_leap_year_ts(ts)) return DAYS_PER_LEAP_YEAR; + return DAYS_PER_YEAR; + } + + /// \brief Get the day of the week. + /// \tparam T1 Return type (default: Weekday). + /// \tparam T2 Year type. + /// \tparam T3 Month type. + /// \tparam T4 Day type. + /// \param year Year. + /// \param month Month. + /// \param day Day. + /// \return Day of the week (SUN = 0, MON = 1, ... SAT = 6). + template + TIME_SHIELD_CONSTEXPR inline T1 day_of_week_date(T2 year, T3 month, T4 day) { + year_t a = 0; + year_t y = 0; + year_t m = 0; + year_t R = 0; + a = (14 - month) / MONTHS_PER_YEAR; + y = year - a; + m = month + MONTHS_PER_YEAR * a - 2; + R = 7000 + ( day + y + (y / 4) - (y / 100) + (y / 400) + (31 * m) / MONTHS_PER_YEAR); + return static_cast(R % DAYS_PER_WEEK); + } + + /// \ingroup time_structures + /// \brief Get the day of the week from a date structure. + /// + /// This function takes a date structure with fields 'year', 'mon', and 'day', + /// and returns the day of the week (SUN = 0, MON = 1, ... SAT = 6). + /// + /// \param date Structure containing year, month, and day. + /// \return Day of the week (SUN = 0, MON = 1, ... SAT = 6). + template + TIME_SHIELD_CONSTEXPR inline T1 weekday_of_date(const T2& date) { + return day_of_week_date(date.year, date.mon, date.day); + } + + /// \ingroup time_structures + /// \brief Alias for weekday_of_date. + /// \copydoc weekday_of_date + template + TIME_SHIELD_CONSTEXPR inline T1 weekday_from_date(const T2& date) { + return weekday_of_date(date); + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_DATE_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/date_time_conversions.hpp b/include/time_shield/date_time_conversions.hpp new file mode 100644 index 00000000..57b8abae --- /dev/null +++ b/include/time_shield/date_time_conversions.hpp @@ -0,0 +1,1214 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DATE_TIME_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_DATE_TIME_CONVERSIONS_HPP_INCLUDED + +/// \file date_time_conversions.hpp +/// \brief Conversions involving DateTimeStruct and day boundary helpers. + +#include "config.hpp" +#include "constants.hpp" +#include "date_conversions.hpp" +#include "date_struct.hpp" +#include "date_time_struct.hpp" +#include "detail/fast_date.hpp" +#include "detail/floor_math.hpp" +#include "enums.hpp" +#include "time_unit_conversions.hpp" +#include "time_utils.hpp" +#include "types.hpp" +#include "unix_time_conversions.hpp" +#include "validation.hpp" + +#include +#include +#include +#include +#include + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + namespace legacy { + + /// \ingroup time_structures + /// \brief Converts a timestamp to a date-time structure. + /// + /// This function converts a timestamp (usually an integer representing seconds since epoch) + /// to a custom date-time structure. The default type for the timestamp is int64_t. + /// + /// \tparam T1 The date-time structure type to be returned. + /// \tparam T2 The type of the timestamp (default is int64_t). + /// \param ts The timestamp to be converted. + /// \return A date-time structure of type T1. + template + T1 to_date_time(T2 ts) { + // 9223372029693630000 - значение на момент 292277024400 от 2000 года + // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS + // Поэтому пришлось снизить до 9223371890843040000 + constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; + constexpr int64_t BIAS_2000 = 946684800LL; + + int64_t y = MAX_YEAR; + int64_t secs = -((static_cast(ts) - BIAS_2000) - BIAS_292277022000); + + const int64_t n_400_years = secs / SEC_PER_400_YEARS; + secs -= n_400_years * SEC_PER_400_YEARS; + y -= n_400_years * 400LL; + + const int64_t n_100_years = secs / SEC_PER_100_YEARS; + secs -= n_100_years * SEC_PER_100_YEARS; + y -= n_100_years * 100LL; + + const int64_t n_4_years = secs / SEC_PER_4_YEARS; + secs -= n_4_years * SEC_PER_4_YEARS; + y -= n_4_years * 4LL; + + const int64_t n_1_years = secs / SEC_PER_YEAR; + secs -= n_1_years * SEC_PER_YEAR; + y -= n_1_years; + + T1 date_time; + + if (secs == 0) { + date_time.year = y; + date_time.mon = 1; + date_time.day = 1; + return date_time; + } + + date_time.year = y - 1; + const bool is_leap_year = is_leap_year_date(date_time.year); + secs = is_leap_year ? SEC_PER_LEAP_YEAR - secs : SEC_PER_YEAR - secs; + const int days = static_cast(secs / SEC_PER_DAY); + + constexpr int JAN_AND_FEB_DAY_LEAP_YEAR = 60 - 1; + constexpr int TABLE_MONTH_OF_YEAR[] = { + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 31 январь + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 28 февраль + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 31 март + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, // 30 апрель + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, + 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, + 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, + }; + constexpr int TABLE_DAY_OF_YEAR[] = { + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 январь + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, // 28 февраль + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 март + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, // 30 апрель + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + }; + + if (is_leap_year) { + const int prev_days = days - 1; + date_time.day = days == JAN_AND_FEB_DAY_LEAP_YEAR ? (TABLE_DAY_OF_YEAR[prev_days] + 1) : + (days > JAN_AND_FEB_DAY_LEAP_YEAR ? TABLE_DAY_OF_YEAR[prev_days] : TABLE_DAY_OF_YEAR[days]); + date_time.mon = days >= JAN_AND_FEB_DAY_LEAP_YEAR ? TABLE_MONTH_OF_YEAR[prev_days] : TABLE_MONTH_OF_YEAR[days]; + } else { + date_time.day = TABLE_DAY_OF_YEAR[days]; + date_time.mon = TABLE_MONTH_OF_YEAR[days]; + } + + ts_t day_secs = static_cast(detail::floor_mod(secs, SEC_PER_DAY)); + date_time.hour = static_cast(day_secs / SEC_PER_HOUR); + ts_t min_secs = static_cast(day_secs - date_time.hour * SEC_PER_HOUR); + date_time.min = static_cast(min_secs / SEC_PER_MIN); + date_time.sec = static_cast(min_secs - date_time.min * SEC_PER_MIN); +# ifdef TIME_SHIELD_CPP17 + if TIME_SHIELD_IF_CONSTEXPR (std::is_floating_point::value) { + date_time.ms = static_cast(std::round(std::fmod(static_cast(ts), static_cast(MS_PER_SEC)))); + } else date_time.ms = 0; +# else + if (std::is_floating_point::value) { + date_time.ms = static_cast(std::round(std::fmod(static_cast(ts), static_cast(MS_PER_SEC)))); + } else date_time.ms = 0; +# endif + return date_time; + } + + } // namespace legacy + + /// \ingroup time_structures + /// \brief Converts a timestamp to a date-time structure. + /// + /// This function converts a timestamp (usually an integer representing seconds since epoch) + /// to a custom date-time structure. The default type for the timestamp is int64_t. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + /// + /// \tparam T1 The date-time structure type to be returned. + /// \tparam T2 The type of the timestamp (default is int64_t). + /// \param ts The timestamp to be converted. + /// \return A date-time structure of type T1. + template + T1 to_date_time(T2 ts) { + const int64_t whole_sec = static_cast(ts); + const detail::DaySplit split = detail::split_unix_day(whole_sec); + const detail::FastDate date = detail::fast_date_from_days(split.days); + + T1 date_time{}; + date_time.year = static_cast(date.year); + date_time.mon = static_cast(date.month); + date_time.day = static_cast(date.day); + + const ts_t day_secs = static_cast(split.sec_of_day); + date_time.hour = static_cast(day_secs / SEC_PER_HOUR); + const ts_t min_secs = static_cast(day_secs - date_time.hour * SEC_PER_HOUR); + date_time.min = static_cast(min_secs / SEC_PER_MIN); + date_time.sec = static_cast(min_secs - date_time.min * SEC_PER_MIN); +# ifdef TIME_SHIELD_CPP17 + if TIME_SHIELD_IF_CONSTEXPR (std::is_floating_point::value) { + date_time.ms = static_cast(std::round(std::fmod(static_cast(ts), static_cast(MS_PER_SEC)))); + } else date_time.ms = 0; +# else + if (std::is_floating_point::value) { + date_time.ms = static_cast(std::round(std::fmod(static_cast(ts), static_cast(MS_PER_SEC)))); + } else date_time.ms = 0; +# endif + return date_time; + } + + /// \ingroup time_structures + /// \brief Converts a timestamp in milliseconds to a date-time structure with milliseconds. + /// \tparam T The type of the date-time structure to return. + /// \param ts The timestamp in milliseconds to convert. + /// \return T A date-time structure with the corresponding date and time components. + template + inline T to_date_time_ms(ts_ms_t ts) { + const ts_t sec = ms_to_sec(ts); + T date_time = to_date_time(sec); + date_time.ms = ms_of_ts(ts); // Extract and set the ms component + return date_time; + } + + namespace legacy { + + /// \brief Converts a date and time to a timestamp. + /// + /// This function converts a given date and time to a timestamp, which is the number + /// of seconds since the Unix epoch (January 1, 1970). + /// + /// If the `day` is ≥ 1970 and `year` ≤ 31, parameters are assumed to be in DD-MM-YYYY order + /// and are automatically reordered. + /// + /// \tparam T1 The type of the year parameter (default is int64_t). + /// \tparam T2 The type of the other date and time parameters (default is int). + /// \param year The year value. + /// \param month The month value. + /// \param day The day value. + /// \param hour The hour value (default is 0). + /// \param min The minute value (default is 0). + /// \param sec The second value (default is 0). + /// \return Timestamp representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + /// + /// \par Aliases: + /// The following function names are provided as aliases: + /// - `ts(...)` + /// - `get_ts(...)` + /// - `get_timestamp(...)` + /// - `timestamp(...)` + /// - `to_ts(...)` + /// + /// These aliases are macro-generated and behave identically to `to_timestamp`. + /// + /// \sa ts() \sa get_ts() \sa get_timestamp() \sa timestamp() \sa to_ts() + template + TIME_SHIELD_CONSTEXPR inline ts_t to_timestamp( + T1 year, + T2 month, + T2 day, + T2 hour = 0, + T2 min = 0, + T2 sec = 0) { + + if (day >= UNIX_EPOCH && year <= 31) { + return to_timestamp((T1)day, month, (T2)year, hour, min, sec); + } + if (!is_valid_date_time(year, month, day, hour, min, sec)) { + throw std::invalid_argument("Invalid date-time combination"); + } + + int64_t secs = 0; + int64_t years = (static_cast(MAX_YEAR) - year); + + const int64_t n_400_years = years / 400LL; + secs += n_400_years * SEC_PER_400_YEARS; + years -= n_400_years * 400LL; + + const int64_t n_100_years = years / 100LL; + secs += n_100_years * SEC_PER_100_YEARS; + years -= n_100_years * 100LL; + + const int64_t n_4_years = years / 4LL; + secs += n_4_years * SEC_PER_4_YEARS; + years -= n_4_years * 4LL; + + secs += years * SEC_PER_YEAR; + + // 9223372029693630000 - значение на момент 292277024400 от 2000 года + // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS + // Поэтому пришлось снизить до 9223371890843040000 + constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; + constexpr int64_t BIAS_2000 = 946684800LL; + + secs = BIAS_292277022000 - secs; + secs += BIAS_2000; + + if (month == 1 && day == 1 && + hour == 0 && min == 0 && + sec == 0) { + return secs; + } + + constexpr int lmos[] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; + constexpr int mos[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + + secs += (is_leap_year_date(year) ? (lmos[month - 1] + day - 1) : (mos[month - 1] + day - 1)) * SEC_PER_DAY; + secs += SEC_PER_HOUR * hour + SEC_PER_MIN * min + sec; + return secs; + } + + /// \brief Converts a date and time to a timestamp without validation. + /// + /// This function converts a given date and time to a timestamp, which is the number + /// of seconds since the Unix epoch (January 1, 1970). + /// + /// If the `day` is ≥ 1970 and `year` ≤ 31, parameters are assumed to be in DD-MM-YYYY order + /// and are automatically reordered. + /// + /// \tparam T1 The type of the year parameter (default is int64_t). + /// \tparam T2 The type of the other date and time parameters (default is int). + /// \param year The year value. + /// \param month The month value. + /// \param day The day value. + /// \param hour The hour value (default is 0). + /// \param min The minute value (default is 0). + /// \param sec The second value (default is 0). + /// \return Timestamp representing the given date and time. + template + TIME_SHIELD_CONSTEXPR inline ts_t to_timestamp_unchecked( + T1 year, + T2 month, + T2 day, + T2 hour = 0, + T2 min = 0, + T2 sec = 0) noexcept { + + if (day >= UNIX_EPOCH && year <= 31) { + return to_timestamp_unchecked((T1)day, month, (T2)year, hour, min, sec); + } + + int64_t secs = 0; + int64_t years = (static_cast(MAX_YEAR) - year); + + const int64_t n_400_years = years / 400LL; + secs += n_400_years * SEC_PER_400_YEARS; + years -= n_400_years * 400LL; + + const int64_t n_100_years = years / 100LL; + secs += n_100_years * SEC_PER_100_YEARS; + years -= n_100_years * 100LL; + + const int64_t n_4_years = years / 4LL; + secs += n_4_years * SEC_PER_4_YEARS; + years -= n_4_years * 4LL; + + secs += years * SEC_PER_YEAR; + + // 9223372029693630000 - значение на момент 292277024400 от 2000 года + // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS + // Поэтому пришлось снизить до 9223371890843040000 + constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; + constexpr int64_t BIAS_2000 = 946684800LL; + + secs = BIAS_292277022000 - secs; + secs += BIAS_2000; + + if (month == 1 && day == 1 && + hour == 0 && min == 0 && + sec == 0) { + return secs; + } + + constexpr int lmos[] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; + constexpr int mos[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + + secs += (is_leap_year_date(year) ? (lmos[month - 1] + day - 1) : (mos[month - 1] + day - 1)) * SEC_PER_DAY; + secs += SEC_PER_HOUR * hour + SEC_PER_MIN * min + sec; + return secs; + } + + } // namespace legacy + + /// \brief Converts a date and time to a timestamp without validation. + /// + /// This function converts a given date and time to a timestamp, which is the number + /// of seconds since the Unix epoch (January 1, 1970). + /// + /// If the `day` is ≥ 1970 and `year` ≤ 31, parameters are assumed to be in DD-MM-YYYY order + /// and are automatically reordered. + /// + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + /// + /// \tparam T1 The type of the year parameter (default is int64_t). + /// \tparam T2 The type of the other date and time parameters (default is int). + /// \param year The year value. + /// \param month The month value. + /// \param day The day value. + /// \param hour The hour value (default is 0). + /// \param min The minute value (default is 0). + /// \param sec The second value (default is 0). + /// \return Timestamp representing the given date and time. + template + TIME_SHIELD_CONSTEXPR inline ts_t to_timestamp_unchecked( + T1 year, + T2 month, + T2 day, + T2 hour = 0, + T2 min = 0, + T2 sec = 0) noexcept { + + if (day >= UNIX_EPOCH && year <= 31) { + return to_timestamp_unchecked((T1)day, month, (T2)year, hour, min, sec); + } + + const dse_t unix_day = date_to_unix_day(year, month, day); + return static_cast(unix_day * SEC_PER_DAY + + SEC_PER_HOUR * static_cast(hour) + + SEC_PER_MIN * static_cast(min) + + static_cast(sec)); + } + + /// \brief Converts a date and time to a timestamp. + /// + /// This function converts a given date and time to a timestamp, which is the number + /// of seconds since the Unix epoch (January 1, 1970). + /// + /// If the `day` is ≥ 1970 and `year` ≤ 31, parameters are assumed to be in DD-MM-YYYY order + /// and are automatically reordered. + /// + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + /// + /// \tparam T1 The type of the year parameter (default is int64_t). + /// \tparam T2 The type of the other date and time parameters (default is int). + /// \param year The year value. + /// \param month The month value. + /// \param day The day value. + /// \param hour The hour value (default is 0). + /// \param min The minute value (default is 0). + /// \param sec The second value (default is 0). + /// \return Timestamp representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + /// + /// \par Aliases: + /// The following function names are provided as aliases: + /// - `ts(...)` + /// - `get_ts(...)` + /// - `get_timestamp(...)` + /// - `timestamp(...)` + /// - `to_ts(...)` + /// + /// These aliases are macro-generated and behave identically to `to_timestamp`. + /// + /// \sa ts() \sa get_ts() \sa get_timestamp() \sa timestamp() \sa to_ts() + template + TIME_SHIELD_CONSTEXPR inline ts_t to_timestamp( + T1 year, + T2 month, + T2 day, + T2 hour = 0, + T2 min = 0, + T2 sec = 0) { + + if (day >= UNIX_EPOCH && year <= 31) { + return to_timestamp((T1)day, month, (T2)year, hour, min, sec); + } + if (!is_valid_date_time(year, month, day, hour, min, sec)) { + throw std::invalid_argument("Invalid date-time combination"); + } + + return to_timestamp_unchecked(year, month, day, hour, min, sec); + } + + /// \ingroup time_structures + /// \brief Converts a date-time structure to a timestamp. + /// + /// This function converts a given date and time to a timestamp, which is the number + /// of seconds since the Unix epoch (January 1, 1970). + /// + /// \tparam T The type of the date-time structure. + /// \param date_time The date-time structure. + /// \return Timestamp representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + template + TIME_SHIELD_CONSTEXPR inline ts_t dt_to_timestamp( + const T& date_time) { + return to_timestamp( + date_time.year, + date_time.mon, + date_time.day, + date_time.hour, + date_time.min, + date_time.sec + ); + } + + /// \ingroup time_structures + /// \brief Converts a std::tm structure to a timestamp. + /// + /// This function converts a given std::tm structure to a timestamp, which is the number + /// of seconds since the Unix epoch (January 1, 1970). + /// + /// \param timeinfo Pointer to a std::tm structure containing the date and time information. + /// \return Timestamp representing the given date and time. + TIME_SHIELD_CONSTEXPR inline ts_t tm_to_timestamp( + const std::tm *timeinfo) { + return to_timestamp( + static_cast(timeinfo->tm_year + 1900), + static_cast(timeinfo->tm_mon + 1), + static_cast(timeinfo->tm_mday), + static_cast(timeinfo->tm_hour), + static_cast(timeinfo->tm_min), + static_cast(timeinfo->tm_sec) + ); + } + + /// \ingroup time_structures + /// \brief Converts a date-time structure to a timestamp in milliseconds. + /// + /// This function converts a given date and time to a timestamp in milliseconds, + /// which is the number of milliseconds since the Unix epoch (January 1, 1970). + /// + /// \tparam T1 The type of the year parameter (default is year_t). + /// \tparam T2 The type of the month, day, hour, minute, and second parameters (default is int). + /// \param year The year value. + /// \param month The month value. + /// \param day The day value. + /// \param hour The hour value (default is 0). + /// \param min The minute value (default is 0). + /// \param sec The second value (default is 0). + /// \param ms The millisecond value (default is 0). + /// \return Timestamp in milliseconds representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + template + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_timestamp_ms( + T1 year, + T2 month, + T2 day, + T2 hour = 0, + T2 min = 0, + T2 sec = 0, + T2 ms = 0) { + int64_t sec_value = static_cast(to_timestamp(year, month, day, hour, min, sec)); + int64_t ms_value = static_cast(ms); + sec_value += detail::floor_div(ms_value, static_cast(MS_PER_SEC)); + ms_value = detail::floor_mod(ms_value, static_cast(MS_PER_SEC)); + if ((sec_value > 0 && + sec_value > ((std::numeric_limits::max)() - ms_value) / MS_PER_SEC) || + (sec_value < 0 && + sec_value < (std::numeric_limits::min)() / MS_PER_SEC)) { + return ERROR_TIMESTAMP; + } + return static_cast(sec_value * MS_PER_SEC + ms_value); + } + + /// \ingroup time_structures + /// \brief Converts a date-time structure to a timestamp in milliseconds. + /// + /// This function converts a given date and time structure to a timestamp in milliseconds, + /// which is the number of milliseconds since the Unix epoch (January 1, 1970). + /// + /// \tparam T The type of the date-time structure. + /// \param date_time The date-time structure containing year, month, day, hour, minute, second, and millisecond fields. + /// \return Timestamp in milliseconds representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + template + TIME_SHIELD_CONSTEXPR inline ts_ms_t dt_to_timestamp_ms( + const T& date_time) { + int64_t sec_value = static_cast(dt_to_timestamp(date_time)); + int64_t ms_value = static_cast(date_time.ms); + sec_value += detail::floor_div(ms_value, static_cast(MS_PER_SEC)); + ms_value = detail::floor_mod(ms_value, static_cast(MS_PER_SEC)); + if ((sec_value > 0 && + sec_value > ((std::numeric_limits::max)() - ms_value) / MS_PER_SEC) || + (sec_value < 0 && + sec_value < (std::numeric_limits::min)() / MS_PER_SEC)) { + return ERROR_TIMESTAMP; + } + return static_cast(sec_value * MS_PER_SEC + ms_value); + } + + /// \ingroup time_structures + /// \brief Converts a std::tm structure to a timestamp in milliseconds. + /// + /// This function converts a given std::tm structure to a timestamp in milliseconds, + /// which is the number of milliseconds since the Unix epoch (January 1, 1970). + /// + /// \param timeinfo Pointer to a std::tm structure containing the date and time information. + /// \return Timestamp in milliseconds representing the given date and time. + TIME_SHIELD_CONSTEXPR inline ts_t tm_to_timestamp_ms( + const std::tm *timeinfo) { + return sec_to_ms(tm_to_timestamp(timeinfo)); + } + + /// \brief Converts a date and time to a floating-point timestamp. + /// + /// This function converts a given date and time to a floating-point timestamp, + /// which is the number of seconds (with fractional milliseconds) since the Unix epoch + /// (January 1, 1970). + /// + /// \tparam T1 The type of the year parameter (default is year_t). + /// \tparam T2 The type of the month, day, hour, minute, and second parameters (default is int). + /// \tparam T3 The type of the millisecond parameter (default is int). + /// \param year The year value. + /// \param month The month value. + /// \param day The day value. + /// \param hour The hour value (default is 0). + /// \param min The minute value (default is 0). + /// \param sec The second value (default is 0). + /// \param ms The millisecond value (default is 0). + /// \return Floating-point timestamp representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + template + TIME_SHIELD_CONSTEXPR inline fts_t to_ftimestamp( + T1 year, + T2 month, + T2 day, + T2 hour = 0, + T2 min = 0, + T2 sec = 0, + T3 ms = 0) { + int64_t sec_value = static_cast(to_timestamp(year, month, day, hour, min, sec)); + int64_t ms_value = static_cast(ms); + sec_value += detail::floor_div(ms_value, static_cast(MS_PER_SEC)); + ms_value = detail::floor_mod(ms_value, static_cast(MS_PER_SEC)); + return static_cast(sec_value) + + static_cast(ms_value) / static_cast(MS_PER_SEC); + } + + /// \ingroup time_structures + /// \brief Converts a date-time structure to a floating-point timestamp. + /// + /// This function converts a given date and time structure to a floating-point timestamp, + /// which is the number of seconds (with fractional milliseconds) since the Unix epoch + /// (January 1, 1970). + /// + /// \tparam T The type of the date-time structure. + /// \param date_time The date-time structure containing year, month, day, hour, minute, second, and millisecond fields. + /// \return Floating-point timestamp representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + template + TIME_SHIELD_CONSTEXPR inline fts_t dt_to_ftimestamp( + const T& date_time) { + int64_t sec_value = static_cast(to_timestamp(date_time)); + int64_t ms_value = static_cast(date_time.ms); + sec_value += detail::floor_div(ms_value, static_cast(MS_PER_SEC)); + ms_value = detail::floor_mod(ms_value, static_cast(MS_PER_SEC)); + return static_cast(sec_value) + + static_cast(ms_value) / static_cast(MS_PER_SEC); + } + + /// \brief Converts a std::tm structure to a floating-point timestamp. + /// + /// This function converts a given std::tm structure to a floating-point timestamp, + /// which is the number of seconds (with fractional milliseconds) since the Unix epoch + /// (January 1, 1970). + /// + /// \param timeinfo Pointer to the std::tm structure containing the date and time. + /// \return Floating-point timestamp representing the given date and time. + /// \throws std::invalid_argument if the date-time combination is invalid. + TIME_SHIELD_CONSTEXPR inline fts_t tm_to_ftimestamp( + const std::tm* timeinfo) { + return static_cast(tm_to_timestamp(timeinfo)); + } + + /// \brief Get the start of the day timestamp. + /// + /// This function returns the timestamp at the start of the day. + /// The function sets the hours, minutes, and seconds to zero. + /// + /// \param ts Timestamp. + /// \return Start of the day timestamp. + constexpr ts_t start_of_day(ts_t ts = time_shield::ts()) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_DAY); + } + + /// \brief Get timestamp of the start of the previous day. + /// + /// This function returns the timestamp at the start of the previous day. + /// + /// \param ts Timestamp of the current day. + /// \param days Number of days to go back (default is 1). + /// \return Timestamp of the start of the previous day. + template + constexpr ts_t start_of_prev_day(ts_t ts = time_shield::ts(), T days = 1) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_DAY) - SEC_PER_DAY * days; + } + + /// \brief Get the start of the day timestamp in seconds. + /// + /// This function returns the timestamp at the start of the day in seconds. + /// The function sets the hours, minutes, and seconds to zero. + /// + /// \param ts_ms Timestamp in milliseconds. + /// \return Start of the day timestamp in seconds. + constexpr ts_t start_of_day_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return start_of_day(ms_to_sec(ts_ms)); + } + + /// \brief Get the start of the day timestamp in milliseconds. + /// + /// This function returns the timestamp at the start of the day in milliseconds. + /// The function sets the hours, minutes, seconds, and milliseconds to zero. + /// + /// \param ts_ms Timestamp in milliseconds. + /// \return Start of the day timestamp in milliseconds. + constexpr ts_ms_t start_of_day_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return ts_ms - detail::floor_mod(ts_ms, MS_PER_DAY); + } + + /// \brief Get the timestamp of the start of the day after a specified number of days. + /// + /// Calculates the timestamp for the beginning of the day after a specified number of days + /// relative to the given timestamp. + /// + /// \param ts The current timestamp in seconds. + /// \param days The number of days after the current day (default is 1). + /// \return The timestamp in seconds representing the beginning of the specified future day. + template + constexpr ts_t start_of_next_day(ts_t ts, T days = 1) noexcept { + return start_of_day(ts) + days * SEC_PER_DAY; + } + + /// \brief Get the timestamp of the start of the day after a specified number of days. + /// + /// Calculates the timestamp for the beginning of the day after a specified number of days + /// relative to the given timestamp in milliseconds. + /// + /// \param ts_ms The current timestamp in milliseconds. + /// \param days The number of days after the current day (default is 1). + /// \return The timestamp in milliseconds representing the beginning of the specified future day. + template + constexpr ts_ms_t start_of_next_day_ms(ts_ms_t ts_ms, T days = 1) noexcept { + return start_of_day_ms(ts_ms) + days * MS_PER_DAY; + } + + /// \brief Calculate the timestamp for a specified number of days in the future. + /// + /// Adds the given number of days to the provided timestamp, without adjusting to the start of the day. + /// + /// \param ts The current timestamp in seconds. + /// \param days The number of days to add to the current timestamp (default is 1). + /// \return The timestamp in seconds after adding the specified number of days. + template + constexpr ts_t next_day(ts_t ts, T days = 1) noexcept { + return ts + days * SEC_PER_DAY; + } + + /// \brief Calculate the timestamp for a specified number of days in the future (milliseconds). + /// + /// Adds the given number of days to the provided timestamp, without adjusting to the start of the day. + /// + /// \param ts_ms The current timestamp in milliseconds. + /// \param days The number of days to add to the current timestamp (default is 1). + /// \return The timestamp in milliseconds after adding the specified number of days. + template + constexpr ts_ms_t next_day_ms(ts_ms_t ts_ms, T days = 1) noexcept { + return ts_ms + days * MS_PER_DAY; + } + + /// \brief Get the timestamp at the end of the day. + /// + /// This function sets the hour to 23, minute to 59, and second to 59. + /// + /// \param ts Timestamp. + /// \return Timestamp at the end of the day. + constexpr ts_t end_of_day(ts_t ts = time_shield::ts()) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_DAY) + SEC_PER_DAY - 1; + } + + /// \brief Get the timestamp at the end of the day in seconds. + /// + /// This function sets the hour to 23, minute to 59, and second to 59. + /// + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at the end of the day in seconds. + constexpr ts_t end_of_day_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return end_of_day(ms_to_sec(ts_ms)); + } + + /// \brief Get the timestamp at the end of the day in milliseconds. + /// + /// This function sets the hour to 23, minute to 59, second to 59, and millisecond to 999. + /// + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at the end of the day in milliseconds. + constexpr ts_ms_t end_of_day_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return ts_ms - detail::floor_mod(ts_ms, MS_PER_DAY) + MS_PER_DAY - 1; + } + + /// \brief Get the timestamp of the start of the year. + /// \param year Year. + /// \return Timestamp at 00:00:00 of the first day of the year. + template + TIME_SHIELD_CONSTEXPR inline ts_t start_of_year_date(T year) { + const ts_t year_ts = to_timestamp(year, 1, 1); + + return start_of_day(year_ts); + } + + /// \brief Get the timestamp in milliseconds of the start of the year. + /// + /// This function returns the timestamp at the start of the specified year in milliseconds. + /// + /// \param year Year. + /// \return Timestamp of the start of the year in milliseconds. + /// \throws std::invalid_argument if the date-time combination is invalid. + template + TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_year_date_ms(T year) { + return sec_to_ms(start_of_year_date(year)); + } + + /// \brief Get the start of the year timestamp. + /// + /// This function resets the days, months, hours, minutes, and seconds of the given timestamp + /// to the beginning of the year. + /// + /// \param ts Timestamp. + /// \return Start of the year timestamp. + TIME_SHIELD_CONSTEXPR inline ts_t start_of_year(ts_t ts) noexcept { + constexpr ts_t BIAS_2100 = 4102444800; + if (ts >= 0 && ts < BIAS_2100) { + constexpr ts_t SEC_PER_YEAR_X2 = SEC_PER_YEAR * 2; + ts_t year_start_ts = detail::floor_mod(ts, SEC_PER_4_YEARS); + if (year_start_ts < SEC_PER_YEAR) { + return ts - year_start_ts; + } else if (year_start_ts < SEC_PER_YEAR_X2) { + return ts + SEC_PER_YEAR - year_start_ts; + } else if (year_start_ts < (SEC_PER_YEAR_X2 + SEC_PER_LEAP_YEAR)) { + return ts + SEC_PER_YEAR_X2 - year_start_ts; + } + return ts + (SEC_PER_YEAR_X2 + SEC_PER_LEAP_YEAR) - year_start_ts; + } + + constexpr ts_t BIAS_2000 = 946684800; + ts_t secs = ts - BIAS_2000; + + ts_t offset_y400 = detail::floor_mod(secs, SEC_PER_400_YEARS); + ts_t start_ts = secs - offset_y400 + BIAS_2000; + secs = offset_y400; + + if (secs >= SEC_PER_FIRST_100_YEARS) { + secs -= SEC_PER_FIRST_100_YEARS; + start_ts += SEC_PER_FIRST_100_YEARS; + while (secs >= SEC_PER_100_YEARS) { + secs -= SEC_PER_100_YEARS; + start_ts += SEC_PER_100_YEARS; + } + + constexpr ts_t SEC_PER_4_YEARS_V2 = 4 * SEC_PER_YEAR; + if (secs >= SEC_PER_4_YEARS_V2) { + secs -= SEC_PER_4_YEARS_V2; + start_ts += SEC_PER_4_YEARS_V2; + } else { + start_ts += secs - detail::floor_mod(secs, SEC_PER_YEAR); + return start_ts; + } + } + + ts_t offset_4y = detail::floor_mod(secs, SEC_PER_4_YEARS); + start_ts += secs - offset_4y; + secs = offset_4y; + + if (secs >= SEC_PER_LEAP_YEAR) { + secs -= SEC_PER_LEAP_YEAR; + start_ts += SEC_PER_LEAP_YEAR; + start_ts += secs - detail::floor_mod(secs, SEC_PER_YEAR); + return start_ts; + } + + start_ts += secs - detail::floor_mod(secs, SEC_PER_YEAR); + return start_ts; + } + + /// \brief Get the timestamp at the start of the year in milliseconds. + /// \param ts_ms Timestamp in milliseconds. + /// \return Timestamp at 00:00:00.000 of the first day of the year. + TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { + return sec_to_ms(start_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Get the end-of-year timestamp. + /// + /// This function finds the last timestamp of the current year. + /// + /// \param ts Timestamp. + /// \return End-of-year timestamp. + TIME_SHIELD_CONSTEXPR inline ts_t end_of_year(ts_t ts = time_shield::ts()) { + const ts_t year_start = start_of_year(ts); + const ts_t year_days = static_cast(num_days_in_year_ts(ts)); + return year_start + year_days * SEC_PER_DAY - 1; + } + + /// \brief Get the timestamp in milliseconds of the end of the year. + /// + /// This function finds the last millisecond of the current year in milliseconds. + /// + /// \param ts_ms Timestamp in milliseconds. + /// \return End-of-year timestamp in milliseconds. + template + TIME_SHIELD_CONSTEXPR inline ts_ms_t end_of_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { + return sec_to_ms(end_of_year(ms_to_sec(ts_ms))) + (MS_PER_SEC - 1); + } + + /// \brief Get the day of the year. + /// + /// This function returns the day of the year for the specified timestamp. + /// + /// \param ts Timestamp. + /// \return Day of the year. + template + inline T day_of_year(ts_t ts = time_shield::ts()) { + return static_cast(((ts - start_of_year(ts)) / SEC_PER_DAY) + 1); + } + + /// \brief Get the month of the year. + /// + /// This function returns the month of the year for the specified timestamp. + /// + /// \param ts Timestamp. + /// \return Month of the year. + template + TIME_SHIELD_CONSTEXPR inline T month_of_year(ts_t ts) noexcept { + constexpr int JAN_AND_FEB_DAY_LEAP_YEAR = 60; + constexpr int TABLE_MONTH_OF_YEAR[] = { + 0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 31 январь + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 28 февраль + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 31 март + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, // 30 апрель + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, + 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, + 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, + }; + const size_t dy = day_of_year(ts); + return static_cast((is_leap_year(ts) && dy >= JAN_AND_FEB_DAY_LEAP_YEAR) ? TABLE_MONTH_OF_YEAR[dy - 1] : TABLE_MONTH_OF_YEAR[dy]); + } + + /// \brief Get the day of the month. + /// + /// This function returns the day of the month for the specified timestamp. + /// + /// \param ts Timestamp. + /// \return Day of the month. + template + TIME_SHIELD_CONSTEXPR inline T day_of_month(ts_t ts = time_shield::ts()) { + constexpr int JAN_AND_FEB_DAY_LEAP_YEAR = 60; + // таблица для обычного года, не високосного + constexpr int TABLE_DAY_OF_YEAR[] = { + 0, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 январь + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, // 28 февраль + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 март + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, // 30 апрель + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, + 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + }; + const size_t dy = day_of_year(ts); + if(is_leap_year(ts)) { + if(dy == JAN_AND_FEB_DAY_LEAP_YEAR) return TABLE_DAY_OF_YEAR[dy - 1] + 1; + if(dy > JAN_AND_FEB_DAY_LEAP_YEAR) return TABLE_DAY_OF_YEAR[dy - 1]; + } + return TABLE_DAY_OF_YEAR[dy]; + } + + /// \brief Get the number of days in a month. + /// + /// This function calculates and returns the number of days in the specified month and year. + /// + /// \param year Year as an integer. + /// \param month Month as an integer. + /// \return The number of days in the given month and year. + template + TIME_SHIELD_CONSTEXPR T1 num_days_in_month(T2 year, T3 month) noexcept { + constexpr T1 num_days[13] = {0,31,30,31,30,31,30,31,31,30,31,30,31}; + return (month > MONTHS_PER_YEAR || month < 0) + ? static_cast(0) + : (month == FEB ? static_cast(is_leap_year_date(year) ? 29 : 28) : num_days[month]); + } + + /// \brief Get the number of days in the month of the given timestamp. + /// + /// This function calculates and returns the number of days in the month of the specified timestamp. + /// + /// \param ts The timestamp to extract month and year from. + /// \return The number of days in the month of the given timestamp. + template + TIME_SHIELD_CONSTEXPR T1 num_days_in_month_ts(ts_t ts = time_shield::ts()) noexcept { + constexpr T1 num_days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; + const int month = month_of_year(ts); + if (month == FEB) { + return is_leap_year(ts) ? 29 : 28; + } + return num_days[month]; + } + + /// \brief Get the second of the week day from a timestamp. + /// \param ts Timestamp. + /// \return Weekday (SUN = 0, MON = 1, ... SAT = 6). + template + constexpr T weekday_of_ts(ts_t ts) noexcept { + const ts_t days = detail::floor_div(ts, SEC_PER_DAY); + return static_cast(detail::floor_mod(days + THU, DAYS_PER_WEEK)); + } + + /// \brief Get the weekday from a timestamp in milliseconds. + /// \param ts_ms Timestamp in milliseconds. + /// \return Weekday (SUN = 0, MON = 1, ... SAT = 6). + template + constexpr T weekday_of_ts_ms(ts_ms_t ts_ms) { + return weekday_of_ts(ms_to_sec(ts_ms)); + } + + /// \brief Alias for weekday_of_ts. + /// \copydoc weekday_of_ts + template + constexpr T get_weekday_from_ts(ts_t ts) noexcept { + return weekday_of_ts(ts); + } + + /// \brief Alias for weekday_of_ts_ms. + /// \copydoc weekday_of_ts_ms + template + constexpr T get_weekday_from_ts_ms(ts_ms_t ts_ms) { + return weekday_of_ts_ms(ts_ms); + } + + /// \brief Get the timestamp at the start of the current month. + /// + /// This function returns the timestamp at the start of the current month, + /// setting the day to the first day of the month and the time to 00:00:00. + /// + /// \param ts Timestamp (default is current timestamp) + /// \return Timestamp at the start of the current month + TIME_SHIELD_CONSTEXPR inline ts_t start_of_month(ts_t ts = time_shield::ts()) { + return start_of_day(ts) - (day_of_month(ts) - 1) * SEC_PER_DAY; + } + + /// \brief Get the last timestamp of the current month. + /// + /// This function returns the last timestamp of the current month, + /// setting the day to the last day of the month and the time to 23:59:59. + /// + /// \param ts Timestamp (default is current timestamp) + /// \return Last timestamp of the current month + TIME_SHIELD_CONSTEXPR inline ts_t end_of_month(ts_t ts = time_shield::ts()) { + return end_of_day(ts) + (num_days_in_month_ts(ts) - day_of_month(ts)) * SEC_PER_DAY; + } + + /// \brief Get the timestamp of the last Sunday of the current month. + /// + /// This function returns the timestamp of the last Sunday of the current month, + /// setting the day to the last Sunday and the time to 00:00:00. + /// + /// \param ts Timestamp (default is current timestamp) + /// \return Timestamp of the last Sunday of the current month + TIME_SHIELD_CONSTEXPR inline ts_t last_sunday_of_month(ts_t ts = time_shield::ts()) { + return end_of_month(ts) - weekday_of_ts(ts) * SEC_PER_DAY; + } + + /// \brief Get the day of the last Sunday of the given month and year. + /// + /// This function returns the day of the last Sunday of the specified month and year. + /// + /// \param year Year + /// \param month Month (1 = January, 12 = December) + /// \return Day of the last Sunday of the given month and year + template + TIME_SHIELD_CONSTEXPR inline T1 last_sunday_month_day(T2 year, T3 month) { + const T1 days = num_days_in_month(year, month); + return days - day_of_week_date(year, month, days); + } + + /// \brief Get the timestamp of the beginning of the week. + /// + /// This function finds the timestamp of the beginning of the week, + /// which corresponds to the start of Sunday. + /// + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the beginning of the week. + constexpr ts_t start_of_week(ts_t ts = time_shield::ts()) { + return start_of_day(ts) - weekday_of_ts(ts) * SEC_PER_DAY; + } + + /// \brief Get the timestamp of the end of the week. + /// + /// This function finds the timestamp of the end of the week, + /// which corresponds to the end of Saturday. + /// + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the end of the week. + constexpr ts_t end_of_week(ts_t ts = time_shield::ts()) { + return start_of_day(ts) + (DAYS_PER_WEEK - weekday_of_ts(ts)) * SEC_PER_DAY - 1; + } + + /// \brief Get the timestamp of the start of Saturday. + /// + /// This function finds the timestamp of the beginning of the day on Saturday, + /// which corresponds to the start of Saturday. + /// + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the start of Saturday. + constexpr ts_t start_of_saturday(ts_t ts = time_shield::ts()) { + return start_of_day(ts) + (SAT - weekday_of_ts(ts)) * SEC_PER_DAY; + } + + + /// \brief Get the timestamp at the start of the hour. + /// + /// This function sets the minute and second to zero. + /// + /// \param ts Timestamp (default: current timestamp). + /// \return Timestamp at the start of the hour. + constexpr ts_t start_of_hour(ts_t ts = time_shield::ts()) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_HOUR); + } + + /// \brief Get the timestamp at the start of the hour. + /// + /// This function sets the minute and second to zero. + /// + /// \param ts_ms Timestamp in milliseconds (default: current timestamp in milliseconds). + /// \return Timestamp at the start of the hour in seconds. + constexpr ts_t start_of_hour_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return start_of_hour(ms_to_sec(ts_ms)); + } + + /// \brief Get the timestamp at the start of the hour. + /// This function sets the minute and second to zero. + /// \param ts_ms Timestamp in milliseconds (default: current timestamp in milliseconds). + /// \return Timestamp at the start of the hour in milliseconds. + constexpr ts_ms_t start_of_hour_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return ts_ms - detail::floor_mod(ts_ms, MS_PER_HOUR); + } + + /// \brief Get the timestamp at the end of the hour. + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the end of the hour. + constexpr ts_t end_of_hour(ts_t ts = time_shield::ts()) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_HOUR) + SEC_PER_HOUR - 1; + } + + /// \brief Get the timestamp at the end of the hour in seconds. + /// \param ts_ms Timestamp in milliseconds (default: current timestamp). + /// \return Returns the timestamp of the end of the hour in seconds. + constexpr ts_t end_of_hour_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return end_of_hour(ms_to_sec(ts_ms)); + } + + /// \brief Get the timestamp at the end of the hour in milliseconds. + /// \param ts_ms Timestamp in milliseconds (default: current timestamp). + /// \return Returns the timestamp of the end of the hour in milliseconds. + constexpr ts_ms_t end_of_hour_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return ts_ms - detail::floor_mod(ts_ms, MS_PER_HOUR) + MS_PER_HOUR - 1; + } + + /// \brief Get the timestamp of the beginning of the minute. + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the beginning of the minute. + constexpr ts_t start_of_min(ts_t ts = time_shield::ts()) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_MIN); + } + + /// \brief Get the timestamp of the end of the minute. + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the end of the minute. + constexpr ts_t end_of_min(ts_t ts = time_shield::ts()) noexcept { + return ts - detail::floor_mod(ts, SEC_PER_MIN) + SEC_PER_MIN - 1; + } + + /// \brief Get minute of day. + /// This function returns a value between 0 to 1439 (minute of day). + /// \param ts Timestamp in seconds (default: current timestamp). + /// \return Minute of day. + template + constexpr T min_of_day(ts_t ts = time_shield::ts()) noexcept { + const ts_t minutes = detail::floor_div(ts, SEC_PER_MIN); + return static_cast(detail::floor_mod(minutes, MIN_PER_DAY)); + } + + /// \brief Get hour of day. + /// This function returns a value between 0 to 23. + /// \param ts Timestamp in seconds (default: current timestamp). + /// \return Hour of day. + template + constexpr T hour_of_day(ts_t ts = time_shield::ts()) noexcept { + const ts_t hours = detail::floor_div(ts, SEC_PER_HOUR); + return static_cast(detail::floor_mod(hours, HOURS_PER_DAY)); + } + + /// \brief Get minute of hour. + /// This function returns a value between 0 to 59. + /// \param ts Timestamp in seconds (default: current timestamp). + /// \return Minute of hour. + template + constexpr T min_of_hour(ts_t ts = time_shield::ts()) noexcept { + const ts_t minutes = detail::floor_div(ts, SEC_PER_MIN); + return static_cast(detail::floor_mod(minutes, MIN_PER_HOUR)); + } + + /// \brief Get the timestamp of the start of the period. + /// \param p Period duration in seconds. + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the start of the period. + template + constexpr ts_t start_of_period(T p, ts_t ts = time_shield::ts()) { + return ts - detail::floor_mod(ts, static_cast(p)); + } + + /// \brief Get the timestamp of the end of the period. + /// \param p Period duration in seconds. + /// \param ts Timestamp (default: current timestamp). + /// \return Returns the timestamp of the end of the period. + template + constexpr ts_t end_of_period(T p, ts_t ts = time_shield::ts()) { + return ts - detail::floor_mod(ts, static_cast(p)) + p - 1; + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_DATE_TIME_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/detail/fast_date.hpp b/include/time_shield/detail/fast_date.hpp new file mode 100644 index 00000000..6563b657 --- /dev/null +++ b/include/time_shield/detail/fast_date.hpp @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DETAIL_FAST_DATE_HPP_INCLUDED +#define _TIME_SHIELD_DETAIL_FAST_DATE_HPP_INCLUDED + +/// \file fast_date.hpp +/// \brief Fast date conversion helpers. + +#include "mul_hi.hpp" + +#include + +namespace time_shield { +namespace detail { + + namespace { + constexpr int16_t k_doy_from_march[12] = { + 0, // Mar + 31, // Apr + 61, // May + 92, // Jun + 122, // Jul + 153, // Aug + 184, // Sep + 214, // Oct + 245, // Nov + 275, // Dec + 306, // Jan + 337 // Feb + }; + } // namespace + + struct DaySplit { + int64_t days; + int64_t sec_of_day; + }; + + /// \brief Split UNIX seconds into whole days and seconds-of-day. + TIME_SHIELD_CONSTEXPR inline DaySplit split_unix_day(ts_t p_ts) noexcept { + int64_t days = p_ts / SEC_PER_DAY; + int64_t sec_of_day = p_ts % SEC_PER_DAY; + if (sec_of_day < 0) { + sec_of_day += SEC_PER_DAY; + days -= 1; + } + return {days, sec_of_day}; + } + + struct FastDate { + int64_t year; + int month; + int day; + }; + + /// \brief Convert date to days since Unix epoch using a fast constexpr algorithm. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + TIME_SHIELD_CONSTEXPR inline int64_t fast_days_from_date_constexpr( + int64_t p_year, + int p_month, + int p_day) noexcept { + const int month_adjust = (p_month <= 2 ? 1 : 0); + const int64_t y = p_year - month_adjust; + int m = p_month - 3; + if (m < 0) { + m += 12; + } + + if (y >= 0) { + const uint64_t y_u = static_cast(y); + const uint64_t era = y_u / 400U; + const uint64_t yoe = y_u - era * 400U; + const uint64_t doy = static_cast(k_doy_from_march[m]) + static_cast(p_day - 1); + const uint64_t doe = yoe * 365U + yoe / 4U - yoe / 100U + doy; + return static_cast(era * 146097U + doe) - 719468; + } + + const int64_t era = (y - 399) / 400; + const int64_t yoe = y - era * 400; + const int64_t doy = static_cast(k_doy_from_march[m]) + static_cast(p_day - 1); + const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; + } + + /// \brief Convert date to days since Unix epoch using a fast algorithm. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + inline int64_t fast_days_from_date(int64_t p_year, int p_month, int p_day) noexcept { + const int month_adjust = (p_month <= 2 ? 1 : 0); + const int64_t y = p_year - month_adjust; + int m = p_month - 3; + if (m < 0) { + m += 12; + } + + if (y >= 0) { + const uint64_t y_u = static_cast(y); + const uint64_t era = y_u / 400U; + const uint64_t yoe = y_u - era * 400U; + const uint64_t doy = static_cast(k_doy_from_march[m]) + static_cast(p_day - 1); + const uint64_t doe = yoe * 365U + yoe / 4U - yoe / 100U + doy; + return static_cast(era * 146097U + doe) - 719468; + } + + const int64_t era = (y - 399) / 400; + const int64_t yoe = y - era * 400; + const int64_t doy = static_cast(k_doy_from_march[m]) + static_cast(p_day - 1); + const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; + } + + /// \brief Convert days since Unix epoch to date using a fast constexpr algorithm. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + TIME_SHIELD_CONSTEXPR inline FastDate fast_date_from_days_constexpr(int64_t p_days) noexcept { + constexpr uint64_t ERAS = 4726498270ULL; + constexpr int64_t D_SHIFT = static_cast(146097ULL * ERAS - 719469ULL); + constexpr int64_t Y_SHIFT = static_cast(400ULL * ERAS - 1ULL); + constexpr uint64_t C1 = 505054698555331ULL; + constexpr uint64_t C2 = 50504432782230121ULL; + constexpr uint64_t C3 = 8619973866219416ULL; + constexpr uint64_t YPT_SCALE = 782432ULL; + constexpr uint64_t YPT_BUMP_THRESHOLD = 126464ULL; + constexpr uint64_t SHIFT_JAN_FEB = 191360ULL; + constexpr uint64_t SHIFT_OTHER = 977792ULL; + + const uint64_t rev = static_cast(D_SHIFT - p_days); + const uint64_t cen = mul_shift_u64_constexpr(rev, C1); + const uint64_t jul = rev + cen - (cen / 4U); + + const uint64_t num_hi = mul_shift_u64_constexpr(jul, C2); + const uint64_t num_low = jul * C2; + const uint64_t yrs = static_cast(Y_SHIFT) - num_hi; + + const uint64_t ypt = mul_shift_u64_constexpr(YPT_SCALE, num_low); + const bool bump = ypt < YPT_BUMP_THRESHOLD; + const uint64_t shift = bump ? SHIFT_JAN_FEB : SHIFT_OTHER; + + const uint64_t N = (yrs & 3ULL) * 512ULL + shift - ypt; + const uint64_t d = mul_shift_u64_constexpr((N & 0xFFFFULL), C3); + + return FastDate{ + static_cast(yrs + (bump ? 1U : 0U)), + static_cast(N >> 16), + static_cast(d + 1U) + }; + } + + /// \brief Convert days since Unix epoch to date using a fast algorithm. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + inline FastDate fast_date_from_days(int64_t p_days) noexcept { + constexpr uint64_t ERAS = 4726498270ULL; + constexpr int64_t D_SHIFT = static_cast(146097ULL * ERAS - 719469ULL); + constexpr int64_t Y_SHIFT = static_cast(400ULL * ERAS - 1ULL); + constexpr uint64_t C1 = 505054698555331ULL; + constexpr uint64_t C2 = 50504432782230121ULL; + constexpr uint64_t C3 = 8619973866219416ULL; + constexpr uint64_t YPT_SCALE = 782432ULL; + constexpr uint64_t YPT_BUMP_THRESHOLD = 126464ULL; + constexpr uint64_t SHIFT_JAN_FEB = 191360ULL; + constexpr uint64_t SHIFT_OTHER = 977792ULL; + + const uint64_t rev = static_cast(D_SHIFT - p_days); + const uint64_t cen = mul_shift_u64(rev, C1); + const uint64_t jul = rev + cen - (cen / 4U); + + const uint64_t num_hi = mul_shift_u64(jul, C2); + const uint64_t num_low = jul * C2; + const uint64_t yrs = static_cast(Y_SHIFT) - num_hi; + + const uint64_t ypt = mul_shift_u64(YPT_SCALE, num_low); + const bool bump = ypt < YPT_BUMP_THRESHOLD; + const uint64_t shift = bump ? SHIFT_JAN_FEB : SHIFT_OTHER; + + const uint64_t N = (yrs & 3ULL) * 512ULL + shift - ypt; + const uint64_t d = mul_shift_u64((N & 0xFFFFULL), C3); + + FastDate result{}; + result.day = static_cast(d + 1U); + result.month = static_cast(N >> 16); + result.year = static_cast(yrs + (bump ? 1U : 0U)); + return result; + } + + /// \brief Convert days since Unix epoch to year using a fast constexpr algorithm. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + TIME_SHIELD_CONSTEXPR inline int64_t fast_year_from_days_constexpr(int64_t p_days) noexcept { + constexpr uint64_t ERAS = 4726498270ULL; + constexpr int64_t D_SHIFT = static_cast(146097ULL * ERAS - 719469ULL); + constexpr int64_t Y_SHIFT = static_cast(400ULL * ERAS - 1ULL); + constexpr uint64_t C1 = 505054698555331ULL; + constexpr uint64_t C2 = 50504432782230121ULL; + constexpr uint64_t YPT_SCALE = 782432ULL; + constexpr uint64_t YPT_BUMP_THRESHOLD = 126464ULL; + + const uint64_t rev = static_cast(D_SHIFT - p_days); + const uint64_t cen = mul_shift_u64_constexpr(rev, C1); + const uint64_t jul = rev + cen - (cen / 4U); + + const uint64_t num_hi = mul_shift_u64_constexpr(jul, C2); + const uint64_t num_low = jul * C2; + const uint64_t yrs = static_cast(Y_SHIFT) - num_hi; + + const uint64_t ypt = mul_shift_u64_constexpr(YPT_SCALE, num_low); + const bool bump = ypt < YPT_BUMP_THRESHOLD; + return static_cast(yrs + (bump ? 1U : 0U)); + } + + /// \brief Convert days since Unix epoch to year using a fast algorithm. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + inline int64_t fast_year_from_days(int64_t p_days) noexcept { + constexpr uint64_t ERAS = 4726498270ULL; + constexpr int64_t D_SHIFT = static_cast(146097ULL * ERAS - 719469ULL); + constexpr int64_t Y_SHIFT = static_cast(400ULL * ERAS - 1ULL); + constexpr uint64_t C1 = 505054698555331ULL; + constexpr uint64_t C2 = 50504432782230121ULL; + constexpr uint64_t YPT_SCALE = 782432ULL; + constexpr uint64_t YPT_BUMP_THRESHOLD = 126464ULL; + + const uint64_t rev = static_cast(D_SHIFT - p_days); + const uint64_t cen = mul_shift_u64(rev, C1); + const uint64_t jul = rev + cen - (cen / 4U); + + const uint64_t num_hi = mul_shift_u64(jul, C2); + const uint64_t num_low = jul * C2; + const uint64_t yrs = static_cast(Y_SHIFT) - num_hi; + + const uint64_t ypt = mul_shift_u64(YPT_SCALE, num_low); + const bool bump = ypt < YPT_BUMP_THRESHOLD; + return static_cast(yrs + (bump ? 1U : 0U)); + } + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_DETAIL_FAST_DATE_HPP_INCLUDED diff --git a/include/time_shield/detail/floor_math.hpp b/include/time_shield/detail/floor_math.hpp new file mode 100644 index 00000000..baf6f370 --- /dev/null +++ b/include/time_shield/detail/floor_math.hpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DETAIL_FLOOR_MATH_HPP_INCLUDED +#define _TIME_SHIELD_DETAIL_FLOOR_MATH_HPP_INCLUDED + +/// \file floor_math.hpp +/// \brief Floor division and modulus helpers. + +namespace time_shield { +namespace detail { + + /// \brief Floor division for positive divisor. + template + TIME_SHIELD_CONSTEXPR inline T floor_div(T a, T b) noexcept { + T q = a / b; + T r = a % b; + if (r != 0 && a < 0) --q; + return q; + } + + /// \brief Floor-mod for positive modulus (returns r in [0..b)). + template + TIME_SHIELD_CONSTEXPR inline T floor_mod(T a, T b) noexcept { + T r = a % b; + if (r < 0) r += b; + return r; + } + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_DETAIL_FLOOR_MATH_HPP_INCLUDED diff --git a/include/time_shield/detail/mul_hi.hpp b/include/time_shield/detail/mul_hi.hpp new file mode 100644 index 00000000..f1d80bd5 --- /dev/null +++ b/include/time_shield/detail/mul_hi.hpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_DETAIL_MUL_HI_HPP_INCLUDED +#define _TIME_SHIELD_DETAIL_MUL_HI_HPP_INCLUDED + +/// \file mul_hi.hpp +/// \brief Helpers for 64-bit multiply-high operations. + +#include + +#if defined(_MSC_VER) +# include +#endif + +namespace time_shield { +namespace detail { + + /// \brief Return the high 64 bits of a 64x64-bit multiplication (constexpr variant). + TIME_SHIELD_CONSTEXPR inline uint64_t mul_hi_u64_constexpr(uint64_t p_a, uint64_t p_b) noexcept { + const uint64_t a_low = p_a & 0xFFFFFFFFULL; + const uint64_t a_high = p_a >> 32; + const uint64_t b_low = p_b & 0xFFFFFFFFULL; + const uint64_t b_high = p_b >> 32; + + const uint64_t p0 = a_low * b_low; + const uint64_t p1 = a_low * b_high; + const uint64_t p2 = a_high * b_low; + const uint64_t p3 = a_high * b_high; + + const uint64_t carry = ((p0 >> 32) + (p1 & 0xFFFFFFFFULL) + (p2 & 0xFFFFFFFFULL)) >> 32; + return p3 + (p1 >> 32) + (p2 >> 32) + carry; + } + + /// \brief Return the high 64 bits of a 64x64-bit multiplication. + inline uint64_t mul_hi_u64(uint64_t p_a, uint64_t p_b) noexcept { +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + uint64_t high = 0; + (void)_umul128(p_a, p_b, &high); + return high; +#else + const __uint128_t product = static_cast<__uint128_t>(p_a) * static_cast<__uint128_t>(p_b); + return static_cast(product >> 64); +#endif + } + + /// \brief Alias for mul_hi_u64 used for shift-by-64 operations. + inline uint64_t mul_shift_u64(uint64_t p_x, uint64_t p_c) noexcept { + return mul_hi_u64(p_x, p_c); + } + + /// \brief Alias for mul_hi_u64_constexpr used for shift-by-64 operations. + TIME_SHIELD_CONSTEXPR inline uint64_t mul_shift_u64_constexpr(uint64_t p_x, uint64_t p_c) noexcept { + return mul_hi_u64_constexpr(p_x, p_c); + } + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_DETAIL_MUL_HI_HPP_INCLUDED diff --git a/include/time_shield/iso_week_conversions.hpp b/include/time_shield/iso_week_conversions.hpp new file mode 100644 index 00000000..1f21a67b --- /dev/null +++ b/include/time_shield/iso_week_conversions.hpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_ISO_WEEK_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_ISO_WEEK_CONVERSIONS_HPP_INCLUDED + +/// \file iso_week_conversions.hpp +/// \brief Conversions and utilities for ISO week dates (ISO 8601). +/// +/// This file provides helpers to convert between calendar dates, timestamps, and ISO week dates, +/// as well as formatting and parsing helpers for ISO week-date strings. + +#include "config.hpp" +#include "constants.hpp" +#include "date_struct.hpp" +#include "date_time_struct.hpp" +#include "iso_week_struct.hpp" +#include "time_conversions.hpp" +#include "unix_time_conversions.hpp" + +#include +#include +#include +#include +#include +#include + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + /// \brief Convert Weekday enum to ISO weekday (Mon=1 .. Sun=7). + /// \param weekday Weekday enum value. + /// \return ISO weekday number. + TIME_SHIELD_CONSTEXPR inline int iso_weekday_from_weekday(Weekday weekday) noexcept { + return static_cast((static_cast(weekday) + DAYS_PER_WEEK - 1) % DAYS_PER_WEEK) + 1; + } + + /// \brief Get ISO weekday for a calendar date. + /// \param year Year component. + /// \param month Month component. + /// \param day Day component. + /// \return ISO weekday number (1=Monday .. 7=Sunday). + template + TIME_SHIELD_CONSTEXPR inline int iso_weekday_of_date(Y year, M month, D day) { + return iso_weekday_from_weekday(day_of_week_date(year, month, day)); + } + + /// \brief Convert calendar date to ISO week date. + /// \param year Year component. + /// \param month Month component. + /// \param day Day component. + /// \return ISO week date representation. + template + inline IsoWeekDateStruct to_iso_week_date(Y year, M month, D day) { + const int iso_weekday = iso_weekday_of_date(year, month, day); + const dse_t unix_day = date_to_unix_day(year, month, day); + const dse_t thursday_day = unix_day + static_cast(4 - iso_weekday); + + const DateTimeStruct thursday_date = to_date_time(unix_day_to_ts(thursday_day)); + const year_t iso_year = thursday_date.year; + + const dse_t jan4_day = date_to_unix_day(iso_year, 1, 4); + const int jan4_iso_weekday = iso_weekday_of_date(iso_year, 1, 4); + const dse_t first_thursday = jan4_day + static_cast(4 - jan4_iso_weekday); + + const int32_t week = static_cast((thursday_day - first_thursday) / DAYS_PER_WEEK + 1); + return create_iso_week_date_struct(iso_year, week, static_cast(iso_weekday)); + } + + /// \brief Convert DateStruct to ISO week date. + /// \param date DateStruct instance. + /// \return ISO week date representation. + inline IsoWeekDateStruct to_iso_week_date(const DateStruct& date) { + return to_iso_week_date(date.year, date.mon, date.day); + } + + /// \brief Convert timestamp to ISO week date. + /// \tparam T Timestamp type. + /// \param ts Timestamp in seconds. + /// \return ISO week date representation. + template + inline IsoWeekDateStruct to_iso_week_date(T ts) { + const DateTimeStruct date_time = to_date_time(ts); + return to_iso_week_date(date_time.year, date_time.mon, date_time.day); + } + + /// \brief Calculate number of ISO weeks in a year. + /// \param iso_year ISO week-numbering year. + /// \return 52 or 53 depending on the ISO year length. + inline int iso_weeks_in_year(year_t iso_year) { + const IsoWeekDateStruct info = to_iso_week_date(iso_year, 12, 28); + return static_cast(info.week); + } + + /// \brief Validate ISO week date components. + /// \param iso_year ISO week-numbering year. + /// \param week ISO week number. + /// \param weekday ISO weekday (1-7). + /// \return True if components form a valid ISO week date. + inline bool is_valid_iso_week_date(year_t iso_year, int week, int weekday) { + if (iso_year < MIN_YEAR) return false; + if (iso_year > MAX_YEAR) return false; + if (weekday < 1 || weekday > 7) return false; + if (week < 1) return false; + const int max_week = iso_weeks_in_year(iso_year); + return week <= max_week; + } + + /// \brief Convert ISO week date to calendar date. + /// \param iso_date ISO week date structure. + /// \return Calendar date corresponding to the ISO week date. + /// \throws std::invalid_argument if the ISO week date is invalid. + inline DateStruct iso_week_date_to_date(const IsoWeekDateStruct& iso_date) { + if (!is_valid_iso_week_date(iso_date.year, iso_date.week, iso_date.weekday)) { + throw std::invalid_argument("Invalid ISO week date"); + } + + const dse_t jan4_day = date_to_unix_day(iso_date.year, 1, 4); + const int jan4_iso_weekday = iso_weekday_of_date(iso_date.year, 1, 4); + const dse_t first_thursday = jan4_day + static_cast(4 - jan4_iso_weekday); + const dse_t target_thursday = first_thursday + static_cast(iso_date.week - 1) * DAYS_PER_WEEK; + const dse_t target_day = target_thursday + static_cast(iso_date.weekday - 4); + + const DateTimeStruct date_time = to_date_time(unix_day_to_ts(target_day)); + return create_date_struct(date_time.year, date_time.mon, date_time.day); + } + + /// \brief Format ISO week date to string. + /// \param iso_date ISO week date to format. + /// \param extended When true, uses extended format with separators ("YYYY-Www-D"). + /// \param include_weekday When false, omits weekday ("YYYY-Www"). Weekday defaults to Monday when omitted. + /// \return Formatted ISO week-date string. + inline std::string format_iso_week_date(const IsoWeekDateStruct& iso_date, bool extended = true, bool include_weekday = true) { + if (!is_valid_iso_week_date(iso_date.year, iso_date.week, iso_date.weekday)) { + throw std::invalid_argument("Invalid ISO week date"); + } + + if (!include_weekday) { + const char* fmt = extended ? "%" PRId64 "-W%.2d" : "%" PRId64 "W%.2d"; + char buffer[32] = {0}; + std::snprintf(buffer, sizeof(buffer), fmt, iso_date.year, iso_date.week); + return std::string(buffer); + } + + const char* fmt = extended ? "%" PRId64 "-W%.2d-%d" : "%" PRId64 "W%.2d%d"; + char buffer[32] = {0}; + std::snprintf(buffer, sizeof(buffer), fmt, iso_date.year, iso_date.week, iso_date.weekday); + return std::string(buffer); + } + + /// \brief Parse ISO week date string buffer. + /// \param input Pointer to character buffer (may be not null-terminated). + /// \param length Length of the buffer. + /// \param iso_date Output ISO week date structure. + /// \return True if parsing succeeded and produced a valid ISO week date; otherwise false. + inline bool parse_iso_week_date(const char* input, std::size_t length, IsoWeekDateStruct& iso_date) noexcept { + if (input == nullptr) { + return false; + } + + iso_date = create_iso_week_date_struct(0, 0, 0); + + const char* p = input; + const char* const end = input + length; + + bool negative = false; + if (p < end && (*p == '+' || *p == '-')) { + negative = (*p == '-'); + ++p; + } + + const char* start_digits = p; + int64_t value = 0; + while (p < end && std::isdigit(static_cast(*p)) != 0) { + value = value * 10 + static_cast(*p - '0'); + ++p; + } + + if (p == start_digits) return false; + iso_date.year = negative ? -value : value; + + if (p >= end) return false; + + const bool has_dash_after_year = (*p == '-'); + if (has_dash_after_year) { + ++p; + if (p >= end) return false; + } + + if (*p != 'W' && *p != 'w') return false; + ++p; + + int week = 0; + for (int i = 0; i < 2; ++i) { + if (p >= end || std::isdigit(static_cast(*p)) == 0) return false; + week = week * 10 + (*p - '0'); + ++p; + } + + if (week == 0) return false; + + bool has_weekday = false; + if (p < end) { + if ((*p == '-' && has_dash_after_year) || (!has_dash_after_year && std::isdigit(static_cast(*p)) == 0)) { + if (*p == '-') ++p; + if (p >= end) return false; + if (std::isdigit(static_cast(*p)) == 0) return false; + iso_date.weekday = *p - '0'; + ++p; + has_weekday = true; + } else if (std::isdigit(static_cast(*p)) != 0) { + iso_date.weekday = *p - '0'; + ++p; + has_weekday = true; + } + } + + if (!has_weekday) { + iso_date.weekday = 1; + } + + iso_date.week = week; + + if (p != end) return false; + return is_valid_iso_week_date(iso_date.year, iso_date.week, iso_date.weekday); + } + + /// \brief Parse ISO week date string. + /// \param input Input string containing ISO week date. + /// \param iso_date Output ISO week date structure. + /// \return True if parsing succeeded and produced a valid ISO week date; otherwise false. + inline bool parse_iso_week_date(const std::string& input, IsoWeekDateStruct& iso_date) noexcept { + return parse_iso_week_date(input.c_str(), input.size(), iso_date); + } + + /// \brief Alias for parse_iso_week_date. + /// \param input Pointer to character buffer (may be not null-terminated). + /// \param length Length of the buffer. + /// \param iso_date Output ISO week date structure. + /// \return True if parsing succeeded and produced a valid ISO week date; otherwise false. + inline bool try_parse_iso_week_date(const char* input, std::size_t length, IsoWeekDateStruct& iso_date) noexcept { + return parse_iso_week_date(input, length, iso_date); + } + + /// \brief Alias for parse_iso_week_date, std::string overload. + /// \param input Input string containing ISO week date. + /// \param iso_date Output ISO week date structure. + /// \return True if parsing succeeded and produced a valid ISO week date; otherwise false. + inline bool try_parse_iso_week_date(const std::string& input, IsoWeekDateStruct& iso_date) noexcept { + return parse_iso_week_date(input, iso_date); + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_ISO_WEEK_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/iso_week_struct.hpp b/include/time_shield/iso_week_struct.hpp new file mode 100644 index 00000000..58f4754f --- /dev/null +++ b/include/time_shield/iso_week_struct.hpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_ISO_WEEK_STRUCT_HPP_INCLUDED +#define _TIME_SHIELD_ISO_WEEK_STRUCT_HPP_INCLUDED + +/// \file iso_week_struct.hpp +/// \brief Header for ISO week date structure. +/// +/// This file defines the IsoWeekDateStruct structure used to represent ISO 8601 week dates. + +#include + +namespace time_shield { + + /// \ingroup time_structures + /// \brief Structure to represent an ISO week date. + struct IsoWeekDateStruct { + int64_t year; ///< ISO week-numbering year component. + int32_t week; ///< ISO week number component (1-52/53). + int32_t weekday; ///< ISO weekday component (1=Monday .. 7=Sunday). + }; + + /// \ingroup time_structures + /// \brief Creates an IsoWeekDateStruct instance. + /// \param year ISO week-numbering year component. + /// \param week ISO week number component. + /// \param weekday ISO weekday component (1=Monday .. 7=Sunday). + /// \return An IsoWeekDateStruct instance with the provided components. + inline const IsoWeekDateStruct create_iso_week_date_struct( + int64_t year, + int32_t week = 1, + int32_t weekday = 1) { + IsoWeekDateStruct data{year, week, weekday}; + return data; + } + +}; // namespace time_shield + +#endif // _TIME_SHIELD_ISO_WEEK_STRUCT_HPP_INCLUDED diff --git a/include/time_shield/ntp_client.hpp b/include/time_shield/ntp_client.hpp index ccf44698..d0e9b3dd 100644 --- a/include/time_shield/ntp_client.hpp +++ b/include/time_shield/ntp_client.hpp @@ -6,9 +6,6 @@ /// \file ntp_client.hpp /// \brief Simple NTP client for querying time offset from NTP servers. /// -/// This module provides a minimal client that can query a remote NTP server over UDP -/// and calculate the offset between local system time and the NTP-reported time. -/// /// The feature is optional and controlled by `TIME_SHIELD_ENABLE_NTP_CLIENT`. /// \ingroup ntp @@ -16,217 +13,166 @@ #if TIME_SHIELD_ENABLE_NTP_CLIENT -#include "ntp_client/wsa_guard.hpp" #include "time_utils.hpp" +#include "ntp_client/ntp_client_core.hpp" +#include "ntp_client/ntp_packet.hpp" +#include "ntp_client/udp_transport.hpp" + +#if TIME_SHIELD_PLATFORM_WINDOWS +# include "ntp_client/udp_transport_win.hpp" +#elif TIME_SHIELD_PLATFORM_UNIX +# include "ntp_client/udp_transport_posix.hpp" +#endif +#include #include -#include #include -#include -#include -#include -#include namespace time_shield { +#if TIME_SHIELD_PLATFORM_WINDOWS + namespace detail { using PlatformUdpTransport = UdpTransportWin; } +#elif TIME_SHIELD_PLATFORM_UNIX + namespace detail { using PlatformUdpTransport = UdpTransportPosix; } +#endif + +#if TIME_SHIELD_PLATFORM_WINDOWS || TIME_SHIELD_PLATFORM_UNIX + /// \ingroup ntp - /// \brief Simple Windows-only NTP client for measuring time offset. + /// \brief NTP client for measuring time offset. class NtpClient { public: /// \brief Constructs NTP client with specified host and port. + /// \param server NTP server host name. + /// \param port NTP server port. NtpClient(std::string server = "pool.ntp.org", int port = 123) - : m_host(std::move(server)), m_port(port) { + : m_host(std::move(server)) + , m_port(port) + , m_offset_us(0) + , m_delay_us(0) + , m_stratum(-1) + , m_is_success(false) { now_realtime_us(); } /// \brief Queries the NTP server and updates the local offset. - /// \return true if successful. + /// \return True when response parsed successfully. + /// \note Requires network connectivity and a reachable server. bool query() { last_error_code_slot() = 0; - if (!WsaGuard::instance().success()) { - m_is_success = false; - throw std::runtime_error("WSAStartup failed with error: " + std::to_string(WsaGuard::instance().ret_code())); - } - SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sock == INVALID_SOCKET) { +#if TIME_SHIELD_PLATFORM_WINDOWS + if (!WsaGuard::instance().success()) { + last_error_code_slot() = WsaGuard::instance().ret_code(); m_is_success = false; return false; } - - struct sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(static_cast(m_port)); - - addrinfo hints{}, *res = nullptr; - hints.ai_family = AF_INET; // IPv4 - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - - if (getaddrinfo(m_host.c_str(), nullptr, &hints, &res) != 0 || !res) { - last_error_code_slot() = WSAGetLastError(); - closesocket(sock); +#endif + + detail::PlatformUdpTransport transport; + detail::NtpClientCore core; + + int error_code = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + + const bool ok = core.query( + transport, + m_host, + m_port, + k_default_timeout_ms, + error_code, + offset, + delay, + stratum + ); + + last_error_code_slot() = error_code; + + if (!ok) { + m_delay_us = 0; + m_stratum = -1; m_is_success = false; return false; } - sockaddr_in* resolved = reinterpret_cast(res->ai_addr); - addr.sin_addr = resolved->sin_addr; - freeaddrinfo(res); - - ntp_packet pkt; - fill_packet(pkt); - - int timeout_ms = 5000; - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout_ms), sizeof(timeout_ms)); - if (sendto(sock, reinterpret_cast(&pkt), sizeof(pkt), 0, - reinterpret_cast(&addr), sizeof(addr)) < 0) { - last_error_code_slot() = WSAGetLastError(); - closesocket(sock); - m_is_success = false; - return false; - } + m_offset_us = offset; + m_delay_us = delay; + m_stratum = stratum; + m_is_success = true; + return true; + } - sockaddr_in from; - int from_len = sizeof(from); - if (recvfrom(sock, reinterpret_cast(&pkt), sizeof(pkt), 0, - reinterpret_cast(&from), &from_len) < 0) { - last_error_code_slot() = WSAGetLastError(); - closesocket(sock); - m_is_success = false; - return false; - } + /// \brief Returns whether the last NTP query was successful. + /// \return True when the last query updated internal state. + bool success() const noexcept { return m_is_success.load(); } - closesocket(sock); + /// \brief Returns the last measured offset in microseconds. + /// \return Offset in microseconds (UTC - local realtime). + int64_t offset_us() const noexcept { return m_offset_us; } - int64_t result_offset; - if (parse_packet(pkt, result_offset)) { - m_offset_us = result_offset; - m_is_success = true; - return true; - } + /// \brief Returns the last measured delay in microseconds. + /// \return Round-trip delay estimate in microseconds. + int64_t delay_us() const noexcept { return m_delay_us; } - m_is_success = false; - return false; - } - - /// \brief Returns whether the last NTP query was successful. - /// \return True if the last offset measurement succeeded. - bool success() const noexcept { - return m_is_success.load(); - } + /// \brief Returns the last received stratum value. + /// \return NTP stratum value. + int stratum() const noexcept { return m_stratum; } - /// \brief Returns the last measured offset in microseconds. - int64_t get_offset_us() const noexcept { - return m_offset_us; - } - /// \brief Returns current UTC time in microseconds based on last NTP offset. - /// \return Current UTC time in µs. - int64_t get_utc_time_us() const noexcept { - const int64_t offset = m_offset_us.load(); - return now_realtime_us() + offset; - } - + /// \return UTC time in microseconds using last offset. + int64_t utc_time_us() const noexcept { return now_realtime_us() + m_offset_us.load(); } + /// \brief Returns current UTC time in milliseconds based on last NTP offset. - /// \return Current UTC time in ms. - int64_t get_utc_time_ms() const noexcept { - return get_utc_time_us() / 1000; - } + /// \return UTC time in milliseconds using last offset. + int64_t utc_time_ms() const noexcept { return utc_time_us() / 1000; } /// \brief Returns current UTC time as time_t (seconds since Unix epoch). - /// \return UTC time in seconds. - time_t get_utc_time() const noexcept { - return static_cast(get_utc_time_us() / 1000000); - } - - /// \brief Returns last WinSock error code (if any). - int get_last_error_code() const noexcept { - return last_error_code_slot(); - } - + /// \return UTC time in seconds since Unix epoch. + time_t utc_time_sec() const noexcept { return static_cast(utc_time_us() / 1000000); } + + /// \brief Returns last socket error code (if any). + /// \return Error code from last query attempt. + int last_error_code() const noexcept { return last_error_code_slot(); } + private: - static constexpr int64_t NTP_TIMESTAMP_DELTA = 2208988800ll; ///< Seconds between 1900 and 1970 epochs. - - /// \brief Структура пакета NTP - /// Total: 384 bits or 48 bytes. - struct ntp_packet { - uint8_t li_vn_mode; // Eight bits. li, vn, and mode. - // li. Two bits. Leap indicator. - // vn. Three bits. Version number of the protocol. - // mode. Three bits. Client will pick mode 3 for client. - uint8_t stratum; // Eight bits. Stratum level of the local clock. - uint8_t poll; // Eight bits. Maximum interval between successive messages. - uint8_t precision; // Eight bits. Precision of the local clock. - uint32_t root_delay; // 32 bits. Total round trip delay time. - uint32_t root_dispersion; // 32 bits. Max error aloud from primary clock source. - uint32_t ref_id; // 32 bits. Reference clock identifier. - uint32_t ref_ts_sec; // 32 bits. Reference time-stamp seconds. - uint32_t ref_ts_frac; // 32 bits. Reference time-stamp fraction of a second. - uint32_t orig_ts_sec; // 32 bits. Originate time-stamp seconds. - uint32_t orig_ts_frac; // 32 bits. Originate time-stamp fraction of a second. - uint32_t recv_ts_sec; // 32 bits. Received time-stamp seconds. - uint32_t recv_ts_frac; // 32 bits. Received time-stamp fraction of a second. - uint32_t tx_ts_sec; // 32 bits and the most important field the client cares about. Transmit time-stamp seconds. - uint32_t tx_ts_frac; // 32 bits. Transmit time-stamp fraction of a second. - }; - - std::string m_host; - int m_port = 123; - std::atomic m_offset_us{0}; - std::atomic m_is_success{false}; + std::string m_host; + int m_port; + std::atomic m_offset_us; + std::atomic m_delay_us; + std::atomic m_stratum; + std::atomic m_is_success; + static const int k_default_timeout_ms = 5000; + static int& last_error_code_slot() noexcept { - static thread_local int value = 0; + static TIME_SHIELD_THREAD_LOCAL int value = 0; return value; } + }; - /// \brief Converts local time to NTP timestamp format. - void fill_packet(ntp_packet& pkt) const { - std::memset(&pkt, 0, sizeof(pkt)); - pkt.li_vn_mode = (0 << 6) | (3 << 3) | 3; // LI=0, VN=3, Mode=3 (client) - - const uint64_t now_us = time_shield::now_realtime_us(); - const uint64_t sec = now_us / 1000000 + NTP_TIMESTAMP_DELTA; - const uint64_t frac = ((now_us % 1000000) * 0x100000000ULL) / 1000000; +#else - pkt.tx_ts_sec = htonl(static_cast(sec)); - pkt.tx_ts_frac = htonl(static_cast(frac)); + class NtpClient { + public: + NtpClient() { + static_assert(sizeof(void*) == 0, "NtpClient is disabled by configuration."); } + }; - /// \brief Parses response and calculates offset. - bool parse_packet(const ntp_packet& pkt, int64_t& result_offset_us) const { - const uint64_t arrival_us = time_shield::now_realtime_us(); - - const uint64_t originate_us = ((static_cast(ntohl(pkt.orig_ts_sec)) - NTP_TIMESTAMP_DELTA) * 1000000) + - (static_cast(ntohl(pkt.orig_ts_frac)) * 1000000 / 0xFFFFFFFFull); - const uint64_t receive_us = ((static_cast(ntohl(pkt.recv_ts_sec)) - NTP_TIMESTAMP_DELTA) * 1000000) + - (static_cast(ntohl(pkt.recv_ts_frac)) * 1000000 / 0xFFFFFFFFull); - const uint64_t transmit_us = ((static_cast(ntohl(pkt.tx_ts_sec)) - NTP_TIMESTAMP_DELTA) * 1000000) + - (static_cast(ntohl(pkt.tx_ts_frac)) * 1000000 / 0xFFFFFFFFull); +#endif // platform switch - // RFC 5905 - result_offset_us = ((static_cast(receive_us) - static_cast(originate_us)) - + (static_cast(transmit_us) - static_cast(arrival_us))) / 2; - return true; - } - }; - } // namespace time_shield -#else // !TIME_SHIELD_ENABLE_NTP_CLIENT - -# warning "NtpClient is disabled or unsupported on this platform." +#else // TIME_SHIELD_ENABLE_NTP_CLIENT namespace time_shield { - - /// \brief Placeholder used when NTP client is disabled. class NtpClient { public: NtpClient() { - static_assert(sizeof(void*) == 0, "time_shield::NtpClient is disabled by configuration."); + static_assert(sizeof(void*) == 0, "NtpClient is disabled by configuration."); } }; - } // namespace time_shield #endif // TIME_SHIELD_ENABLE_NTP_CLIENT diff --git a/include/time_shield/ntp_client/ntp_client_core.hpp b/include/time_shield/ntp_client/ntp_client_core.hpp new file mode 100644 index 00000000..3cfc3939 --- /dev/null +++ b/include/time_shield/ntp_client/ntp_client_core.hpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_NTP_CLIENT_CORE_HPP_INCLUDED +#define _TIME_SHIELD_NTP_CLIENT_CORE_HPP_INCLUDED + +#include "ntp_packet.hpp" +#include "udp_transport.hpp" + +#include +#include + +namespace time_shield { +namespace detail { + + /// \brief Core NTP query logic that parses packets and computes offsets. + class NtpClientCore { + public: + /// \brief Perform one NTP transaction using a UDP transport. + bool query(IUdpTransport& transport, + const std::string& host, + int port, + int timeout_ms, + int& out_error_code, + int64_t& out_offset_us, + int64_t& out_delay_us, + int& out_stratum) noexcept { + out_error_code = 0; + out_offset_us = 0; + out_delay_us = 0; + out_stratum = -1; + + uint64_t now_us = 0; + if (!get_now_us(now_us)) { + out_error_code = -1; + return false; + } + + NtpPacket pkt{}; + fill_client_packet(pkt, now_us); + + NtpPacket reply{}; + UdpRequest req; + req.host = host; + req.port = port; + req.send_data = &pkt; + req.send_size = sizeof(pkt); + req.recv_data = &reply; + req.recv_size = sizeof(reply); + req.timeout_ms = timeout_ms; + + if (!transport.transact(req, out_error_code)) { + if (out_error_code == 0) { + out_error_code = -1; + } + return false; + } + + uint64_t arrival_us = 0; + if (!get_now_us(arrival_us)) { + out_error_code = -1; + return false; + } + + if (!parse_server_packet(reply, arrival_us, out_offset_us, out_delay_us, out_stratum, out_error_code)) { + if (out_error_code == 0) { + out_error_code = -1; + } + return false; + } + + return true; + } + + private: + /// \brief Read current realtime clock in microseconds. + static bool get_now_us(uint64_t& out) noexcept { + const int64_t v = time_shield::now_realtime_us(); + if (v < 0) return false; + out = static_cast(v); + return true; + } + }; + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_NTP_CLIENT_CORE_HPP_INCLUDED diff --git a/include/time_shield/ntp_client/ntp_packet.hpp b/include/time_shield/ntp_client/ntp_packet.hpp new file mode 100644 index 00000000..7cb3981d --- /dev/null +++ b/include/time_shield/ntp_client/ntp_packet.hpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_NTP_PACKET_HPP_INCLUDED +#define _TIME_SHIELD_NTP_PACKET_HPP_INCLUDED + +#include +#include + +#if TIME_SHIELD_PLATFORM_WINDOWS +# include +#else +# include +#endif + +namespace time_shield { +namespace detail { + + /// \ingroup ntp + /// \brief NTP packet layout (48 bytes). + struct NtpPacket { + uint8_t li_vn_mode; + uint8_t stratum; + uint8_t poll; + uint8_t precision; + uint32_t root_delay; + uint32_t root_dispersion; + uint32_t ref_id; + uint32_t ref_ts_sec; + uint32_t ref_ts_frac; + uint32_t orig_ts_sec; + uint32_t orig_ts_frac; + uint32_t recv_ts_sec; + uint32_t recv_ts_frac; + uint32_t tx_ts_sec; + uint32_t tx_ts_frac; + }; + + static_assert(sizeof(NtpPacket) == 48, "NtpPacket must be 48 bytes"); + + /// \ingroup ntp + /// \brief Protocol-level error codes for NTP parsing. + enum NtpProtoError { + NTP_EPROTO_BASE = -10000, + NTP_E_BAD_MODE = NTP_EPROTO_BASE - 1, + NTP_E_BAD_VERSION = NTP_EPROTO_BASE - 2, + NTP_E_BAD_LI = NTP_EPROTO_BASE - 3, + NTP_E_BAD_STRATUM = NTP_EPROTO_BASE - 4, + NTP_E_KOD = NTP_EPROTO_BASE - 5, + NTP_E_BAD_TS = NTP_EPROTO_BASE - 6 + }; + + /// \brief Extract leap indicator from LI/VN/Mode field. + static inline uint8_t ntp_li(uint8_t li_vn_mode) noexcept { + return static_cast((li_vn_mode >> 6) & 0x03); + } + + /// \brief Extract version number from LI/VN/Mode field. + static inline uint8_t ntp_vn(uint8_t li_vn_mode) noexcept { + return static_cast((li_vn_mode >> 3) & 0x07); + } + + /// \brief Extract mode from LI/VN/Mode field. + static inline uint8_t ntp_mode(uint8_t li_vn_mode) noexcept { + return static_cast(li_vn_mode & 0x07); + } + + /// \brief Convert NTP fractional seconds to microseconds. + static inline uint64_t ntp_frac_to_us(uint32_t frac_net) noexcept { + const uint64_t frac = static_cast(ntohl(frac_net)); + return (frac * 1000000ULL) >> 32; + } + + /// \brief Convert NTP timestamp parts to Unix microseconds. + static inline bool ntp_ts_to_unix_us(uint32_t sec_net, uint32_t frac_net, uint64_t& out_us) noexcept { + static const int64_t NTP_TIMESTAMP_DELTA = 2208988800ll; + const int64_t sec = static_cast(ntohl(sec_net)) - NTP_TIMESTAMP_DELTA; + if (sec < 0) return false; + out_us = static_cast(sec) * 1000000ULL + ntp_frac_to_us(frac_net); + return true; + } + + /// \brief Fill an NTP client request packet using local time. + static inline void fill_client_packet(NtpPacket& pkt, uint64_t now_us) { + std::memset(&pkt, 0, sizeof(pkt)); + pkt.li_vn_mode = static_cast((0 << 6) | (3 << 3) | 3); // LI=0, VN=3, Mode=3 + + const uint64_t sec = now_us / 1000000 + 2208988800ULL; + const uint64_t frac = ((now_us % 1000000) * 0x100000000ULL) / 1000000; + + pkt.tx_ts_sec = htonl(static_cast(sec)); + pkt.tx_ts_frac = htonl(static_cast(frac)); + } + + /// \brief Parse server response and compute offset and delay. + static inline bool parse_server_packet(const NtpPacket& pkt, + uint64_t arrival_us, + int64_t& offset_us, + int64_t& delay_us, + int& stratum, + int& out_error_code) noexcept { + const uint8_t li = ntp_li(pkt.li_vn_mode); + const uint8_t vn = ntp_vn(pkt.li_vn_mode); + const uint8_t mode = ntp_mode(pkt.li_vn_mode); + + if (mode != 4) { + out_error_code = NTP_E_BAD_MODE; + return false; + } + if (vn < 3 || vn > 4) { + out_error_code = NTP_E_BAD_VERSION; + return false; + } + if (li == 3) { + out_error_code = NTP_E_BAD_LI; + return false; + } + if (pkt.stratum == 0) { + out_error_code = NTP_E_KOD; + return false; + } + if (pkt.stratum >= 16) { + out_error_code = NTP_E_BAD_STRATUM; + return false; + } + + uint64_t originate_us = 0; + uint64_t receive_us = 0; + uint64_t transmit_us = 0; + + if (!ntp_ts_to_unix_us(pkt.orig_ts_sec, pkt.orig_ts_frac, originate_us)) { + out_error_code = NTP_E_BAD_TS; + return false; + } + if (!ntp_ts_to_unix_us(pkt.recv_ts_sec, pkt.recv_ts_frac, receive_us)) { + out_error_code = NTP_E_BAD_TS; + return false; + } + if (!ntp_ts_to_unix_us(pkt.tx_ts_sec, pkt.tx_ts_frac, transmit_us)) { + out_error_code = NTP_E_BAD_TS; + return false; + } + + const int64_t t1 = static_cast(originate_us); + const int64_t t2 = static_cast(receive_us); + const int64_t t3 = static_cast(transmit_us); + const int64_t t4 = static_cast(arrival_us); + + if (t3 < t2) { + out_error_code = NTP_E_BAD_TS; + return false; + } + + offset_us = ((t2 - t1) + (t3 - t4)) / 2; + delay_us = (t4 - t1) - (t3 - t2); + if (delay_us < 0) { + out_error_code = NTP_E_BAD_TS; + return false; + } + stratum = pkt.stratum; + return true; + } + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_NTP_PACKET_HPP_INCLUDED diff --git a/include/time_shield/ntp_client/udp_transport.hpp b/include/time_shield/ntp_client/udp_transport.hpp new file mode 100644 index 00000000..9661f1bd --- /dev/null +++ b/include/time_shield/ntp_client/udp_transport.hpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_UDP_TRANSPORT_HPP_INCLUDED +#define _TIME_SHIELD_UDP_TRANSPORT_HPP_INCLUDED + +#include +#include + +namespace time_shield { +namespace detail { + + /// \brief UDP request parameters for NTP transactions. + struct UdpRequest { + std::string host; ///< Target host name or IP address. + int port = 123; ///< Target port. + const void* send_data = nullptr; ///< Pointer to outgoing payload. + std::size_t send_size = 0; ///< Outgoing payload size in bytes. + void* recv_data = nullptr; ///< Pointer to receive buffer. + std::size_t recv_size = 0; ///< Receive buffer size in bytes. + int timeout_ms = 5000; ///< Receive timeout in milliseconds. + }; + + /// \brief Abstract UDP transport interface for NTP queries. + class IUdpTransport { + public: + /// \brief Virtual destructor. + virtual ~IUdpTransport() {} + /// \brief Send request and receive response over UDP. + virtual bool transact(const UdpRequest& req, int& out_error_code) noexcept = 0; + }; + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_UDP_TRANSPORT_HPP_INCLUDED diff --git a/include/time_shield/ntp_client/udp_transport_posix.hpp b/include/time_shield/ntp_client/udp_transport_posix.hpp new file mode 100644 index 00000000..0dbff1de --- /dev/null +++ b/include/time_shield/ntp_client/udp_transport_posix.hpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_UDP_TRANSPORT_POSIX_HPP_INCLUDED +#define _TIME_SHIELD_UDP_TRANSPORT_POSIX_HPP_INCLUDED + +#if TIME_SHIELD_PLATFORM_UNIX + +#include "udp_transport.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace time_shield { +namespace detail { + + /// \brief POSIX UDP transport for NTP queries. + class UdpTransportPosix : public IUdpTransport { + public: + /// \brief Send request and receive response over UDP. + bool transact(const UdpRequest& req, int& out_error_code) noexcept override { + out_error_code = 0; + const int sock = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + out_error_code = errno; + return false; + } + + addrinfo hints{}, *res = nullptr; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + const int resolve_code = getaddrinfo(req.host.c_str(), nullptr, &hints, &res); + if (resolve_code != 0 || !res) { + out_error_code = (resolve_code != 0) ? resolve_code : errno; + ::close(sock); + return false; + } + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(req.port)); + addr.sin_addr = reinterpret_cast(res->ai_addr)->sin_addr; + freeaddrinfo(res); + res = nullptr; + + const int timeout_ms = req.timeout_ms > 0 ? req.timeout_ms : 5000; + timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + ::setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + const ssize_t sent = ::sendto(sock, + req.send_data, + req.send_size, + 0, + reinterpret_cast(&addr), + sizeof(addr)); + if (sent < 0 || static_cast(sent) != req.send_size) { + out_error_code = errno; + ::close(sock); + return false; + } + + sockaddr_in from{}; + socklen_t from_len = sizeof(from); + const ssize_t received = ::recvfrom(sock, + req.recv_data, + req.recv_size, + 0, + reinterpret_cast(&from), + &from_len); + + if (received < 0 || static_cast(received) != req.recv_size) { + out_error_code = errno; + ::close(sock); + return false; + } + + ::close(sock); + return true; + } + }; + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_PLATFORM_UNIX + +#endif // _TIME_SHIELD_UDP_TRANSPORT_POSIX_HPP_INCLUDED diff --git a/include/time_shield/ntp_client/udp_transport_win.hpp b/include/time_shield/ntp_client/udp_transport_win.hpp new file mode 100644 index 00000000..f597ef6b --- /dev/null +++ b/include/time_shield/ntp_client/udp_transport_win.hpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_UDP_TRANSPORT_WIN_HPP_INCLUDED +#define _TIME_SHIELD_UDP_TRANSPORT_WIN_HPP_INCLUDED + +#if TIME_SHIELD_PLATFORM_WINDOWS + +#include "wsa_guard.hpp" +#include "udp_transport.hpp" + +#include +#include +#include + +namespace time_shield { +namespace detail { + + /// \brief Windows UDP transport for NTP queries. + class UdpTransportWin : public IUdpTransport { + public: + /// \brief Send request and receive response over UDP. + bool transact(const UdpRequest& req, int& out_error_code) noexcept override { + out_error_code = 0; + if (!WsaGuard::instance().success()) { + out_error_code = WsaGuard::instance().ret_code(); + return false; + } + + SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock == INVALID_SOCKET) { + out_error_code = WSAGetLastError(); + return false; + } + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(req.port)); + + addrinfo hints{}, *res = nullptr; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + if (getaddrinfo(req.host.c_str(), nullptr, &hints, &res) != 0 || !res) { + out_error_code = WSAGetLastError(); + closesocket(sock); + return false; + } + addr.sin_addr = reinterpret_cast(res->ai_addr)->sin_addr; + + const int timeout_ms = req.timeout_ms > 0 ? req.timeout_ms : 5000; + DWORD timeout = static_cast(timeout_ms); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); + + const int send_res = sendto(sock, + static_cast(req.send_data), + static_cast(req.send_size), + 0, + reinterpret_cast(&addr), + sizeof(addr)); + if (send_res == SOCKET_ERROR || static_cast(send_res) != req.send_size) { + out_error_code = WSAGetLastError(); + freeaddrinfo(res); + closesocket(sock); + return false; + } + + sockaddr_in from{}; + int from_len = sizeof(from); + const int recv_res = recvfrom(sock, + static_cast(req.recv_data), + static_cast(req.recv_size), + 0, + reinterpret_cast(&from), + &from_len); + + if (recv_res == SOCKET_ERROR || static_cast(recv_res) != req.recv_size) { + out_error_code = WSAGetLastError(); + freeaddrinfo(res); + closesocket(sock); + return false; + } + + freeaddrinfo(res); + closesocket(sock); + return true; + } + }; + +} // namespace detail +} // namespace time_shield + +#endif // _TIME_SHIELD_PLATFORM_WINDOWS + +#endif // _TIME_SHIELD_UDP_TRANSPORT_WIN_HPP_INCLUDED diff --git a/include/time_shield/ntp_client/wsa_guard.hpp b/include/time_shield/ntp_client/wsa_guard.hpp index ddac4a34..f2c3a32c 100644 --- a/include/time_shield/ntp_client/wsa_guard.hpp +++ b/include/time_shield/ntp_client/wsa_guard.hpp @@ -6,8 +6,6 @@ /// \brief Singleton guard for WinSock initialization. /// \ingroup ntp -#include "../config.hpp" - #if TIME_SHIELD_HAS_WINSOCK # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN diff --git a/include/time_shield/ntp_client_pool.hpp b/include/time_shield/ntp_client_pool.hpp new file mode 100644 index 00000000..18ed49dc --- /dev/null +++ b/include/time_shield/ntp_client_pool.hpp @@ -0,0 +1,733 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_NTP_CLIENT_POOL_HPP_INCLUDED +#define _TIME_SHIELD_NTP_CLIENT_POOL_HPP_INCLUDED + +#include "config.hpp" + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include "ntp_client.hpp" +#include "time_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace time_shield { + + /// \ingroup ntp + /// \brief NTP measurement sample (one server response). + struct NtpSample { + std::string host; ///< Server host name. + int port = 123; ///< Server port. + bool is_ok = false; ///< Indicates successful response parsing. + int error_code = 0; ///< Error code when query or parsing failed. + int stratum = -1; ///< NTP stratum level reported by server. + int64_t offset_us = 0; ///< Offset between UTC and local realtime, microseconds. + int64_t delay_us = 0; ///< Estimated round-trip delay, microseconds. + int64_t max_delay_us = 0; ///< Maximum acceptable delay for this sample. + }; + + /// \ingroup ntp + /// \brief Per-server configuration. + struct NtpServerConfig { + std::string host; ///< Server host name. + int port = 123; ///< Server port. + + std::chrono::milliseconds min_interval{15000}; ///< Minimum time between queries to the same server. + std::chrono::milliseconds max_delay{250}; ///< Maximum acceptable delay for responses from this server. + + std::chrono::milliseconds backoff_initial{15000}; ///< Initial backoff after failure. + std::chrono::milliseconds backoff_max{std::chrono::minutes(10)}; ///< Maximum backoff interval after repeated failures. + }; + + /// \ingroup ntp + /// \brief Pool configuration. + struct NtpPoolConfig { + std::size_t sample_servers = 5; ///< Number of servers to sample per measurement. + std::size_t min_valid_samples = 3; ///< Minimum number of valid samples required to update offset. + + /// \brief Aggregation strategy for offset estimation. + enum class Aggregation { + Median, + BestDelay, + MedianMadTrim + } aggregation = Aggregation::Median; + + double smoothing_alpha = 1.0; ///< Exponential smoothing factor for offset updates. + std::uint64_t rng_seed = 0; ///< Random seed for server sampling; 0 uses time-based seed. + }; + + /// \ingroup ntp + /// \brief Pool of NTP servers: rate-limited multi-server offset estimation. + /// \tparam ClientT NTP client type with interface: + /// ClientT(const std::string& host, int port); + /// bool query(); // may throw + /// int last_error_code() const; + /// int64_t offset_us() const; + /// int64_t delay_us() const; + /// int stratum() const; + template + class NtpClientPoolT { + public: + /// \brief Construct pool with configuration. + /// \param cfg Pool configuration. + explicit NtpClientPoolT(NtpPoolConfig cfg = {}) + : m_cfg(std::move(cfg)) + , m_offset_us(0) + , m_rng(init_seed(m_cfg.rng_seed)) {} + + NtpClientPoolT(const NtpClientPoolT&) = delete; + NtpClientPoolT& operator=(const NtpClientPoolT&) = delete; + + /// \brief Move-construct pool state. + NtpClientPoolT(NtpClientPoolT&& other) noexcept + : m_cfg() + , m_offset_us(0) + , m_rng(init_seed(other.m_cfg.rng_seed)) { + std::lock_guard lk(other.m_mtx); + m_cfg = other.m_cfg; + m_servers = std::move(other.m_servers); + m_last_samples = std::move(other.m_last_samples); + m_offset_us.store(other.m_offset_us.load()); + m_rng = std::move(other.m_rng); + } + + /// \brief Move-assign pool state. + NtpClientPoolT& operator=(NtpClientPoolT&& other) noexcept { + if (this == &other) { + return *this; + } + std::lock(m_mtx, other.m_mtx); + std::lock_guard lk1(m_mtx, std::adopt_lock); + std::lock_guard lk2(other.m_mtx, std::adopt_lock); + + m_cfg = other.m_cfg; + m_servers = std::move(other.m_servers); + m_last_samples = std::move(other.m_last_samples); + m_offset_us.store(other.m_offset_us.load()); + m_rng = std::move(other.m_rng); + return *this; + } + + /// \brief Replace server list (keeps pool config). + /// \param servers Server configurations to use. + void set_servers(std::vector servers) { + std::lock_guard lk(m_mtx); + m_servers.clear(); + m_servers.reserve(servers.size()); + for (auto& server_cfg : servers) { + ServerState state; + state.cfg = std::move(server_cfg); + m_servers.push_back(std::move(state)); + } + } + + /// \brief Add one server. + /// \param server_cfg Server configuration to add. + void add_server(NtpServerConfig server_cfg) { + std::lock_guard lk(m_mtx); + ServerState state; + state.cfg = std::move(server_cfg); + m_servers.push_back(std::move(state)); + } + + /// \brief Build a conservative default server list. + /// \return Default server list with conservative timing settings. + static std::vector build_default_servers() { + std::vector servers; + servers.reserve(160); + + auto add = [&servers](const char* host) { + NtpServerConfig cfg; + cfg.host = host; + cfg.min_interval = std::chrono::milliseconds{60000}; + cfg.max_delay = std::chrono::milliseconds{500}; + cfg.backoff_initial = std::chrono::milliseconds{120000}; + cfg.backoff_max = std::chrono::minutes(10); + servers.push_back(std::move(cfg)); + }; + + add("time.google.com"); + add("time1.google.com"); + add("time2.google.com"); + add("time3.google.com"); + add("time4.google.com"); + + add("time.cloudflare.com"); + + add("time.facebook.com"); + add("time1.facebook.com"); + add("time2.facebook.com"); + add("time3.facebook.com"); + add("time4.facebook.com"); + add("time5.facebook.com"); + + add("time.windows.com"); + + add("time.apple.com"); + add("time1.apple.com"); + add("time2.apple.com"); + add("time3.apple.com"); + add("time4.apple.com"); + add("time5.apple.com"); + add("time6.apple.com"); + add("time7.apple.com"); + add("time.euro.apple.com"); + + add("time-a-g.nist.gov"); + add("time-b-g.nist.gov"); + add("time-c-g.nist.gov"); + add("time-d-g.nist.gov"); + add("time-a-wwv.nist.gov"); + add("time-b-wwv.nist.gov"); + add("time-c-wwv.nist.gov"); + add("time-d-wwv.nist.gov"); + add("time-a-b.nist.gov"); + add("time-b-b.nist.gov"); + add("time-c-b.nist.gov"); + add("time-d-b.nist.gov"); + add("time.nist.gov"); + add("utcnist.colorado.edu"); + add("utcnist2.colorado.edu"); + + add("ntp1.vniiftri.ru"); + add("ntp2.vniiftri.ru"); + add("ntp3.vniiftri.ru"); + add("ntp4.vniiftri.ru"); + add("ntp1.niiftri.irkutsk.ru"); + add("ntp2.niiftri.irkutsk.ru"); + add("vniiftri.khv.ru"); + add("vniiftri2.khv.ru"); + add("ntp21.vniiftri.ru"); + + add("ntp.mobatime.ru"); + + add("ntp1.stratum1.ru"); + add("ntp2.stratum1.ru"); + add("ntp3.stratum1.ru"); + add("ntp4.stratum1.ru"); + add("ntp5.stratum1.ru"); + add("ntp2.stratum2.ru"); + add("ntp3.stratum2.ru"); + add("ntp4.stratum2.ru"); + add("ntp5.stratum2.ru"); + + add("stratum1.net"); + + add("ntp.time.in.ua"); + add("ntp2.time.in.ua"); + add("ntp3.time.in.ua"); + + add("ntp.ru"); + + add("ts1.aco.net"); + add("ts2.aco.net"); + + add("ntp1.net.berkeley.edu"); + add("ntp2.net.berkeley.edu"); + + add("ntp.gsu.edu"); + + add("tick.usask.ca"); + add("tock.usask.ca"); + + add("ntp.nsu.ru"); + add("ntp.rsu.edu.ru"); + + add("ntp.nict.jp"); + + add("x.ns.gin.ntt.net"); + add("y.ns.gin.ntt.net"); + + add("clock.nyc.he.net"); + add("clock.sjc.he.net"); + + add("ntp.fiord.ru"); + + add("gbg1.ntp.se"); + add("gbg2.ntp.se"); + add("mmo1.ntp.se"); + add("mmo2.ntp.se"); + add("sth1.ntp.se"); + add("sth2.ntp.se"); + add("svl1.ntp.se"); + add("svl2.ntp.se"); + + add("clock.isc.org"); + + add("pool.ntp.org"); + add("0.pool.ntp.org"); + add("1.pool.ntp.org"); + add("2.pool.ntp.org"); + add("3.pool.ntp.org"); + + add("europe.pool.ntp.org"); + add("0.europe.pool.ntp.org"); + add("1.europe.pool.ntp.org"); + add("2.europe.pool.ntp.org"); + add("3.europe.pool.ntp.org"); + + add("asia.pool.ntp.org"); + add("0.asia.pool.ntp.org"); + add("1.asia.pool.ntp.org"); + add("2.asia.pool.ntp.org"); + add("3.asia.pool.ntp.org"); + + add("ru.pool.ntp.org"); + add("0.ru.pool.ntp.org"); + add("1.ru.pool.ntp.org"); + add("2.ru.pool.ntp.org"); + add("3.ru.pool.ntp.org"); + + add("0.gentoo.pool.ntp.org"); + add("1.gentoo.pool.ntp.org"); + add("2.gentoo.pool.ntp.org"); + add("3.gentoo.pool.ntp.org"); + + add("0.arch.pool.ntp.org"); + add("1.arch.pool.ntp.org"); + add("2.arch.pool.ntp.org"); + add("3.arch.pool.ntp.org"); + + add("0.fedora.pool.ntp.org"); + add("1.fedora.pool.ntp.org"); + add("2.fedora.pool.ntp.org"); + add("3.fedora.pool.ntp.org"); + + add("0.opensuse.pool.ntp.org"); + add("1.opensuse.pool.ntp.org"); + add("2.opensuse.pool.ntp.org"); + add("3.opensuse.pool.ntp.org"); + + add("0.centos.pool.ntp.org"); + add("1.centos.pool.ntp.org"); + add("2.centos.pool.ntp.org"); + add("3.centos.pool.ntp.org"); + + add("0.debian.pool.ntp.org"); + add("1.debian.pool.ntp.org"); + add("2.debian.pool.ntp.org"); + add("3.debian.pool.ntp.org"); + + add("0.ubuntu.pool.ntp.org"); + add("1.ubuntu.pool.ntp.org"); + add("2.ubuntu.pool.ntp.org"); + add("3.ubuntu.pool.ntp.org"); + + add("0.askozia.pool.ntp.org"); + add("1.askozia.pool.ntp.org"); + add("2.askozia.pool.ntp.org"); + add("3.askozia.pool.ntp.org"); + + add("0.freebsd.pool.ntp.org"); + add("1.freebsd.pool.ntp.org"); + add("2.freebsd.pool.ntp.org"); + add("3.freebsd.pool.ntp.org"); + + add("0.netbsd.pool.ntp.org"); + add("1.netbsd.pool.ntp.org"); + add("2.netbsd.pool.ntp.org"); + add("3.netbsd.pool.ntp.org"); + + add("0.openbsd.pool.ntp.org"); + add("1.openbsd.pool.ntp.org"); + add("2.openbsd.pool.ntp.org"); + add("3.openbsd.pool.ntp.org"); + + add("0.dragonfly.pool.ntp.org"); + add("1.dragonfly.pool.ntp.org"); + add("2.dragonfly.pool.ntp.org"); + add("3.dragonfly.pool.ntp.org"); + + add("0.pfsense.pool.ntp.org"); + add("1.pfsense.pool.ntp.org"); + add("2.pfsense.pool.ntp.org"); + add("3.pfsense.pool.ntp.org"); + + add("0.opnsense.pool.ntp.org"); + add("1.opnsense.pool.ntp.org"); + add("2.opnsense.pool.ntp.org"); + add("3.opnsense.pool.ntp.org"); + + add("0.smartos.pool.ntp.org"); + add("1.smartos.pool.ntp.org"); + add("2.smartos.pool.ntp.org"); + add("3.smartos.pool.ntp.org"); + + add("0.android.pool.ntp.org"); + add("1.android.pool.ntp.org"); + add("2.android.pool.ntp.org"); + add("3.android.pool.ntp.org"); + + add("0.amazon.pool.ntp.org"); + add("1.amazon.pool.ntp.org"); + add("2.amazon.pool.ntp.org"); + add("3.amazon.pool.ntp.org"); + + return servers; + } + + /// \brief Replace server list with a conservative default set. + void set_default_servers() { + set_servers(build_default_servers()); + } + + /// \brief Clear server list. + void clear_servers() { + std::lock_guard lk(m_mtx); + m_servers.clear(); + } + + /// \brief Perform measurement using current config (queries up to sample_servers). + /// \return True when pool offset updated. + bool measure() { + const auto cfg = config(); + return measure_n(cfg.sample_servers); + } + + /// \brief Perform measurement using a custom number of servers. + /// \param servers_to_sample Number of servers to query in this measurement. + /// \return True when pool offset updated. + bool measure_n(std::size_t servers_to_sample) { + std::vector picked; + NtpPoolConfig cfg; + { + std::lock_guard lk(m_mtx); + cfg = m_cfg; + picked = pick_servers_locked(servers_to_sample); + } + + std::vector samples; + samples.reserve(picked.size()); + + for (std::size_t idx : picked) { + samples.push_back(query_one(idx)); + } + + const bool is_updated = update_from_samples(samples, cfg); + + { + std::lock_guard lk(m_mtx); + m_last_samples = std::move(samples); + } + + return is_updated; + } + + /// \brief Last estimated pool offset (µs). + /// \return Offset in microseconds (UTC - local realtime). + int64_t offset_us() const noexcept { return m_offset_us.load(); } + + /// \brief Current UTC time in microseconds based on pool offset. + /// \return UTC time in microseconds using pool offset. + int64_t utc_time_us() const noexcept { return now_realtime_us() + m_offset_us.load(); } + + /// \brief Current UTC time in milliseconds based on pool offset. + /// \return UTC time in milliseconds using pool offset. + int64_t utc_time_ms() const noexcept { return utc_time_us() / 1000; } + + /// \brief Current UTC time in seconds based on pool offset. + /// \return UTC time in seconds using pool offset. + int64_t utc_time_sec() const noexcept { return utc_time_us() / 1000000; } + + /// \brief Returns last measurement samples (copy). + /// \return Copy of samples from the last measurement. + std::vector last_samples() const { + std::lock_guard lk(m_mtx); + return m_last_samples; + } + + /// \brief Apply pre-collected samples (testing/offline). + /// \param samples Sample list to apply. + /// \return True when pool offset updated. + /// \note Primarily for tests; does not enforce rate limiting or backoff. + bool apply_samples(const std::vector& samples) { + const NtpPoolConfig cfg = config(); + const bool is_updated = update_from_samples(samples, cfg); + std::lock_guard lk(m_mtx); + m_last_samples = samples; + return is_updated; + } + + /// \brief Returns median of values. + /// \param values Values to process in-place. + /// \return Median of the input values. + static int64_t median(std::vector& values) { + using diff_t = std::vector::difference_type; + const diff_t mid_index = static_cast(values.size() / 2); + std::nth_element(values.begin(), values.begin() + mid_index, values.end()); + const int64_t mid = values[static_cast(mid_index)]; + if (values.size() % 2 == 1) { + return mid; + } + + const auto it = std::max_element(values.begin(), values.begin() + mid_index); + return (*it + mid) / 2; + } + + /// \brief Median with MAD trimming. + /// \param offsets Offset list to process in-place. + /// \return Median after MAD-based trimming. + static int64_t median_mad_trim(std::vector& offsets) { + const int64_t med = median(offsets); + + std::vector deviations; + deviations.reserve(offsets.size()); + for (auto value : offsets) { + deviations.push_back(value > med ? (value - med) : (med - value)); + } + + const int64_t mad = median(deviations); + if (mad == 0) { + return med; + } + + const int64_t threshold = mad * 3; + std::vector kept; + kept.reserve(offsets.size()); + for (auto value : offsets) { + const int64_t deviation = value > med ? (value - med) : (med - value); + if (deviation <= threshold) { + kept.push_back(value); + } + } + if (kept.empty()) { + return med; + } + return median(kept); + } + + /// \brief Offset from best (lowest) delay sample. + /// \param samples Sample list to scan. + /// \return Offset from the sample with the lowest delay. + static int64_t best_delay_offset(const std::vector& samples) { + const NtpSample* best = nullptr; + for (const auto& sample : samples) { + if (!sample.is_ok) { + continue; + } + if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) { + continue; + } + if (best == nullptr) { + best = &sample; + continue; + } + if (sample.delay_us > 0 && best->delay_us > 0 && sample.delay_us < best->delay_us) { + best = &sample; + } + } + return best ? best->offset_us : 0; + } + + /// \brief Access config. + /// \return Current pool configuration. + NtpPoolConfig config() const { + std::lock_guard lk(m_mtx); + return m_cfg; + } + /// \brief Replace pool configuration. + /// \param cfg New pool configuration. + void set_config(NtpPoolConfig cfg) { + std::lock_guard lk(m_mtx); + m_cfg = std::move(cfg); + } + + /// \brief Runtime state for a configured server. + struct ServerState { + NtpServerConfig cfg; + + std::chrono::steady_clock::time_point next_allowed{}; + std::chrono::milliseconds backoff{0}; + + int fail_count = 0; + + int64_t last_offset_us = 0; + int64_t last_delay_us = 0; + int last_error = 0; + bool is_last_ok = false; + }; + + NtpPoolConfig m_cfg; + + mutable std::mutex m_mtx; + std::vector m_servers; + std::vector m_last_samples; + + std::atomic m_offset_us; + + std::mt19937_64 m_rng; + + private: + static std::uint64_t init_seed(std::uint64_t seed) { + if (seed != 0) return seed; + const auto v = static_cast( + std::chrono::high_resolution_clock::now().time_since_epoch().count()); + return v ^ 0x9E3779B97F4A7C15ULL; + } + + std::vector pick_servers_locked(std::size_t servers_to_sample) { + std::vector eligible; + eligible.reserve(m_servers.size()); + + const auto now_point = std::chrono::steady_clock::now(); + for (std::size_t i = 0; i < m_servers.size(); ++i) { + if (now_point >= m_servers[i].next_allowed) { + eligible.push_back(i); + } + } + + if (eligible.empty()) { + return {}; + } + + std::shuffle(eligible.begin(), eligible.end(), m_rng); + if (servers_to_sample < eligible.size()) { + eligible.resize(servers_to_sample); + } + return eligible; + } + + NtpSample query_one(std::size_t server_index) { + NtpServerConfig cfg; + { + std::lock_guard lk(m_mtx); + cfg = m_servers[server_index].cfg; + m_servers[server_index].next_allowed = + std::chrono::steady_clock::now() + cfg.min_interval; + } + + NtpSample out; + out.host = cfg.host; + out.port = cfg.port; + out.max_delay_us = cfg.max_delay.count() > 0 ? cfg.max_delay.count() * 1000 : 0; + + ClientT client(cfg.host, cfg.port); + + bool is_ok = false; + try { + is_ok = client.query(); + } catch (...) { + out.error_code = client.last_error_code(); + } + + if (out.error_code == 0) { + out.error_code = client.last_error_code(); + } + if (out.error_code == 0 && !is_ok) { + out.error_code = -1; + } + + out.is_ok = is_ok; + out.offset_us = client.offset_us(); + out.delay_us = client.delay_us(); + out.stratum = client.stratum(); + + update_server_state_after_query(server_index, out); + return out; + } + + void update_server_state_after_query(std::size_t index, const NtpSample& sample) { + std::lock_guard lk(m_mtx); + auto& state = m_servers[index]; + + state.is_last_ok = sample.is_ok; + state.last_error = sample.error_code; + state.last_offset_us = sample.offset_us; + state.last_delay_us = sample.delay_us; + + if (sample.is_ok) { + state.fail_count = 0; + state.backoff = std::chrono::milliseconds(0); + return; + } + + state.fail_count++; + const auto init = state.cfg.backoff_initial; + const auto max_backoff = state.cfg.backoff_max; + + if (state.backoff.count() == 0) { + state.backoff = init; + } else { + state.backoff = std::min(max_backoff, state.backoff * 2); + } + + state.next_allowed = std::chrono::steady_clock::now() + state.backoff; + } + + bool update_from_samples(const std::vector& samples, const NtpPoolConfig& cfg) { + std::vector offsets; + offsets.reserve(samples.size()); + + for (const auto& sample : samples) { + if (!sample.is_ok) { + continue; + } + if (sample.max_delay_us > 0 && sample.delay_us > sample.max_delay_us) { + continue; + } + offsets.push_back(sample.offset_us); + } + + if (offsets.size() < cfg.min_valid_samples) { + return false; + } + + int64_t estimate = 0; + switch (cfg.aggregation) { + case NtpPoolConfig::Aggregation::BestDelay: + estimate = best_delay_offset(samples); + break; + case NtpPoolConfig::Aggregation::MedianMadTrim: + estimate = median_mad_trim(offsets); + break; + case NtpPoolConfig::Aggregation::Median: + default: + estimate = median(offsets); + break; + } + + double alpha = cfg.smoothing_alpha; + if (alpha < 0.0) { + alpha = 0.0; + } else if (alpha > 1.0) { + alpha = 1.0; + } + if (alpha >= 1.0) { + m_offset_us.store(estimate); + } else if (alpha > 0.0) { + const int64_t old_value = m_offset_us.load(); + const double new_value = + (1.0 - alpha) * static_cast(old_value) + alpha * static_cast(estimate); + m_offset_us.store(static_cast(new_value)); + } + return true; + } + + }; + + using NtpClientPool = NtpClientPoolT; +} // namespace time_shield + +#else // TIME_SHIELD_ENABLE_NTP_CLIENT + +namespace time_shield { + class NtpClientPool { + public: + NtpClientPool() { + static_assert(sizeof(void*) == 0, "NtpClientPool is disabled by configuration."); + } + }; +} // namespace time_shield + +#endif // TIME_SHIELD_ENABLE_NTP_CLIENT + +#endif // _TIME_SHIELD_NTP_CLIENT_POOL_HPP_INCLUDED diff --git a/include/time_shield/ntp_client_pool_runner.hpp b/include/time_shield/ntp_client_pool_runner.hpp new file mode 100644 index 00000000..89063530 --- /dev/null +++ b/include/time_shield/ntp_client_pool_runner.hpp @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_NTP_CLIENT_POOL_RUNNER_HPP_INCLUDED +#define _TIME_SHIELD_NTP_CLIENT_POOL_RUNNER_HPP_INCLUDED + +#include "config.hpp" + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include "ntp_client_pool.hpp" +#include "time_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace time_shield { + + /// \ingroup ntp + /// \brief Background runner that periodically measures NTP offsets using a pool. + /// + /// \code + /// time_shield::NtpClientPool pool; + /// pool.set_default_servers(); + /// + /// time_shield::NtpClientPoolRunner runner(std::move(pool)); + /// runner.start(std::chrono::seconds(30)); + /// + /// auto now_ms = runner.utc_time_ms(); + /// auto offset = runner.offset_us(); + /// + /// runner.stop(); + /// \endcode + template + class BasicPoolRunner { + public: + /// \brief Construct runner with a pool instance. + /// \param pool Pool instance to use. + explicit BasicPoolRunner(PoolT pool = PoolT{}) + : m_pool(std::move(pool)) {} + + /// \brief Stop background thread on destruction. + ~BasicPoolRunner() { + stop(); + } + + /// \brief Start periodic measurements on a background thread. + /// \param interval Measurement interval. + /// \param measure_immediately Measure before first sleep if true. + /// \return True when background thread started. + bool start(std::chrono::milliseconds interval = std::chrono::seconds(30), + bool measure_immediately = true) { + if (m_is_running.load()) { + return false; + } + if (interval.count() <= 0) { + interval = std::chrono::milliseconds(1); + } + + m_is_stop_requested.store(false); + m_is_force_requested.store(false); + m_is_running.store(true); + + try { + m_thread = std::thread(&BasicPoolRunner::run_loop, this, interval, measure_immediately); + } catch (...) { + m_is_running.store(false); + return false; + } + + return true; + } + + /// \brief Start periodic measurements using milliseconds. + /// \param interval_ms Measurement interval in milliseconds. + /// \param measure_immediately Measure before first sleep if true. + /// \return True when background thread started. + bool start(int interval_ms, bool measure_immediately = true) { + return start(std::chrono::milliseconds(interval_ms), measure_immediately); + } + + /// \brief Stop background measurements. + void stop() { + m_is_stop_requested.store(true); + m_cv.notify_all(); + if (m_thread.joinable()) { + m_thread.join(); + } + m_is_running.store(false); + } + + /// \brief Return true when background thread is running. + /// \return True when background measurements are active. + bool running() const noexcept { return m_is_running.load(); } + + /// \brief Wake the worker thread and request a measurement. + /// \return True when request accepted. + bool force_measure() { + if (!m_is_running.load()) { + return false; + } + m_is_force_requested.store(true); + m_cv.notify_one(); + return true; + } + + /// \brief Perform one measurement immediately. + /// \return True when pool offset updated. + bool measure_now() { + return do_measure(); + } + + /// \brief Return last estimated offset in microseconds. + /// \return Offset in microseconds (UTC - local realtime). + int64_t offset_us() const noexcept { return m_pool.offset_us(); } + /// \brief Return current UTC time in microseconds using pool offset. + /// \return UTC time in microseconds using pool offset. + int64_t utc_time_us() const noexcept { return m_pool.utc_time_us(); } + /// \brief Return current UTC time in milliseconds using pool offset. + /// \return UTC time in milliseconds using pool offset. + int64_t utc_time_ms() const noexcept { return m_pool.utc_time_ms(); } + /// \brief Return current UTC time in seconds using pool offset. + /// \return UTC time in seconds using pool offset. + int64_t utc_time_sec() const noexcept { return utc_time_us() / 1000000; } + + /// \brief Return whether last measurement updated the offset. + /// \return True when last measurement updated the offset. + bool last_measure_ok() const noexcept { return m_last_measure_ok.load(); } + /// \brief Return total number of measurement attempts. + /// \return Number of measurement attempts. + uint64_t measure_count() const noexcept { return m_measure_count.load(); } + /// \brief Return number of failed measurement attempts. + /// \return Number of failed measurement attempts. + uint64_t fail_count() const noexcept { return m_fail_count.load(); } + /// \brief Return realtime timestamp of last measurement attempt. + /// \return Realtime microseconds timestamp for last measurement attempt. + int64_t last_update_realtime_us() const noexcept { return m_last_update_realtime_us.load(); } + /// \brief Return realtime timestamp of last successful measurement. + /// \return Realtime microseconds timestamp for last successful measurement. + int64_t last_success_realtime_us() const noexcept { return m_last_success_realtime_us.load(); } + + /// \brief Return copy of the most recent samples. + /// \return Copy of samples from the last measurement. + std::vector last_samples() const { return m_pool.last_samples(); } + + private: + void run_loop(std::chrono::milliseconds interval, bool measure_immediately) { + bool is_first = measure_immediately; + while (!m_is_stop_requested.load()) { + if (is_first) { + do_measure(); + is_first = false; + } else { + std::unique_lock lk(m_cv_mtx); + m_cv.wait_for(lk, interval, [this]() { + return m_is_stop_requested.load() || m_is_force_requested.load(); + }); + if (m_is_stop_requested.load()) { + break; + } + m_is_force_requested.store(false); + do_measure(); + } + } + m_is_running.store(false); + } + + bool do_measure() { + bool is_ok = false; + try { + std::lock_guard lk(m_pool_mtx); + is_ok = m_pool.measure(); + } catch (...) { + is_ok = false; + } + + m_measure_count.fetch_add(1); + if (!is_ok) { + m_fail_count.fetch_add(1); + } + + m_last_measure_ok.store(is_ok); + + const int64_t now = now_realtime_us(); + m_last_update_realtime_us.store(now); + if (is_ok) { + m_last_success_realtime_us.store(now); + } + + return is_ok; + } + + private: + PoolT m_pool; + mutable std::mutex m_pool_mtx; + + std::thread m_thread; + std::condition_variable m_cv; + std::mutex m_cv_mtx; + + std::atomic m_is_running{false}; + std::atomic m_is_stop_requested{false}; + std::atomic m_is_force_requested{false}; + + std::atomic m_last_measure_ok{false}; + std::atomic m_measure_count{0}; + std::atomic m_fail_count{0}; + std::atomic m_last_update_realtime_us{0}; + std::atomic m_last_success_realtime_us{0}; + }; + + using NtpClientPoolRunner = BasicPoolRunner; + +} // namespace time_shield + +#else // TIME_SHIELD_ENABLE_NTP_CLIENT + +namespace time_shield { + class NtpClientPoolRunner { + public: + NtpClientPoolRunner() { + static_assert(sizeof(void*) == 0, "NtpClientPoolRunner is disabled by configuration."); + } + }; +} // namespace time_shield + +#endif // _TIME_SHIELD_ENABLE_NTP_CLIENT + +#endif // _TIME_SHIELD_NTP_CLIENT_POOL_RUNNER_HPP_INCLUDED diff --git a/include/time_shield/ntp_time_service.hpp b/include/time_shield/ntp_time_service.hpp new file mode 100644 index 00000000..e4b679eb --- /dev/null +++ b/include/time_shield/ntp_time_service.hpp @@ -0,0 +1,680 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_NTP_TIME_SERVICE_HPP_INCLUDED +#define _TIME_SHIELD_NTP_TIME_SERVICE_HPP_INCLUDED + +#include "config.hpp" + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include "ntp_client_pool.hpp" +#include "ntp_client_pool_runner.hpp" +#include "time_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace time_shield { + + template + class NtpTimeServiceT; + + namespace detail { +#ifdef TIME_SHIELD_TEST_FAKE_NTP + /// \brief Fake runner for tests without network access. + class FakeNtpRunner { + public: + /// \brief Construct fake runner. + FakeNtpRunner() = default; + /// \brief Construct fake runner with an unused pool. + explicit FakeNtpRunner(NtpClientPool) {} + + FakeNtpRunner(const FakeNtpRunner&) = delete; + FakeNtpRunner& operator=(const FakeNtpRunner&) = delete; + + /// \brief Stop background thread on destruction. + ~FakeNtpRunner() { + stop(); + } + + /// \brief Start fake measurements on a background thread. + bool start(std::chrono::milliseconds interval = std::chrono::seconds(30), + bool measure_immediately = true) { + if (m_is_running.load()) { + return false; + } + if (interval.count() <= 0) { + interval = std::chrono::milliseconds(1); + } + m_interval = interval; + m_is_stop_requested.store(false); + m_is_force_requested.store(false); + m_is_running.store(true); + m_measure_immediately = measure_immediately; + try { + m_thread = std::thread(&FakeNtpRunner::run_loop, this); + } catch (...) { + m_is_running.store(false); + return false; + } + return true; + } + + /// \brief Start fake measurements using milliseconds. + bool start(int interval_ms, bool measure_immediately = true) { + return start(std::chrono::milliseconds(interval_ms), measure_immediately); + } + + /// \brief Stop background measurements. + void stop() { + m_is_stop_requested.store(true); + m_cv.notify_all(); + if (m_thread.joinable()) { + m_thread.join(); + } + m_is_running.store(false); + } + + /// \brief Return true when background thread is running. + bool running() const noexcept { return m_is_running.load(); } + + /// \brief Wake the worker thread and request a measurement. + bool force_measure() { + if (!m_is_running.load()) { + return false; + } + m_is_force_requested.store(true); + m_cv.notify_one(); + return true; + } + + /// \brief Perform one measurement immediately. + bool measure_now() { + return do_measure(); + } + + /// \brief Return last estimated offset in microseconds. + int64_t offset_us() const noexcept { return m_offset_us.load(); } + /// \brief Return current UTC time in microseconds based on offset. + int64_t utc_time_us() const noexcept { return now_realtime_us() + m_offset_us.load(); } + /// \brief Return current UTC time in milliseconds based on offset. + int64_t utc_time_ms() const noexcept { return utc_time_us() / 1000; } + /// \brief Return current UTC time in seconds based on offset. + int64_t utc_time_sec() const noexcept { return utc_time_us() / 1000000; } + + /// \brief Return whether last measurement updated the offset. + bool last_measure_ok() const noexcept { return m_last_measure_ok.load(); } + /// \brief Return total number of measurement attempts. + uint64_t measure_count() const noexcept { return m_measure_count.load(); } + /// \brief Return number of failed measurement attempts. + uint64_t fail_count() const noexcept { return m_fail_count.load(); } + /// \brief Return realtime timestamp of last measurement attempt. + int64_t last_update_realtime_us() const noexcept { return m_last_update_realtime_us.load(); } + /// \brief Return realtime timestamp of last successful measurement. + int64_t last_success_realtime_us() const noexcept { return m_last_success_realtime_us.load(); } + + /// \brief Return copy of most recent samples. + std::vector last_samples() const { return {}; } + + private: + /// \brief Background loop for fake measurements. + void run_loop() { + const auto sleep_interval = m_interval; + bool is_first = m_measure_immediately; + while (!m_is_stop_requested.load()) { + if (is_first) { + do_measure(); + is_first = false; + } else { + std::unique_lock lk(m_cv_mtx); + m_cv.wait_for(lk, sleep_interval, [this]() { + return m_is_stop_requested.load() || m_is_force_requested.load(); + }); + if (m_is_stop_requested.load()) { + break; + } + m_is_force_requested.store(false); + do_measure(); + } + } + m_is_running.store(false); + } + + /// \brief Update fake offset and stats. + bool do_measure() { + const uint64_t count = m_measure_count.fetch_add(1) + 1; + m_offset_us.store(static_cast(count * 1000)); + m_last_measure_ok.store(true); + const int64_t now = now_realtime_us(); + m_last_update_realtime_us.store(now); + m_last_success_realtime_us.store(now); + return true; + } + + private: + std::chrono::milliseconds m_interval{std::chrono::seconds(30)}; + bool m_measure_immediately{true}; + + std::thread m_thread; + std::condition_variable m_cv; + std::mutex m_cv_mtx; + + std::atomic m_is_running{false}; + std::atomic m_is_stop_requested{false}; + std::atomic m_is_force_requested{false}; + + std::atomic m_last_measure_ok{false}; + std::atomic m_measure_count{0}; + std::atomic m_fail_count{0}; + std::atomic m_last_update_realtime_us{0}; + std::atomic m_last_success_realtime_us{0}; + std::atomic m_offset_us{0}; + }; +#endif // _TIME_SHIELD_TEST_FAKE_NTP + +#ifndef _TIME_SHIELD_CPP17 +#if defined(TIME_SHIELD_TEST_FAKE_NTP) + using RunnerAlias = detail::FakeNtpRunner; +#else + using RunnerAlias = NtpClientPoolRunner; +#endif + + extern NtpTimeServiceT g_ntp_time_service; +#endif // !TIME_SHIELD_CPP17 + } // namespace detail + + /// \ingroup ntp + /// \brief Singleton service for background NTP measurements. + /// + /// Uses an internal runner to keep offset updated. It exposes UTC time + /// computed as realtime clock plus the latest offset. Configure pool + /// servers and sampling before starting the service. + template + class NtpTimeServiceT { + public: + /// \brief Return the singleton instance. + /// \return Singleton instance. + static NtpTimeServiceT& instance() noexcept { +#ifdef TIME_SHIELD_CPP17 + return m_instance; +#else + return detail::g_ntp_time_service; +#endif + } + + NtpTimeServiceT(const NtpTimeServiceT&) = delete; + NtpTimeServiceT& operator=(const NtpTimeServiceT&) = delete; + + /// \brief Start background measurements using stored interval. + /// \return True when background runner started. + bool init() { + return init(m_interval, m_measure_immediately); + } + + /// \brief Start background measurements with interval and immediate flag. + /// \param interval Measurement interval. + /// \param measure_immediately Measure before first sleep if true. + /// \return True when background runner started. + bool init(std::chrono::milliseconds interval, bool measure_immediately = true) { + std::unique_ptr local_runner; + { + std::lock_guard lk(m_mtx); + if (is_running_locked()) { + return true; + } + if (interval.count() <= 0) { + interval = std::chrono::milliseconds(1); + } + m_interval = interval; + m_measure_immediately = measure_immediately; + + local_runner = build_runner_locked(); + if (!local_runner) { + return false; + } + } + + if (!local_runner->start(m_interval, m_measure_immediately)) { + return false; + } + + bool is_ok = false; + try { + is_ok = local_runner->measure_now(); + } catch (...) { + is_ok = false; + } + + { + std::lock_guard lk(m_mtx); + m_runner = std::move(local_runner); + } + return is_ok; + } + + /// \brief Stop background measurements and release resources. + void shutdown() { + std::unique_ptr local_runner; + { + std::lock_guard lk(m_mtx); + if (!m_runner) { + return; + } + local_runner = std::move(m_runner); + } + try { + local_runner->stop(); + } catch (...) { + // no-throw + } + } + + /// \brief Return true when background runner is active. + /// \return True when background runner is active. + bool running() const noexcept { + std::lock_guard lk(m_mtx); + return is_running_locked(); + } + + /// \brief Ensure background runner is started with current config. + void ensure_started() noexcept { + if (running()) { + return; + } + (void)init(); + } + + /// \brief Return last estimated offset in microseconds. + /// \return Offset in microseconds (UTC - local realtime). + int64_t offset_us() noexcept { + ensure_started(); + std::lock_guard lk(m_mtx); + if (!m_runner) return 0; + return m_runner->offset_us(); + } + + /// \brief Return current UTC time in microseconds based on offset. + /// \return UTC time in microseconds using last offset. + int64_t utc_time_us() noexcept { + ensure_started(); + std::lock_guard lk(m_mtx); + if (!m_runner) return now_realtime_us(); + return m_runner->utc_time_us(); + } + + /// \brief Return current UTC time in milliseconds based on offset. + /// \return UTC time in milliseconds using last offset. + int64_t utc_time_ms() noexcept { + return utc_time_us() / 1000; + } + + /// \brief Return current UTC time in seconds based on offset. + /// \return UTC time in seconds using last offset. + int64_t utc_time_sec() noexcept { + return utc_time_us() / 1000000; + } + + /// \brief Return whether last measurement updated the offset. + /// \return True when last measurement updated the offset. + bool last_measure_ok() const noexcept { + std::lock_guard lk(m_mtx); + if (!m_runner) return false; + return m_runner->last_measure_ok(); + } + + /// \brief Return total number of measurement attempts. + /// \return Number of measurement attempts. + uint64_t measure_count() const noexcept { + std::lock_guard lk(m_mtx); + if (!m_runner) return 0; + return m_runner->measure_count(); + } + + /// \brief Return number of failed measurement attempts. + /// \return Number of failed measurement attempts. + uint64_t fail_count() const noexcept { + std::lock_guard lk(m_mtx); + if (!m_runner) return 0; + return m_runner->fail_count(); + } + + /// \brief Return realtime timestamp of last measurement attempt. + /// \return Realtime microseconds timestamp for last measurement attempt. + int64_t last_update_realtime_us() const noexcept { + std::lock_guard lk(m_mtx); + if (!m_runner) return 0; + return m_runner->last_update_realtime_us(); + } + + /// \brief Return realtime timestamp of last successful measurement. + /// \return Realtime microseconds timestamp for last successful measurement. + int64_t last_success_realtime_us() const noexcept { + std::lock_guard lk(m_mtx); + if (!m_runner) return 0; + return m_runner->last_success_realtime_us(); + } + + /// \brief Return true when last measurement is older than max_age. + /// \param max_age Maximum allowed age. + /// \return True when last measurement age exceeds max_age. + bool stale(std::chrono::milliseconds max_age) const noexcept { + const int64_t last = last_update_realtime_us(); + if (last == 0) { + return true; + } + const int64_t age = now_realtime_us() - last; + return age > max_age.count() * 1000; + } + + /// \brief Replace server list used for new runner instances. + /// \param servers Server configurations to use. + /// \return False when service is already running. + bool set_servers(std::vector servers) { + std::lock_guard lk(m_mtx); + if (is_running_locked()) { + return false; + } + m_has_custom_servers = true; + m_servers = std::move(servers); + return true; + } + + /// \brief Use conservative default servers for new runner instances. + /// \return False when service is already running. + bool set_default_servers() { + std::lock_guard lk(m_mtx); + if (is_running_locked()) { + return false; + } + m_has_custom_servers = true; + m_servers = NtpClientPool::build_default_servers(); + return true; + } + + /// \brief Clear custom server list and return to default behavior. + /// \return False when service is already running. + bool clear_servers() { + std::lock_guard lk(m_mtx); + if (is_running_locked()) { + return false; + } + m_has_custom_servers = false; + m_servers.clear(); + return true; + } + + /// \brief Override pool configuration for new runner instances. + /// \param cfg Pool configuration to apply. + /// \return False when service is already running. + bool set_pool_config(NtpPoolConfig cfg) { + std::lock_guard lk(m_mtx); + if (is_running_locked()) { + return false; + } + m_has_custom_pool_cfg = true; + m_pool_cfg = std::move(cfg); + return true; + } + + /// \brief Return current pool configuration. + /// \return Current pool configuration. + NtpPoolConfig pool_config() const { + std::lock_guard lk(m_mtx); + if (m_has_custom_pool_cfg) { + return m_pool_cfg; + } + return NtpPoolConfig{}; + } + + /// \brief Return copy of last measurement samples. + /// \return Copy of samples from the last measurement. + std::vector last_samples() const { + std::lock_guard lk(m_mtx); + if (!m_runner) return {}; + return m_runner->last_samples(); + } + + /// \brief Construct service. + NtpTimeServiceT() = default; + /// \brief Stop background runner on destruction. + ~NtpTimeServiceT() { + shutdown(); + } + + /// \brief Apply current config by rebuilding the runner. + /// \return True when runner restarted successfully. + bool apply_config_now() { + std::unique_ptr new_runner; + std::unique_ptr old_runner; + std::chrono::milliseconds interval; + bool measure_immediately = true; + { + std::lock_guard lk(m_mtx); + new_runner = build_runner_locked(); + if (!new_runner) { + return false; + } + interval = m_interval; + measure_immediately = m_measure_immediately; + old_runner = std::move(m_runner); + } + + if (old_runner) { + try { + old_runner->stop(); + } catch (...) { + } + } + + if (!new_runner->start(interval, measure_immediately)) { + return false; + } + + bool is_ok = false; + try { + is_ok = new_runner->measure_now(); + } catch (...) { + is_ok = false; + } + + { + std::lock_guard lk(m_mtx); + m_runner = std::move(new_runner); + } + return is_ok; + } + + private: + /// \brief Check runner status under lock. + bool is_running_locked() const noexcept { + return m_runner && m_runner->running(); + } + + /// \brief Build a runner with current server list and pool config. + std::unique_ptr build_runner_locked() { + std::vector servers; + if (m_has_custom_servers) { + servers = m_servers; + } else { + servers = NtpClientPool::build_default_servers(); + } + + NtpPoolConfig cfg = m_has_custom_pool_cfg ? m_pool_cfg : NtpPoolConfig{}; + NtpClientPool pool(cfg); + pool.set_servers(std::move(servers)); + + std::unique_ptr runner; + try { + runner.reset(new RunnerT(std::move(pool))); + } catch (...) { + return nullptr; + } + return runner; + } + + private: + mutable std::mutex m_mtx; + std::chrono::milliseconds m_interval{std::chrono::seconds(30)}; + bool m_measure_immediately{true}; + + bool m_has_custom_servers{false}; + std::vector m_servers; + + bool m_has_custom_pool_cfg{false}; + NtpPoolConfig m_pool_cfg{}; + + std::unique_ptr m_runner; + +#ifdef TIME_SHIELD_CPP17 + static NtpTimeServiceT m_instance; +#endif + }; + +#ifdef TIME_SHIELD_CPP17 + template + inline NtpTimeServiceT NtpTimeServiceT::m_instance{}; +#endif + +#ifndef _TIME_SHIELD_CPP17 +namespace detail { +#if defined(TIME_SHIELD_NTP_TIME_SERVICE_DEFINE) + NtpTimeServiceT g_ntp_time_service; +#endif +} // namespace detail +#endif + +#if defined(TIME_SHIELD_TEST_FAKE_NTP) + /// \ingroup ntp + /// \brief NTP time service alias that uses a fake runner for tests. + using NtpTimeService = NtpTimeServiceT; +#else + /// \ingroup ntp + /// \brief NTP time service alias that uses the real pool runner. + using NtpTimeService = NtpTimeServiceT; +#endif + +namespace ntp { + + /// \ingroup ntp + /// \brief Initialize NTP time service and start background measurements. + /// \param interval Measurement interval. + /// \param measure_immediately Measure before first sleep if true. + /// \return True when background runner started. + inline bool init(std::chrono::milliseconds interval = std::chrono::seconds(30), + bool measure_immediately = true) { + return NtpTimeService::instance().init(interval, measure_immediately); + } + + /// \ingroup ntp + /// \brief Initialize NTP time service using milliseconds. + /// \param interval_ms Measurement interval in milliseconds. + /// \param measure_immediately Measure before first sleep if true. + /// \return True when background runner started. + inline bool init(int interval_ms, + bool measure_immediately = true) { + return NtpTimeService::instance().init(std::chrono::milliseconds(interval_ms), measure_immediately); + } + + /// \ingroup ntp + /// \brief Stop NTP time service. + inline void shutdown() { + NtpTimeService::instance().shutdown(); + } + + /// \ingroup ntp + /// \brief Return last estimated offset in microseconds. + /// \return Offset in microseconds (UTC - local realtime). + inline int64_t offset_us() noexcept { + return NtpTimeService::instance().offset_us(); + } + + /// \ingroup ntp + /// \brief Return current UTC time in microseconds based on offset. + /// \return UTC time in microseconds using last offset. + inline int64_t utc_time_us() noexcept { + return NtpTimeService::instance().utc_time_us(); + } + + /// \ingroup ntp + /// \brief Return current UTC time in milliseconds based on offset. + /// \return UTC time in milliseconds using last offset. + inline int64_t utc_time_ms() noexcept { + return NtpTimeService::instance().utc_time_ms(); + } + + /// \ingroup ntp + /// \brief Return current UTC time in seconds based on offset. + /// \return UTC time in seconds using last offset. + inline int64_t utc_time_sec() noexcept { + return NtpTimeService::instance().utc_time_sec(); + } + + /// \ingroup ntp + /// \brief Return whether last measurement updated the offset. + /// \return True when last measurement updated the offset. + inline bool last_measure_ok() noexcept { + return NtpTimeService::instance().last_measure_ok(); + } + + /// \ingroup ntp + /// \brief Return total number of measurement attempts. + /// \return Number of measurement attempts. + inline uint64_t measure_count() noexcept { + return NtpTimeService::instance().measure_count(); + } + + /// \ingroup ntp + /// \brief Return number of failed measurement attempts. + /// \return Number of failed measurement attempts. + inline uint64_t fail_count() noexcept { + return NtpTimeService::instance().fail_count(); + } + + /// \ingroup ntp + /// \brief Return realtime timestamp of last measurement attempt. + /// \return Realtime microseconds timestamp for last measurement attempt. + inline int64_t last_update_realtime_us() noexcept { + return NtpTimeService::instance().last_update_realtime_us(); + } + + /// \ingroup ntp + /// \brief Return realtime timestamp of last successful measurement. + /// \return Realtime microseconds timestamp for last successful measurement. + inline int64_t last_success_realtime_us() noexcept { + return NtpTimeService::instance().last_success_realtime_us(); + } + + /// \ingroup ntp + /// \brief Return true when last measurement is older than max_age. + /// \param max_age Maximum allowed age. + /// \return True when last measurement age exceeds max_age. + inline bool stale(std::chrono::milliseconds max_age) noexcept { + return NtpTimeService::instance().stale(max_age); + } + +} // namespace ntp + +} // namespace time_shield + +#else // TIME_SHIELD_ENABLE_NTP_CLIENT + +namespace time_shield { + class NtpTimeService { + public: + static NtpTimeService& instance() { + static_assert(sizeof(void*) == 0, "NtpTimeService is disabled by configuration."); + return *reinterpret_cast(0); + } + }; +} // namespace time_shield + +#endif // _TIME_SHIELD_ENABLE_NTP_CLIENT + +#endif // _TIME_SHIELD_NTP_TIME_SERVICE_HPP_INCLUDED diff --git a/include/time_shield/ole_automation_conversions.hpp b/include/time_shield/ole_automation_conversions.hpp new file mode 100644 index 00000000..a7dc674c --- /dev/null +++ b/include/time_shield/ole_automation_conversions.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_OLE_AUTOMATION_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_OLE_AUTOMATION_CONVERSIONS_HPP_INCLUDED + +/// \file ole_automation_conversions.hpp +/// \brief OLE Automation Date (OA date) conversions. +/// \ingroup time_conversions +/// +/// OA date is a floating-point day count where: +/// - 0.0 is 1899-12-30 00:00:00 +/// - the integer part is days offset from that date +/// - the fractional part is time-of-day / 24 +/// +/// This header provides conversions between OA date and: +/// - Unix timestamps in seconds (ts_t) +/// - Unix timestamps in milliseconds (ts_ms_t) +/// - floating seconds (fts_t) + +#include "config.hpp" +#include "types.hpp" +#include "constants.hpp" +#include "time_conversions.hpp" + +namespace time_shield { + + /// \brief Convert Unix timestamp (seconds) to OA date. + /// \param ts Unix timestamp in seconds (may be negative). + /// \return OA date value. + TIME_SHIELD_CONSTEXPR inline oadate_t ts_to_oadate(ts_t ts) noexcept { + return static_cast(OLE_EPOCH) + + static_cast(ts) / static_cast(SEC_PER_DAY); + } + + /// \brief Convert Unix timestamp (floating seconds) to OA date. + /// \param ts Unix timestamp in seconds as floating point (may be negative). + /// \return OA date value. + TIME_SHIELD_CONSTEXPR inline oadate_t fts_to_oadate(fts_t ts) noexcept { + return static_cast(OLE_EPOCH) + + static_cast(ts) / static_cast(SEC_PER_DAY); + } + + /// \brief Convert Unix timestamp (milliseconds) to OA date. + /// \param ts_ms Unix timestamp in milliseconds (may be negative). + /// \return OA date value. + TIME_SHIELD_CONSTEXPR inline oadate_t ts_ms_to_oadate(ts_ms_t ts_ms) noexcept { + return static_cast(OLE_EPOCH) + + static_cast(ts_ms) / static_cast(MS_PER_DAY); + } + + /// \brief Convert OA date to Unix timestamp (seconds). + /// \param oa OA date value. + /// \return Unix timestamp in seconds (truncated toward zero). + TIME_SHIELD_CONSTEXPR inline ts_t oadate_to_ts(oadate_t oa) noexcept { + const oadate_t seconds = (oa - static_cast(OLE_EPOCH)) + * static_cast(SEC_PER_DAY); + return static_cast(seconds); + } + + /// \brief Convert OA date to Unix timestamp (floating seconds). + /// \param oa OA date value. + /// \return Unix timestamp in seconds as floating point. + TIME_SHIELD_CONSTEXPR inline fts_t oadate_to_fts(oadate_t oa) noexcept { + return static_cast((oa - static_cast(OLE_EPOCH)) + * static_cast(SEC_PER_DAY)); + } + + /// \brief Convert OA date to Unix timestamp (milliseconds). + /// \param oa OA date value. + /// \return Unix timestamp in milliseconds (truncated toward zero). + TIME_SHIELD_CONSTEXPR inline ts_ms_t oadate_to_ts_ms(oadate_t oa) noexcept { + const oadate_t ms = (oa - static_cast(OLE_EPOCH)) + * static_cast(MS_PER_DAY); + return static_cast(ms); + } + + /// \brief Build OA date from calendar components (Gregorian). + /// \tparam T1 Year type. + /// \tparam T2 Month/day/time components type. + /// \tparam T3 Milliseconds type. + /// \return OA date value. + template + TIME_SHIELD_CONSTEXPR inline oadate_t to_oadate( + T1 year, T2 month, T2 day, + T2 hour = 0, T2 min = 0, T2 sec = 0, T3 ms = 0) noexcept { + // Use existing conversion to floating timestamp (seconds). + const fts_t fts = to_ftimestamp(year, month, day, hour, min, sec, ms); + return fts_to_oadate(fts); + } + +} // namespace time_shield + +#endif // _TIME_SHIELD_OLE_AUTOMATION_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/time_conversion_aliases.hpp b/include/time_shield/time_conversion_aliases.hpp index 0d80f5b5..12a16c33 100644 --- a/include/time_shield/time_conversion_aliases.hpp +++ b/include/time_shield/time_conversion_aliases.hpp @@ -10,23 +10,32 @@ /// to functions like `ts`, `get_ts`, etc., which are defined via macros. /// This file should be included only at the end of `time_conversion.hpp`. +#include + namespace time_shield { - + /// \ingroup time_conversions /// \{ - - /// \brief Alias for get_unix_year function. - /// \copydoc get_unix_year + + /// \brief Alias for years_since_epoch function. + /// \copydoc years_since_epoch template constexpr T unix_year(ts_t ts) noexcept { - return get_unix_year(ts); + return years_since_epoch(ts); } - /// \brief Alias for get_unix_year function. - /// \copydoc get_unix_year + /// \brief Alias for years_since_epoch function. + /// \copydoc years_since_epoch template constexpr T to_unix_year(ts_t ts) noexcept { - return get_unix_year(ts); + return years_since_epoch(ts); + } + + /// \brief Alias for years_since_epoch function. + /// \copydoc years_since_epoch + template + constexpr T get_unix_year(ts_t ts) noexcept { + return years_since_epoch(ts); } //------------------------------------------------------------------------------ @@ -34,127 +43,141 @@ namespace time_shield { /// \brief Alias for get_unix_day function. /// \copydoc get_unix_day - template + template constexpr T get_unixday(ts_t ts = time_shield::ts()) noexcept { - return get_unix_day(ts); + return days_since_epoch(ts); } - /// \brief Alias for get_unix_day function. - /// \copydoc get_unix_day - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T unix_day(ts_t ts = time_shield::ts()) noexcept { - return get_unix_day(ts); + return days_since_epoch(ts); } - /// \brief Alias for get_unix_day function. - /// \copydoc get_unix_day - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T unixday(ts_t ts = time_shield::ts()) noexcept { - return get_unix_day(ts); + return days_since_epoch(ts); } - /// \brief Alias for get_unix_day function. - /// \copydoc get_unix_day - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T uday(ts_t ts = time_shield::ts()) noexcept { - return get_unix_day(ts); + return days_since_epoch(ts); + } + + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template + constexpr T get_unix_day(ts_t ts = time_shield::ts()) noexcept { + return days_since_epoch(ts); } //------------------------------------------------------------------------------ - /// \brief Alias for get_unix_day_ms function. - /// \copydoc get_unix_day_ms - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T get_unixday_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { - return get_unix_day_ms(t_ms); + return days_since_epoch_ms(t_ms); } - /// \brief Alias for get_unix_day_ms function. - /// \copydoc get_unix_day_ms - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T unix_day_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { - return get_unix_day_ms(t_ms); + return days_since_epoch_ms(t_ms); } - /// \brief Alias for get_unix_day_ms function. - /// \copydoc get_unix_day_ms - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T unixday_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { - return get_unix_day_ms(t_ms); + return days_since_epoch_ms(t_ms); } - /// \brief Alias for get_unix_day_ms function. - /// \copydoc get_unix_day_ms - template + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template constexpr T uday_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { - return get_unix_day_ms(t_ms); + return days_since_epoch_ms(t_ms); + } + + /// \brief Alias for days_since_epoch function. + /// \copydoc days_since_epoch + template + constexpr T get_unix_day_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { + return days_since_epoch_ms(t_ms); } //------------------------------------------------------------------------------ - /// \brief Alias for unix_day_to_timestamp function. - /// \copydoc unix_day_to_timestamp + /// \brief Alias for unix_day_to_ts function. + /// \copydoc unix_day_to_ts template - constexpr T unix_day_to_ts(uday_t unix_day) noexcept { - return unix_day_to_timestamp(unix_day); + constexpr T unix_day_to_timestamp(dse_t unix_day) noexcept { + return unix_day_to_ts(unix_day); } - /// \brief Alias for unix_day_to_timestamp function. - /// \copydoc unix_day_to_timestamp + /// \brief Alias for unix_day_to_ts function. + /// \copydoc unix_day_to_ts template - constexpr T unixday_to_ts(uday_t unix_day) noexcept { - return unix_day_to_timestamp(unix_day); + constexpr T unixday_to_ts(dse_t unix_day) noexcept { + return unix_day_to_ts(unix_day); } - /// \brief Alias for unix_day_to_timestamp function. - /// \copydoc unix_day_to_timestamp + /// \brief Alias for unix_day_to_ts function. + /// \copydoc unix_day_to_ts template - constexpr T uday_to_ts(uday_t unix_day) noexcept { - return unix_day_to_timestamp(unix_day); + constexpr T uday_to_ts(dse_t unix_day) noexcept { + return unix_day_to_ts(unix_day); } - /// \brief Alias for unix_day_to_timestamp function. - /// \copydoc unix_day_to_timestamp + /// \brief Alias for unix_day_to_ts function. + /// \copydoc unix_day_to_ts template - constexpr T start_of_day_from_unix_day(uday_t unix_day) noexcept { - return unix_day_to_timestamp(unix_day); + constexpr T start_of_day_from_unix_day(dse_t unix_day) noexcept { + return unix_day_to_ts(unix_day); } //------------------------------------------------------------------------------ - /// \brief Alias for unix_day_to_timestamp_ms function. - /// \copydoc unix_day_to_timestamp_ms + /// \brief Alias for unix_day_to_ts_ms function. + /// \copydoc unix_day_to_ts_ms template - constexpr T unix_day_to_ts_ms(uday_t unix_day) noexcept { - return unix_day_to_timestamp_ms(unix_day); + constexpr T unix_day_to_timestamp_ms(dse_t unix_day) noexcept { + return unix_day_to_ts_ms(unix_day); } - /// \brief Alias for unix_day_to_timestamp_ms function. - /// \copydoc unix_day_to_timestamp_ms + /// \brief Alias for unix_day_to_ts_ms function. + /// \copydoc unix_day_to_ts_ms template - constexpr T unixday_to_ts_ms(uday_t unix_day) noexcept { - return unix_day_to_timestamp_ms(unix_day); + constexpr T unixday_to_ts_ms(dse_t unix_day) noexcept { + return unix_day_to_ts_ms(unix_day); } - /// \brief Alias for unix_day_to_timestamp_ms function. - /// \copydoc unix_day_to_timestamp_ms + /// \brief Alias for unix_day_to_ts_ms function. + /// \copydoc unix_day_to_ts_ms template - constexpr T uday_to_ts_ms(uday_t unix_day) noexcept { - return unix_day_to_timestamp_ms(unix_day); + constexpr T uday_to_ts_ms(dse_t unix_day) noexcept { + return unix_day_to_ts_ms(unix_day); } - /// \brief Alias for unix_day_to_timestamp_ms function. - /// \copydoc unix_day_to_timestamp_ms + /// \brief Alias for unix_day_to_ts_ms function. + /// \copydoc unix_day_to_ts_ms template - constexpr T start_of_day_from_unix_day_ms(uday_t unix_day) noexcept { - return unix_day_to_timestamp_ms(unix_day); + constexpr T start_of_day_from_unix_day_ms(dse_t unix_day) noexcept { + return unix_day_to_ts_ms(unix_day); } //------------------------------------------------------------------------------ @@ -162,21 +185,21 @@ namespace time_shield { /// \brief Alias for start_of_next_day_from_unix_day function. /// \copydoc start_of_next_day_from_unix_day template - constexpr T next_day_from_unix_day(uday_t unix_day) noexcept { + constexpr T next_day_from_unix_day(dse_t unix_day) noexcept { return start_of_next_day_from_unix_day(unix_day); } /// \brief Alias for start_of_next_day_from_unix_day function. /// \copydoc start_of_next_day_from_unix_day template - constexpr T next_day_unix_day(uday_t unix_day) noexcept { + constexpr T next_day_unix_day(dse_t unix_day) noexcept { return start_of_next_day_from_unix_day(unix_day); } /// \brief Alias for start_of_next_day_from_unix_day function. /// \copydoc start_of_next_day_from_unix_day template - constexpr T next_day_unixday(uday_t unix_day) noexcept { + constexpr T next_day_unixday(dse_t unix_day) noexcept { return start_of_next_day_from_unix_day(unix_day); } @@ -185,47 +208,80 @@ namespace time_shield { /// \brief Alias for start_of_next_day_from_unix_day_ms function. /// \copydoc start_of_next_day_from_unix_day_ms template - constexpr T next_day_from_unix_day_ms(uday_t unix_day) noexcept { + constexpr T next_day_from_unix_day_ms(dse_t unix_day) noexcept { return start_of_next_day_from_unix_day_ms(unix_day); } /// \brief Alias for start_of_next_day_from_unix_day_ms function. /// \copydoc start_of_next_day_from_unix_day_ms template - constexpr T next_day_unix_day_ms(uday_t unix_day) noexcept { + constexpr T next_day_unix_day_ms(dse_t unix_day) noexcept { return start_of_next_day_from_unix_day_ms(unix_day); } /// \brief Alias for start_of_next_day_from_unix_day_ms function. /// \copydoc start_of_next_day_from_unix_day_ms template - constexpr T next_day_unixday_ms(uday_t unix_day) noexcept { + constexpr T next_day_unixday_ms(dse_t unix_day) noexcept { return start_of_next_day_from_unix_day_ms(unix_day); } //------------------------------------------------------------------------------ - /// \brief Alias for get_unix_min function. - /// \copydoc get_unix_min + /// \brief Alias for min_since_epoch function. + /// \copydoc min_since_epoch + template + constexpr T minutes_since_epoch(ts_t ts = time_shield::ts()) { + return min_since_epoch(ts); + } + + /// \brief Alias for min_since_epoch function. + /// \copydoc min_since_epoch template constexpr T unix_min(ts_t ts = time_shield::ts()) { - return get_unix_min(ts); + return min_since_epoch(ts); } - /// \brief Alias for get_unix_min function. - /// \copydoc get_unix_min + /// \brief Alias for min_since_epoch function. + /// \copydoc min_since_epoch template constexpr T to_unix_min(ts_t ts = time_shield::ts()) { - return get_unix_min(ts); + return min_since_epoch(ts); } - /// \brief Alias for get_unix_min function. - /// \copydoc get_unix_min + /// \brief Alias for min_since_epoch function. + /// \copydoc min_since_epoch template constexpr T umin(ts_t ts = time_shield::ts()) { - return get_unix_min(ts); + return min_since_epoch(ts); } + /// \brief Alias for min_since_epoch function. + /// \copydoc min_since_epoch + template + constexpr T get_unix_min(ts_t ts = time_shield::ts()) { + return min_since_epoch(ts); + } + +//------------------------------------------------------------------------------ + + /// \ingroup time_structures + /// \brief Alias for dt_to_timestamp. + /// \copydoc dt_to_timestamp + template + TIME_SHIELD_CONSTEXPR inline auto dt_to_ts(const T& date_time) + -> decltype(dt_to_timestamp(date_time)) { + return dt_to_timestamp(date_time); + } + + /// \ingroup time_structures + /// \brief Alias for tm_to_timestamp. + /// \copydoc tm_to_timestamp + TIME_SHIELD_CONSTEXPR inline auto tm_to_ts(const std::tm* timeinfo) + -> decltype(tm_to_timestamp(timeinfo)) { + return tm_to_timestamp(timeinfo); + } + //------------------------------------------------------------------------------ /// \brief Alias for hour24_to_12 function. @@ -253,6 +309,14 @@ namespace time_shield { return to_date_time(ts); } + /// \ingroup time_structures + /// \brief Alias for to_date_time function. + /// \copydoc to_date_time + inline auto to_dt(ts_t ts) + -> decltype(to_date_time(ts)) { + return to_date_time(ts); + } + //------------------------------------------------------------------------------ /// \ingroup time_structures @@ -270,7 +334,15 @@ namespace time_shield { inline T to_dt_struct_ms(ts_ms_t ts) { return to_date_time_ms(ts); } - + + /// \ingroup time_structures + /// \brief Alias for to_date_time_ms function. + /// \copydoc to_date_time_ms + inline auto to_dt_ms(ts_ms_t ts_ms) + -> decltype(to_date_time_ms(ts_ms)) { + return to_date_time_ms(ts_ms); + } + //------------------------------------------------------------------------------ @@ -725,43 +797,43 @@ namespace time_shield { /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t ts(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t ts(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t get_ts(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t get_ts(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t timestamp(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t timestamp(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t get_timestamp(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t get_timestamp(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t to_ts(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t to_ts(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t ts_from_tm(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t ts_from_tm(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } /// \brief Alias for tm_to_timestamp /// \copydoc tm_to_timestamp - constexpr ts_t to_timestamp(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline ts_t to_timestamp(const std::tm* timeinfo) { return tm_to_timestamp(timeinfo); } @@ -780,7 +852,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t ts_ms(year_t year, int month, int day) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t ts_ms(year_t year, int month, int day) { return to_timestamp_ms(year, month, day); } @@ -798,7 +870,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t ts_ms(year_t year, int month, int day, int hour) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t ts_ms(year_t year, int month, int day, int hour) { return to_timestamp_ms(year, month, day, hour); } @@ -817,7 +889,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t ts_ms(year_t year, int month, int day, int hour, int min) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t ts_ms(year_t year, int month, int day, int hour, int min) { return to_timestamp_ms(year, month, day, hour, min); } @@ -837,7 +909,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t ts_ms(year_t year, int month, int day, int hour, int min, int sec) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t ts_ms(year_t year, int month, int day, int hour, int min, int sec) { return to_timestamp_ms(year, month, day, hour, min, sec); } @@ -858,7 +930,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t ts_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t ts_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { return to_timestamp_ms(year, month, day, hour, min, sec, ms); } @@ -875,7 +947,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_ts_ms(year_t year, int month, int day) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_ts_ms(year_t year, int month, int day) { return to_timestamp_ms(year, month, day); } @@ -893,7 +965,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_ts_ms(year_t year, int month, int day, int hour) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_ts_ms(year_t year, int month, int day, int hour) { return to_timestamp_ms(year, month, day, hour); } @@ -912,7 +984,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_ts_ms(year_t year, int month, int day, int hour, int min) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_ts_ms(year_t year, int month, int day, int hour, int min) { return to_timestamp_ms(year, month, day, hour, min); } @@ -932,7 +1004,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_ts_ms(year_t year, int month, int day, int hour, int min, int sec) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_ts_ms(year_t year, int month, int day, int hour, int min, int sec) { return to_timestamp_ms(year, month, day, hour, min, sec); } @@ -953,7 +1025,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_ts_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_ts_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { return to_timestamp_ms(year, month, day, hour, min, sec, ms); } @@ -970,7 +1042,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_timestamp_ms(year_t year, int month, int day) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_timestamp_ms(year_t year, int month, int day) { return to_timestamp_ms(year, month, day); } @@ -988,7 +1060,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour) { return to_timestamp_ms(year, month, day, hour); } @@ -1007,7 +1079,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour, int min) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour, int min) { return to_timestamp_ms(year, month, day, hour, min); } @@ -1027,7 +1099,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour, int min, int sec) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour, int min, int sec) { return to_timestamp_ms(year, month, day, hour, min, sec); } @@ -1048,7 +1120,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t get_timestamp_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { return to_timestamp_ms(year, month, day, hour, min, sec, ms); } @@ -1065,7 +1137,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t timestamp_ms(year_t year, int month, int day) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t timestamp_ms(year_t year, int month, int day) { return to_timestamp_ms(year, month, day); } @@ -1083,7 +1155,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t timestamp_ms(year_t year, int month, int day, int hour) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t timestamp_ms(year_t year, int month, int day, int hour) { return to_timestamp_ms(year, month, day, hour); } @@ -1102,7 +1174,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t timestamp_ms(year_t year, int month, int day, int hour, int min) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t timestamp_ms(year_t year, int month, int day, int hour, int min) { return to_timestamp_ms(year, month, day, hour, min); } @@ -1122,7 +1194,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t timestamp_ms(year_t year, int month, int day, int hour, int min, int sec) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t timestamp_ms(year_t year, int month, int day, int hour, int min, int sec) { return to_timestamp_ms(year, month, day, hour, min, sec); } @@ -1143,7 +1215,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t timestamp_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t timestamp_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { return to_timestamp_ms(year, month, day, hour, min, sec, ms); } @@ -1160,7 +1232,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t to_ts_ms(year_t year, int month, int day) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_ts_ms(year_t year, int month, int day) { return to_timestamp_ms(year, month, day); } @@ -1178,7 +1250,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t to_ts_ms(year_t year, int month, int day, int hour) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_ts_ms(year_t year, int month, int day, int hour) { return to_timestamp_ms(year, month, day, hour); } @@ -1197,7 +1269,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t to_ts_ms(year_t year, int month, int day, int hour, int min) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_ts_ms(year_t year, int month, int day, int hour, int min) { return to_timestamp_ms(year, month, day, hour, min); } @@ -1217,7 +1289,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t to_ts_ms(year_t year, int month, int day, int hour, int min, int sec) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_ts_ms(year_t year, int month, int day, int hour, int min, int sec) { return to_timestamp_ms(year, month, day, hour, min, sec); } @@ -1238,7 +1310,7 @@ namespace time_shield { /// \return Timestamp in milliseconds representing the given date and time. /// \throws std::invalid_argument if the date-time combination is invalid. /// \see to_timestamp_ms - constexpr ts_ms_t to_ts_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_ts_ms(year_t year, int month, int day, int hour, int min, int sec, int ms) { return to_timestamp_ms(year, month, day, hour, min, sec, ms); } @@ -1253,6 +1325,15 @@ namespace time_shield { return dt_to_timestamp_ms(date_time); } + /// \ingroup time_structures + /// \brief Alias for dt_to_timestamp_ms function. + /// \copydoc dt_to_timestamp_ms + template + TIME_SHIELD_CONSTEXPR inline auto dt_to_ts_ms(const T& date_time) + -> decltype(dt_to_timestamp_ms(date_time)) { + return dt_to_timestamp_ms(date_time); + } + /// \ingroup time_structures /// \brief Alias for dt_to_timestamp_ms function. /// \copydoc dt_to_timestamp_ms @@ -1289,6 +1370,13 @@ namespace time_shield { return tm_to_timestamp_ms(timeinfo); } + /// \brief Alias for tm_to_timestamp_ms function. + /// \copydoc tm_to_timestamp_ms + TIME_SHIELD_CONSTEXPR inline auto tm_to_ts_ms(const std::tm *timeinfo) + -> decltype(tm_to_timestamp_ms(timeinfo)) { + return tm_to_timestamp_ms(timeinfo); + } + /// \brief Alias for tm_to_timestamp_ms function. /// \copydoc tm_to_timestamp_ms TIME_SHIELD_CONSTEXPR inline ts_t to_ts_ms( @@ -1394,6 +1482,15 @@ namespace time_shield { return dt_to_ftimestamp(date_time); } + /// \ingroup time_structures + /// \brief Alias for dt_to_ftimestamp + /// \copydoc dt_to_ftimestamp + template + constexpr auto dt_to_fts(const T& date_time) + -> decltype(dt_to_ftimestamp(date_time)) { + return dt_to_ftimestamp(date_time); + } + /// \ingroup time_structures /// \brief Alias for dt_to_ftimestamp /// \copydoc dt_to_ftimestamp @@ -1423,77 +1520,113 @@ namespace time_shield { /// \ingroup time_structures /// \brief Alias for tm_to_ftimestamp /// \copydoc tm_to_ftimestamp(const std::tm*) - constexpr fts_t to_ftimestamp(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline fts_t to_ftimestamp(const std::tm* timeinfo) { return tm_to_ftimestamp(timeinfo); } /// \ingroup time_structures /// \brief Alias for tm_to_ftimestamp /// \copydoc tm_to_ftimestamp(const std::tm*) - constexpr fts_t to_fts(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline auto tm_to_fts(const std::tm* timeinfo) + -> decltype(tm_to_ftimestamp(timeinfo)) { + return tm_to_ftimestamp(timeinfo); + } + + /// \ingroup time_structures + /// \brief Alias for tm_to_ftimestamp + /// \copydoc tm_to_ftimestamp(const std::tm*) + TIME_SHIELD_CONSTEXPR inline fts_t to_fts(const std::tm* timeinfo) { return tm_to_ftimestamp(timeinfo); } /// \ingroup time_structures /// \brief Alias for tm_to_ftimestamp /// \copydoc tm_to_ftimestamp(const std::tm*) - constexpr fts_t fts(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline fts_t fts(const std::tm* timeinfo) { return tm_to_ftimestamp(timeinfo); } /// \ingroup time_structures /// \brief Alias for tm_to_ftimestamp /// \copydoc tm_to_ftimestamp(const std::tm*) - constexpr fts_t ftimestamp(const std::tm* timeinfo) { + TIME_SHIELD_CONSTEXPR inline fts_t ftimestamp(const std::tm* timeinfo) { return tm_to_ftimestamp(timeinfo); } //------------------------------------------------------------------------------ - /// \brief Alias for get_days_difference function. - /// \copydoc get_days_difference + /// \brief Alias for days_between function. + /// \copydoc days_between template constexpr T get_days(ts_t start, ts_t stop) noexcept { - return get_days_difference(start, stop); + return days_between(start, stop); } - /// \brief Alias for get_days_difference function. - /// \copydoc get_days_difference + /// \brief Alias for days_between function. + /// \copydoc days_between template constexpr T days(ts_t start, ts_t stop) noexcept { - return get_days_difference(start, stop); + return days_between(start, stop); + } + + /// \brief Alias for days_between function. + /// \copydoc days_between + template + constexpr T get_days_difference(ts_t start, ts_t stop) noexcept { + return days_between(start, stop); + } + + /// \brief Alias for days_between function. + /// \copydoc days_between + template + constexpr T diff_in_days(ts_t start, ts_t stop) noexcept { + return days_between(start, stop); } //------------------------------------------------------------------------------ - /// \brief Alias for get_year function. - /// \copydoc get_year + /// \brief Alias for year_of function. + /// \copydoc year_of template TIME_SHIELD_CONSTEXPR inline T year(ts_t ts = time_shield::ts()) { - return get_year(ts); + return year_of(ts); } - /// \brief Alias for get_year function. - /// \copydoc get_year + /// \brief Alias for year_of function. + /// \copydoc year_of template TIME_SHIELD_CONSTEXPR inline T to_year(ts_t ts = time_shield::ts()) { - return get_year(ts); + return year_of(ts); + } + + /// \brief Alias for year_of function. + /// \copydoc year_of + template + TIME_SHIELD_CONSTEXPR inline T get_year(ts_t ts = time_shield::ts()) { + return year_of(ts); } //------------------------------------------------------------------------------ - /// \brief Alias for get_year_ms function. - /// \copydoc get_year_ms + /// \brief Alias for year_of_ms function. + /// \copydoc year_of_ms template TIME_SHIELD_CONSTEXPR inline T year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { - return get_year_ms(ts_ms); + return year_of_ms(ts_ms); } - /// \brief Alias for get_year_ms function. - /// \copydoc get_year_ms + /// \brief Alias for year_of_ms function. + /// \copydoc year_of_ms template TIME_SHIELD_CONSTEXPR inline T to_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { - return get_year_ms(ts_ms); + return year_of_ms(ts_ms); + } + + /// \brief Alias for year_of_ms function. + /// \copydoc year_of_ms + template + TIME_SHIELD_CONSTEXPR inline T get_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { + return year_of_ms(ts_ms); } //------------------------------------------------------------------------------ @@ -1514,13 +1647,13 @@ namespace time_shield { /// \brief Alias for start_of_year_ms function. /// \copydoc start_of_year_ms - TIME_SHIELD_CONSTEXPR inline ts_t year_start_ms(ts_t ts_ms = time_shield::ts_ms()) { + inline ts_ms_t year_start_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { return start_of_year_ms(ts_ms); } /// \brief Alias for start_of_year_ms function. /// \copydoc start_of_year_ms - TIME_SHIELD_CONSTEXPR inline ts_t year_begin_ms(ts_t ts_ms = time_shield::ts_ms()) { + inline ts_ms_t year_begin_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { return start_of_year_ms(ts_ms); } @@ -1759,91 +1892,123 @@ namespace time_shield { //------------------------------------------------------------------------------ /// \ingroup time_structures - /// \brief Alias for get_weekday_from_date - /// \copydoc get_weekday_from_date + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date + template::value, int>::type = 0> + TIME_SHIELD_CONSTEXPR inline T1 get_weekday_from_date(const T2& date) { + return weekday_of_date(date); + } + + /// \ingroup time_structures + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date template::value, int>::type = 0> constexpr T1 get_dow(const T2& date) { - return get_weekday_from_date(date); + return weekday_of_date(date); } /// \ingroup time_structures - /// \brief Alias for get_weekday_from_date - /// \copydoc get_weekday_from_date + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date template::value, int>::type = 0> constexpr T1 dow_from_date(const T2& date) { - return get_weekday_from_date(date); + return weekday_of_date(date); } /// \ingroup time_structures - /// \brief Alias for get_weekday_from_date - /// \copydoc get_weekday_from_date + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date template::value, int>::type = 0> constexpr T1 weekday_of(const T2& date) { - return get_weekday_from_date(date); + return weekday_of_date(date); } /// \ingroup time_structures - /// \brief Alias for get_weekday_from_date - /// \copydoc get_weekday_from_date + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date template::value, int>::type = 0> constexpr T1 day_of_week_dt(const T2& date) { - return get_weekday_from_date(date); + return weekday_of_date(date); } /// \ingroup time_structures - /// \brief Alias for get_weekday_from_date - /// \copydoc get_weekday_from_date + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date template::value, int>::type = 0> constexpr T1 day_of_week(const T2& date) { - return get_weekday_from_date(date); + return weekday_of_date(date); } /// \ingroup time_structures - /// \brief Alias for get_weekday_from_date - /// \copydoc get_weekday_from_date + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date template::value, int>::type = 0> constexpr T1 dow(const T2& date) { - return get_weekday_from_date(date); + return weekday_of_date(date); + } + + /// \ingroup time_structures + /// \brief Alias for weekday_of_date + /// \copydoc weekday_of_date + template::value, int>::type = 0> + constexpr T1 wd(const T2& date) { + return weekday_of_date(date); } //------------------------------------------------------------------------------ - /// \brief Alias for get_weekday_from_ts - /// \copydoc get_weekday_from_ts + /// \brief Alias for weekday_of_ts + /// \copydoc weekday_of_ts template::value, int>::type = 0> constexpr T day_of_week(U ts) noexcept { - return get_weekday_from_ts(ts); + return weekday_of_ts(ts); } - /// \brief Alias for get_weekday_from_ts - /// \copydoc get_weekday_from_ts + /// \brief Alias for weekday_of_ts + /// \copydoc weekday_of_ts template::value, int>::type = 0> constexpr T dow_ts(U ts) noexcept { - return get_weekday_from_ts(ts); + return weekday_of_ts(ts); } - /// \brief Alias for get_weekday_from_ts - /// \copydoc get_weekday_from_ts + /// \brief Alias for weekday_of_ts + /// \copydoc weekday_of_ts template::value, int>::type = 0> constexpr T get_dow_from_ts(U ts) noexcept { - return get_weekday_from_ts(ts); + return weekday_of_ts(ts); } - /// \brief Alias for get_weekday_from_ts - /// \copydoc get_weekday_from_ts + /// \brief Alias for weekday_of_ts + /// \copydoc weekday_of_ts template::value, int>::type = 0> - constexpr T weekday_of_ts(U ts) noexcept { - return get_weekday_from_ts(ts); + constexpr T get_weekday_from_ts(U ts) noexcept { + return weekday_of_ts(ts); } - + //------------------------------------------------------------------------------ - /// \brief Alias for get_weekday_from_ts_ms function. - /// \copydoc get_weekday_from_ts_ms + /// \brief Alias for weekday_of_ts + /// \copydoc weekday_of_ts + template::value, int>::type = 0> + constexpr T wd_ts(U ts) noexcept { + return weekday_of_ts(ts); + } + +//------------------------------------------------------------------------------ + + /// \brief Alias for weekday_of_ts_ms function. + /// \copydoc weekday_of_ts_ms template constexpr T day_of_week_ms(ts_ms_t ts_ms) { - return get_weekday_from_ts_ms(ts_ms); + return weekday_of_ts_ms(ts_ms); + } + + /// \brief Alias for weekday_of_ts_ms function. + /// \copydoc weekday_of_ts_ms + template + constexpr T wd_ms(ts_ms_t ts_ms) { + return weekday_of_ts_ms(ts_ms); } //------------------------------------------------------------------------------ @@ -1975,9 +2140,48 @@ namespace time_shield { constexpr ts_t finish_of_min(ts_t ts = time_shield::ts()) noexcept { return end_of_min(ts); } - + +//------------------------------------------------------------------------------ + + /// \brief Alias for is_workday(ts_t). + /// \copydoc is_workday(ts_t) + TIME_SHIELD_CONSTEXPR inline bool workday(ts_t ts) noexcept { + return is_workday(ts); + } + + /// \brief Alias for is_workday(ts_ms_t). + /// \copydoc is_workday(ts_ms_t) + TIME_SHIELD_CONSTEXPR inline bool workday_ms(ts_ms_t ts_ms) noexcept { + return is_workday_ms(ts_ms); + } + + /// \brief Alias for is_workday(year_t, int, int). + /// \copydoc is_workday(year_t, int, int) + TIME_SHIELD_CONSTEXPR inline bool workday(year_t year, int month, int day) noexcept { + return is_workday(year, month, day); + } + + /// \brief Alias for to_tz_offset. + /// \copydoc to_tz_offset + template + TIME_SHIELD_CONSTEXPR inline tz_t tz_offset(const T& tz) noexcept { + return to_tz_offset(tz); + } + + /// \brief Alias for tz_offset_hm. + /// \copydoc tz_offset_hm + TIME_SHIELD_CONSTEXPR inline tz_t offset_hm(int hour, int min = 0) noexcept { + return tz_offset_hm(hour, min); + } + + /// \brief Alias for is_valid_tz_offset. + /// \copydoc is_valid_tz_offset + TIME_SHIELD_CONSTEXPR inline bool valid_tz_offset(tz_t off) noexcept { + return is_valid_tz_offset(off); + } + /// \} }; // namespace time_shield -#endif // _TIME_SHIELD_TIME_CONVERSIONS_ALIASES_HPP_INCLUDED \ No newline at end of file +#endif // _TIME_SHIELD_TIME_CONVERSIONS_ALIASES_HPP_INCLUDED diff --git a/include/time_shield/time_conversions.hpp b/include/time_shield/time_conversions.hpp index 7e95332e..5c60570d 100644 --- a/include/time_shield/time_conversions.hpp +++ b/include/time_shield/time_conversions.hpp @@ -4,1875 +4,26 @@ #define _TIME_SHIELD_TIME_CONVERSIONS_HPP_INCLUDED /// \file time_conversions.hpp -/// \brief Header file for time conversion functions. -/// -/// This file contains functions for converting between different time representations -/// and performing various time-related calculations. +/// \brief Umbrella header for time conversion functions. +#include "config.hpp" +#include "types.hpp" +#include "constants.hpp" #include "enums.hpp" -#include "validation.hpp" -#include "time_utils.hpp" -#include "time_zone_struct.hpp" +#include "time_struct.hpp" +#include "date_struct.hpp" #include "date_time_struct.hpp" -#include -#include -#include - -namespace time_shield { - -/// \ingroup time_conversions -/// \{ - - /// \brief Get the nanosecond part of the second from a floating-point timestamp. - /// \tparam T Type of the returned value (default is int). - /// \param ts Timestamp in floating-point seconds. - /// \return T Nanosecond part of the second. - template - constexpr T ns_of_sec(fts_t ts) noexcept { - fts_t temp; - return static_cast(std::round(std::modf(ts, &temp) * static_cast(NS_PER_SEC))); - } - - /// \brief Get the microsecond part of the second from a floating-point timestamp. - /// \tparam T Type of the returned value (default is int). - /// \param ts Timestamp in floating-point seconds. - /// \return T Microsecond part of the second. - template - constexpr T us_of_sec(fts_t ts) noexcept { - fts_t temp; - return static_cast(std::round(std::modf(ts, &temp) * static_cast(US_PER_SEC))); - } - - /// \brief Get the millisecond part of the second from a floating-point timestamp. - /// \tparam T Type of the returned value (default is int). - /// \param ts Timestamp in floating-point seconds. - /// \return T Millisecond part of the second. - template - constexpr T ms_of_sec(fts_t ts) noexcept { - fts_t temp; - return static_cast(std::round(std::modf(ts, &temp) * static_cast(MS_PER_SEC))); - } - - /// \brief Get the millisecond part of the timestamp. - /// \tparam T Type of the returned value (default is int). - /// \param ts Timestamp in milliseconds. - /// \return T Millisecond part of the timestamp. - template - constexpr T ms_of_ts(ts_ms_t ts) noexcept { - return ts % MS_PER_SEC; - } - -# ifndef TIME_SHIELD_CPP17 - /// \brief Helper function for converting seconds to milliseconds (floating-point version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in seconds. - /// \param tag std::true_type indicates a floating-point type. - /// \return ts_ms_t Timestamp in milliseconds. - template - constexpr ts_ms_t sec_to_ms_impl(T t, std::true_type tag) noexcept { - return static_cast(std::round(t * static_cast(MS_PER_SEC))); - } - - /// \brief Helper function for converting seconds to milliseconds (integral version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in seconds. - /// \param tag std::false_type indicates a non-floating-point type. - /// \return ts_ms_t Timestamp in milliseconds. - template - constexpr ts_ms_t sec_to_ms_impl(T t, std::false_type tag) noexcept { - return static_cast(t) * static_cast(MS_PER_SEC); - } -# endif // TIME_SHIELD_CPP17 - - /// \brief Converts a timestamp from seconds to milliseconds. - /// \tparam T1 The type of the output timestamp (default is ts_ms_t). - /// \tparam T2 The type of the input timestamp. - /// \param ts Timestamp in seconds. - /// \return T1 Timestamp in milliseconds. - template - constexpr T1 sec_to_ms(T2 ts) noexcept { -# ifdef TIME_SHIELD_CPP17 - if constexpr (std::is_floating_point_v) { - return static_cast(std::round(ts * static_cast(MS_PER_SEC))); - } else { - return static_cast(ts) * static_cast(MS_PER_SEC); - } -# else - return sec_to_ms_impl(ts, typename std::conditional< - (std::is_same::value || std::is_same::value), - std::true_type, - std::false_type - >::type{}); -# endif - } - - /// \brief Converts a floating-point timestamp from seconds to milliseconds. - /// \param ts Timestamp in floating-point seconds. - /// \return ts_ms_t Timestamp in milliseconds. - inline ts_ms_t fsec_to_ms(fts_t ts) noexcept { - return static_cast(std::round(ts * static_cast(MS_PER_SEC))); - } - - /// \brief Converts a timestamp from milliseconds to seconds. - /// \tparam T1 The type of the output timestamp (default is ts_t). - /// \tparam T2 The type of the input timestamp (default is ts_ms_t). - /// \param ts_ms Timestamp in milliseconds. - /// \return T1 Timestamp in seconds. - template - constexpr T1 ms_to_sec(T2 ts_ms) noexcept { - return static_cast(ts_ms) / static_cast(MS_PER_SEC); - } - - /// \brief Converts a timestamp from milliseconds to floating-point seconds. - /// \tparam T The type of the input timestamp (default is ts_ms_t). - /// \param ts_ms Timestamp in milliseconds. - /// \return fts_t Timestamp in floating-point seconds. - template - constexpr fts_t ms_to_fsec(T ts_ms) noexcept { - return static_cast(ts_ms) / static_cast(MS_PER_SEC); - } - -//----------------------------------------------------------------------------// -// Minutes -> Milliseconds -//----------------------------------------------------------------------------// -# ifndef TIME_SHIELD_CPP17 - /// \brief Helper function for converting minutes to milliseconds (floating-point version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in minutes. - /// \param tag std::true_type indicates a floating-point type (double or float). - /// \return ts_ms_t Timestamp in milliseconds. - template - constexpr ts_ms_t min_to_ms_impl(T t, std::true_type tag) noexcept { - return static_cast(std::round(t * static_cast(MS_PER_MIN))); - } - - /// \brief Helper function for converting minutes to milliseconds (integral version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in minutes. - /// \param tag std::false_type indicates a non-floating-point type. - /// \return ts_ms_t Timestamp in milliseconds. - template - constexpr ts_ms_t min_to_ms_impl(T t, std::false_type tag) noexcept { - return static_cast(t) * static_cast(MS_PER_MIN); - } -# endif // TIME_SHIELD_CPP17 - - /// \brief Converts a timestamp from minutes to milliseconds. - /// \tparam T1 The type of the output timestamp (default is ts_ms_t). - /// \tparam T2 The type of the input timestamp. - /// \param ts Timestamp in minutes. - /// \return T1 Timestamp in milliseconds. - template - constexpr T1 min_to_ms(T2 ts) noexcept { -# ifdef TIME_SHIELD_CPP17 - if constexpr (std::is_floating_point_v) { - return static_cast(std::round(ts * static_cast(MS_PER_MIN))); - } else { - return static_cast(ts) * static_cast(MS_PER_MIN); - } -# else - return min_to_ms_impl(ts, typename std::conditional< - (std::is_same::value || std::is_same::value), - std::true_type, - std::false_type - >::type{}); -# endif - } - - /// \brief Converts a timestamp from milliseconds to minutes. - /// \tparam T1 The type of the output timestamp (default is int). - /// \tparam T2 The type of the input timestamp (default is ts_ms_t). - /// \param ts Timestamp in milliseconds. - /// \return T1 Timestamp in minutes. - template - constexpr T1 ms_to_min(T2 ts) noexcept { - return static_cast(ts) / static_cast(MS_PER_MIN); - } - -//----------------------------------------------------------------------------// -// Minutes -> Seconds -//----------------------------------------------------------------------------// -# ifndef TIME_SHIELD_CPP17 - /// \brief Helper function for converting minutes to seconds (floating-point version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in minutes. - /// \param tag std::true_type indicates a floating-point type (double or float). - /// \return ts_t Timestamp in seconds. - template - constexpr ts_t min_to_sec_impl(T t, std::true_type tag) noexcept { - return static_cast(std::round(t * static_cast(SEC_PER_MIN))); - } - - /// \brief Helper function for converting minutes to seconds (integral version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in minutes. - /// \param tag std::false_type indicates a non-floating-point type. - /// \return ts_t Timestamp in seconds. - template - constexpr ts_t min_to_sec_impl(T t, std::false_type tag) noexcept { - return static_cast(t) * static_cast(SEC_PER_MIN); - } -# endif // TIME_SHIELD_CPP17 - - /// \brief Converts a timestamp from minutes to seconds. - /// \tparam T1 The type of the output timestamp (default is ts_t). - /// \tparam T2 The type of the input timestamp. - /// \param ts Timestamp in minutes. - /// \return T1 Timestamp in seconds. - template - constexpr T1 min_to_sec(T2 ts) noexcept { -# ifdef TIME_SHIELD_CPP17 - if constexpr (std::is_floating_point_v) { - return static_cast(std::round(ts * static_cast(SEC_PER_MIN))); - } else { - return static_cast(ts) * static_cast(SEC_PER_MIN); - } -# else - return min_to_sec_impl(ts, typename std::conditional< - (std::is_same::value || std::is_same::value), - std::true_type, - std::false_type - >::type{}); -# endif - } - - /// \brief Converts a timestamp from seconds to minutes. - /// \tparam T1 The type of the output timestamp (default is int). - /// \tparam T2 The type of the input timestamp (default is ts_t). - /// \param ts Timestamp in seconds. - /// \return T1 Timestamp in minutes. - template - constexpr T1 sec_to_min(T2 ts) noexcept { - return static_cast(ts) / static_cast(SEC_PER_MIN); - } - - /// \brief Converts a timestamp from minutes to floating-point seconds. - /// \tparam T The type of the input timestamp (default is int). - /// \param min Timestamp in minutes. - /// \return fts_t Timestamp in floating-point seconds. - template - constexpr fts_t min_to_fsec(T min) noexcept { - return static_cast(min) * static_cast(SEC_PER_MIN); - } - - /// \brief Converts a timestamp from seconds to floating-point minutes. - /// \tparam T The type of the input timestamp (default is ts_t). - /// \param ts Timestamp in seconds. - /// \return double Timestamp in floating-point minutes. - template - constexpr double sec_to_fmin(T ts) noexcept { - return static_cast(ts) / static_cast(SEC_PER_MIN); - } - -//----------------------------------------------------------------------------// -// Hours -> Milliseconds -//----------------------------------------------------------------------------// - -# ifndef TIME_SHIELD_CPP17 - /// \brief Helper function for converting hours to milliseconds (floating-point version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in hours. - /// \param tag std::true_type indicates a floating-point type (double or float). - /// \return ts_ms_t Timestamp in milliseconds. - template - constexpr ts_ms_t hour_to_ms_impl(T t, std::true_type tag) noexcept { - return static_cast(std::round(t * static_cast(MS_PER_HOUR))); - } - - /// \brief Helper function for converting hours to milliseconds (integral version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in hours. - /// \param tag Type tag used to select the integral overload (must be std::false_type). - /// \return ts_ms_t Timestamp in milliseconds. - template - constexpr ts_ms_t hour_to_ms_impl(T t, std::false_type tag) noexcept { - return static_cast(t) * static_cast(MS_PER_HOUR); - } -# endif // TIME_SHIELD_CPP17 - - /// \brief Converts a timestamp from hours to milliseconds. - /// \tparam T1 The type of the output timestamp (default is ts_ms_t). - /// \tparam T2 The type of the input timestamp. - /// \param ts Timestamp in hours. - /// \return T1 Timestamp in milliseconds. - template - constexpr T1 hour_to_ms(T2 ts) noexcept { -# ifdef TIME_SHIELD_CPP17 - if constexpr (std::is_floating_point_v) { - return static_cast(std::round(ts * static_cast(MS_PER_HOUR))); - } else { - return static_cast(ts) * static_cast(MS_PER_HOUR); - } -# else - return hour_to_ms_impl(ts, typename std::conditional< - (std::is_same::value || std::is_same::value), - std::true_type, - std::false_type - >::type{}); -# endif - } - - /// \brief Converts a timestamp from milliseconds to hours. - /// \tparam T1 The type of the output timestamp (default is int). - /// \tparam T2 The type of the input timestamp (default is ts_ms_t). - /// \param ts Timestamp in milliseconds. - /// \return T1 Timestamp in hours. - template - constexpr T1 ms_to_hour(T2 ts) noexcept { - return static_cast(ts) / static_cast(MS_PER_HOUR); - } - -//----------------------------------------------------------------------------// -// Hours -> Seconds -//----------------------------------------------------------------------------// - -# ifndef TIME_SHIELD_CPP17 - /// \brief Helper function for converting hours to seconds (floating-point version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in hours. - /// \param tag std::true_type indicates a floating-point type (double or float). - /// \return ts_t Timestamp in seconds. - template - constexpr ts_t hour_to_sec_impl(T t, std::true_type tag) noexcept { - return static_cast(std::round(t * static_cast(SEC_PER_HOUR))); - } - - /// \brief Helper function for converting hours to seconds (integral version). - /// \tparam T Type of the input timestamp. - /// \param t Timestamp in hours. - /// \param tag std::false_type indicates a non-floating-point type. - /// \return ts_t Timestamp in seconds. - template - constexpr ts_t hour_to_sec_impl(T t, std::false_type tag) noexcept { - return static_cast(t) * static_cast(SEC_PER_HOUR); - } -# endif // TIME_SHIELD_CPP17 - - /// \brief Converts a timestamp from hours to seconds. - /// \tparam T1 The type of the output timestamp (default is ts_t). - /// \tparam T2 The type of the input timestamp. - /// \param ts Timestamp in hours. - /// \return T1 Timestamp in seconds. - template - constexpr T1 hour_to_sec(T2 ts) noexcept { -# ifdef TIME_SHIELD_CPP17 - if constexpr (std::is_floating_point_v) { - return static_cast(std::round(ts * static_cast(SEC_PER_HOUR))); - } else { - return static_cast(ts) * static_cast(SEC_PER_HOUR); - } -# else - return hour_to_sec_impl(ts, typename std::conditional< - (std::is_same::value || std::is_same::value), - std::true_type, - std::false_type - >::type{}); -# endif - } - - /// \brief Converts a timestamp from seconds to hours. - /// \tparam T1 The type of the output timestamp (default is int). - /// \tparam T2 The type of the input timestamp (default is ts_t). - /// \param ts Timestamp in seconds. - /// \return T1 Timestamp in hours. - template - constexpr T1 sec_to_hour(T2 ts) noexcept { - return static_cast(ts) / static_cast(SEC_PER_HOUR); - } - - /// \brief Converts a timestamp from hours to floating-point seconds. - /// \tparam T The type of the input timestamp (default is int). - /// \param hr Timestamp in hours. - /// \return fts_t Timestamp in floating-point seconds. - template - constexpr fts_t hour_to_fsec(T hr) noexcept { - return static_cast(hr) * static_cast(SEC_PER_HOUR); - } - - /// \brief Converts a timestamp from seconds to floating-point hours. - /// \tparam T The type of the input timestamp (default is ts_t). - /// \param ts Timestamp in seconds. - /// \return double Timestamp in floating-point hours. - template - constexpr double sec_to_fhour(T ts) noexcept { - return static_cast(ts) / static_cast(SEC_PER_HOUR); - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a UNIX timestamp to a year. - /// \tparam T The type of the year (default is year_t). - /// \param ts UNIX timestamp. - /// \return T Year corresponding to the given timestamp. - template - constexpr T get_unix_year(ts_t ts) noexcept { - // 9223372029693630000 - значение на момент 292277024400 от 2000 года - // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS - // Поэтому пришлось снизить до 9223371890843040000 - constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; - constexpr int64_t BIAS_2000 = 946684800LL; - - int64_t y = MAX_YEAR; - int64_t secs = -((ts - BIAS_2000) - BIAS_292277022000); - - const int64_t n_400_years = secs / SEC_PER_400_YEARS; - secs -= n_400_years * SEC_PER_400_YEARS; - y -= n_400_years * 400; - - const int64_t n_100_years = secs / SEC_PER_100_YEARS; - secs -= n_100_years * SEC_PER_100_YEARS; - y -= n_100_years * 100; - - const int64_t n_4_years = secs / SEC_PER_4_YEARS; - secs -= n_4_years * SEC_PER_4_YEARS; - y -= n_4_years * 4; - - const int64_t n_1_years = secs / SEC_PER_YEAR; - secs -= n_1_years * SEC_PER_YEAR; - y -= n_1_years; - - y = secs == 0 ? y : y - 1; - return y - UNIX_EPOCH; - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a 24-hour format hour to a 12-hour format. - /// \tparam T Numeric type of the hour (default is int). - /// \param hour The hour in 24-hour format to convert. - /// \return The hour in 12-hour format. - template - TIME_SHIELD_CONSTEXPR inline T hour24_to_12(T hour) noexcept { - if (hour == 0 || hour > 12) return 12; - return hour; - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Converts a timestamp to a date-time structure. - /// - /// This function converts a timestamp (usually an integer representing seconds since epoch) - /// to a custom date-time structure. The default type for the timestamp is int64_t. - /// - /// \tparam T1 The date-time structure type to be returned. - /// \tparam T2 The type of the timestamp (default is int64_t). - /// \param ts The timestamp to be converted. - /// \return A date-time structure of type T1. - template - T1 to_date_time(T2 ts) { - // 9223372029693630000 - значение на момент 292277024400 от 2000 года - // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS - // Поэтому пришлось снизить до 9223371890843040000 - constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; - constexpr int64_t BIAS_2000 = 946684800LL; - - int64_t y = MAX_YEAR; - uint64_t secs = -((ts - BIAS_2000) - BIAS_292277022000); - - const uint64_t n_400_years = secs / SEC_PER_400_YEARS; - secs -= n_400_years * SEC_PER_400_YEARS; - y -= n_400_years * 400; - - const uint64_t n_100_years = secs / SEC_PER_100_YEARS; - secs -= n_100_years * SEC_PER_100_YEARS; - y -= n_100_years * 100; - - const uint64_t n_4_years = secs / SEC_PER_4_YEARS; - secs -= n_4_years * SEC_PER_4_YEARS; - y -= n_4_years * 4; - - const uint64_t n_1_years = secs / SEC_PER_YEAR; - secs -= n_1_years * SEC_PER_YEAR; - y -= n_1_years; - - T1 date_time; - - if (secs == 0) { - date_time.year = y; - date_time.mon = 1; - date_time.day = 1; - return date_time; - } - - date_time.year = y - 1; - const bool is_leap_year = is_leap_year_date(date_time.year); - secs = is_leap_year ? SEC_PER_LEAP_YEAR - secs : SEC_PER_YEAR - secs; - const int days = static_cast(secs / SEC_PER_DAY); - - constexpr int JAN_AND_FEB_DAY_LEAP_YEAR = 60 - 1; - constexpr int TABLE_MONTH_OF_YEAR[] = { - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 31 январь - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 28 февраль - 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 31 март - 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, // 30 апрель - 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, - 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, - 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, - 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, - }; - constexpr int TABLE_DAY_OF_YEAR[] = { - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 январь - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, // 28 февраль - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 март - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, // 30 апрель - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - }; - - if (is_leap_year) { - const int prev_days = days - 1; - date_time.day = days == JAN_AND_FEB_DAY_LEAP_YEAR ? (TABLE_DAY_OF_YEAR[prev_days] + 1) : - (days > JAN_AND_FEB_DAY_LEAP_YEAR ? TABLE_DAY_OF_YEAR[prev_days] : TABLE_DAY_OF_YEAR[days]); - date_time.mon = days >= JAN_AND_FEB_DAY_LEAP_YEAR ? TABLE_MONTH_OF_YEAR[prev_days] : TABLE_MONTH_OF_YEAR[days]; - } else { - date_time.day = TABLE_DAY_OF_YEAR[days]; - date_time.mon = TABLE_MONTH_OF_YEAR[days]; - } - - ts_t day_secs = static_cast(secs % SEC_PER_DAY); - date_time.hour = static_cast(day_secs / SEC_PER_HOUR); - ts_t min_secs = static_cast(day_secs - date_time.hour * SEC_PER_HOUR); - date_time.min = static_cast(min_secs / SEC_PER_MIN); - date_time.sec = static_cast(min_secs - date_time.min * SEC_PER_MIN); -# ifdef TIME_SHIELD_CPP17 - if TIME_SHIELD_IF_CONSTEXPR (std::is_floating_point::value) { - date_time.ms = static_cast(std::round(std::fmod(static_cast(ts), static_cast(MS_PER_SEC)))); - } else date_time.ms = 0; -# else - if (std::is_floating_point::value) { - date_time.ms = static_cast(std::round(std::fmod(static_cast(ts), static_cast(MS_PER_SEC)))); - } else date_time.ms = 0; -# endif - return date_time; - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Converts a timestamp in milliseconds to a date-time structure with milliseconds. - /// \tparam T The type of the date-time structure to return. - /// \param ts The timestamp in milliseconds to convert. - /// \return T A date-time structure with the corresponding date and time components. - template - inline T to_date_time_ms(ts_ms_t ts) { - T date_time = to_date_time(ms_to_sec(ts)); - date_time.ms = ms_of_ts(ts); // Extract and set the ms component - return date_time; - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a date and time to a timestamp. - /// - /// This function converts a given date and time to a timestamp, which is the number - /// of seconds since the Unix epoch (January 1, 1970). - /// - /// If the `day` is ≥ 1970 and `year` ≤ 31, parameters are assumed to be in DD-MM-YYYY order - /// and are automatically reordered. - /// - /// \tparam T1 The type of the year parameter (default is int64_t). - /// \tparam T2 The type of the other date and time parameters (default is int). - /// \param year The year value. - /// \param month The month value. - /// \param day The day value. - /// \param hour The hour value (default is 0). - /// \param min The minute value (default is 0). - /// \param sec The second value (default is 0). - /// \return Timestamp representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - /// - /// \par Aliases: - /// The following function names are provided as aliases: - /// - `ts(...)` - /// - `get_ts(...)` - /// - `get_timestamp(...)` - /// - `timestamp(...)` - /// - `to_ts(...)` - /// - /// These aliases are macro-generated and behave identically to `to_timestamp`. - /// - /// \sa ts() \sa get_ts() \sa get_timestamp() \sa timestamp() \sa to_ts() - template - TIME_SHIELD_CONSTEXPR inline ts_t to_timestamp( - T1 year, - T2 month, - T2 day, - T2 hour = 0, - T2 min = 0, - T2 sec = 0) { - - if (day >= UNIX_EPOCH && year <= 31) { - return to_timestamp((T1)day, month, (T2)year, hour, min, sec); - } - if (!is_valid_date_time(year, month, day, hour, min, sec)) { - throw std::invalid_argument("Invalid date-time combination"); - } - - int64_t secs = 0; - uint64_t years = (static_cast(MAX_YEAR) - year); - - const int64_t n_400_years = years / 400; - secs += n_400_years * SEC_PER_400_YEARS; - years -= n_400_years * 400; - - const int64_t n_100_years = years / 100; - secs += n_100_years * SEC_PER_100_YEARS; - years -= n_100_years * 100; - - const int64_t n_4_years = years / 4; - secs += n_4_years * SEC_PER_4_YEARS; - years -= n_4_years * 4; - - secs += years * SEC_PER_YEAR; - - // 9223372029693630000 - значение на момент 292277024400 от 2000 года - // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS - // Поэтому пришлось снизить до 9223371890843040000 - constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; - constexpr int64_t BIAS_2000 = 946684800LL; - - secs = BIAS_292277022000 - secs; - secs += BIAS_2000; - - if (month == 1 && day == 1 && - hour == 0 && min == 0 && - sec == 0) { - return secs; - } - - constexpr int lmos[] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}; - constexpr int mos[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; - - secs += (is_leap_year_date(year) ? (lmos[month - 1] + day - 1) : (mos[month - 1] + day - 1)) * SEC_PER_DAY; - secs += SEC_PER_HOUR * hour + SEC_PER_MIN * min + sec; - return secs; - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Converts a date-time structure to a timestamp. - /// - /// This function converts a given date and time to a timestamp, which is the number - /// of seconds since the Unix epoch (January 1, 1970). - /// - /// \tparam T The type of the date-time structure. - /// \param date_time The date-time structure. - /// \return Timestamp representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline ts_t dt_to_timestamp( - const T& date_time) { - return to_timestamp( - date_time.year, - date_time.mon, - date_time.day, - date_time.hour, - date_time.min, - date_time.sec); - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a std::tm structure to a timestamp. - /// - /// This function converts a standard C++ std::tm structure to a timestamp, which is the number - /// of seconds since the Unix epoch (January 1, 1970). - /// - /// \param timeinfo Pointer to a std::tm structure containing the date and time information. - /// \return Timestamp representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - TIME_SHIELD_CONSTEXPR inline ts_t tm_to_timestamp( - const std::tm *timeinfo) { - return to_timestamp( - timeinfo->tm_year + 1900, - timeinfo->tm_mon + 1, - timeinfo->tm_mday, - timeinfo->tm_hour, - timeinfo->tm_min, - timeinfo->tm_sec); - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a date and time to a timestamp in milliseconds. - /// - /// This function converts a given date and time to a timestamp in milliseconds, - /// which is the number of milliseconds since the Unix epoch (January 1, 1970). - /// - /// \tparam T1 The type of the year parameter (default is int64_t). - /// \tparam T2 The type of the other date and time parameters (default is int). - /// \param year The year value. - /// \param month The month value. - /// \param day The day value. - /// \param hour The hour value (default is 0). - /// \param min The minute value (default is 0). - /// \param sec The second value (default is 0). - /// \param ms The millisecond value (default is 0). - /// \return Timestamp in milliseconds representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline ts_ms_t to_timestamp_ms( - T1 year, - T2 month, - T2 day, - T2 hour = 0, - T2 min = 0, - T2 sec = 0, - T2 ms = 0) { - return sec_to_ms(to_timestamp(year, month, day, hour, min, sec)) + ms; - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Converts a date-time structure to a timestamp in milliseconds. - /// - /// This function converts a given date-time structure to a timestamp in milliseconds, - /// which is the number of milliseconds since the Unix epoch (January 1, 1970). - /// - /// \tparam T The type of the date-time structure. - /// \param date_time The date-time structure. - /// \return Timestamp in milliseconds representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline ts_t dt_to_timestamp_ms( - const T& date_time) { - return sec_to_ms(dt_to_timestamp(date_time)) + date_time.ms; - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Converts a std::tm structure to a timestamp in milliseconds. - /// - /// This function converts a given std::tm structure to a timestamp in milliseconds, - /// which is the number of milliseconds since the Unix epoch (January 1, 1970). - /// - /// \param timeinfo Pointer to a std::tm structure containing the date and time information. - /// \return Timestamp in milliseconds representing the given date and time. - TIME_SHIELD_CONSTEXPR inline ts_t tm_to_timestamp_ms( - const std::tm *timeinfo) { - return sec_to_ms(tm_to_timestamp(timeinfo)); - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a date and time to a floating-point timestamp. - /// - /// This function converts a given date and time to a floating-point timestamp, - /// which is the number of seconds (with fractional milliseconds) since the Unix epoch - /// (January 1, 1970). - /// - /// \tparam T1 The type of the year parameter (default is year_t). - /// \tparam T2 The type of the month, day, hour, minute, and second parameters (default is int). - /// \tparam T3 The type of the millisecond parameter (default is int). - /// \param year The year value. - /// \param month The month value. - /// \param day The day value. - /// \param hour The hour value (default is 0). - /// \param min The minute value (default is 0). - /// \param sec The second value (default is 0). - /// \param ms The millisecond value (default is 0). - /// \return Floating-point timestamp representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline fts_t to_ftimestamp( - T1 year, - T2 month, - T2 day, - T2 hour = 0, - T2 min = 0, - T2 sec = 0, - T3 ms = 0) { - return static_cast(to_timestamp(year, month, day, hour, min, sec)) + - static_cast(ms)/static_cast(MS_PER_SEC); - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Converts a date-time structure to a floating-point timestamp. - /// - /// This function converts a given date and time structure to a floating-point timestamp, - /// which is the number of seconds (with fractional milliseconds) since the Unix epoch - /// (January 1, 1970). - /// - /// \tparam T The type of the date-time structure. - /// \param date_time The date-time structure containing year, month, day, hour, minute, second, and millisecond fields. - /// \return Floating-point timestamp representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline fts_t dt_to_ftimestamp( - const T& date_time) { - return static_cast(to_timestamp(date_time)) + - static_cast(date_time.ms)/static_cast(MS_PER_SEC); - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a std::tm structure to a floating-point timestamp. - /// - /// This function converts a given std::tm structure to a floating-point timestamp, - /// which is the number of seconds (with fractional milliseconds) since the Unix epoch - /// (January 1, 1970). - /// - /// \param timeinfo Pointer to the std::tm structure containing the date and time. - /// \return Floating-point timestamp representing the given date and time. - /// \throws std::invalid_argument if the date-time combination is invalid. - TIME_SHIELD_CONSTEXPR inline fts_t tm_to_ftimestamp( - const std::tm* timeinfo) { - return static_cast(tm_to_timestamp(timeinfo)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get UNIX day. - /// - /// This function returns the number of days elapsed since the UNIX epoch. - /// - /// \tparam T The return type of the function (default is unixday_t). - /// \param ts Timestamp in seconds (default is current timestamp). - /// \return Number of days since the UNIX epoch. - template - constexpr T get_unix_day(ts_t ts = time_shield::ts()) noexcept { - return ts / SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the number of days between two timestamps. - /// - /// This function calculates the number of days between two timestamps. - /// - /// \tparam T The type of the return value, defaults to int. - /// \param start The timestamp of the start of the period. - /// \param stop The timestamp of the end of the period. - /// \return The number of days between start and stop. - template - constexpr T get_days_difference(ts_t start, ts_t stop) noexcept { - return (stop - start) / SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get UNIX day from milliseconds timestamp. - /// - /// This function returns the number of days elapsed since the UNIX epoch, given a timestamp in milliseconds. - /// - /// \tparam T The return type of the function (default is unixday_t). - /// \param t_ms Timestamp in milliseconds (default is current timestamp in milliseconds). - /// \return Number of days since the UNIX epoch. - template - constexpr T get_unix_day_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { - return get_unix_day(ms_to_sec(t_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a UNIX day to a timestamp in seconds. - /// - /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding - /// timestamp in seconds at the start of the specified day. - /// - /// \tparam T The return type of the function (default is ts_t). - /// \param unix_day Number of days since the UNIX epoch. - /// \return The timestamp in seconds representing the beginning of the specified UNIX day. - template - constexpr T unix_day_to_timestamp(uday_t unix_day) noexcept { - return unix_day * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a UNIX day to a timestamp in milliseconds. - /// - /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding timestamp - /// in milliseconds at the start of the specified day. - /// - /// \tparam T The return type of the function (default is ts_t). - /// \param unix_day Number of days since the UNIX epoch. - /// \return The timestamp in milliseconds representing the beginning of the specified UNIX day. - template - constexpr T unix_day_to_timestamp_ms(uday_t unix_day) noexcept { - return unix_day * MS_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Converts a UNIX day to a timestamp representing the end of the day in seconds. - /// - /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding - /// timestamp in seconds at the end of the specified day (23:59:59). - /// - /// \tparam T The return type of the function (default is ts_t). - /// \param unix_day The number of days since the UNIX epoch. - /// \return The timestamp in seconds representing the end of the specified UNIX day. - template - constexpr T end_of_day_from_unix_day(uday_t unix_day) noexcept { - return unix_day * SEC_PER_DAY + SEC_PER_DAY - 1; - } - - /// \brief Converts a UNIX day to a timestamp representing the end of the day in milliseconds. - /// - /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding - /// timestamp in milliseconds at the end of the specified day (23:59:59.999). - /// - /// \tparam T The return type of the function (default is ts_ms_t). - /// \param unix_day The number of days since the UNIX epoch. - /// \return The timestamp in milliseconds representing the end of the specified UNIX day. - template - constexpr T end_of_day_from_unix_day_ms(uday_t unix_day) noexcept { - return unix_day * MS_PER_DAY + MS_PER_DAY - 1; - } - - /// \brief Converts a UNIX day to a timestamp representing the start of the next day in seconds. - /// - /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding - /// timestamp in seconds at the start of the next day (00:00:00). - /// - /// \tparam T The return type of the function (default is ts_t). - /// \param unix_day The number of days since the UNIX epoch. - /// \return The timestamp in seconds representing the beginning of the next UNIX day. - template - constexpr T start_of_next_day_from_unix_day(uday_t unix_day) noexcept { - return unix_day * SEC_PER_DAY + SEC_PER_DAY; - } - - /// \brief Converts a UNIX day to a timestamp representing the start of the next day in milliseconds. - /// - /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding - /// timestamp in milliseconds at the start of the next day (00:00:00.000). - /// - /// \tparam T The return type of the function (default is ts_ms_t). - /// \param unix_day The number of days since the UNIX epoch. - /// \return The timestamp in milliseconds representing the beginning of the next UNIX day. - template - constexpr T start_of_next_day_from_unix_day_ms(uday_t unix_day) noexcept { - return unix_day * MS_PER_DAY + MS_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get UNIX minute. - /// - /// This function returns the number of minutes elapsed since the UNIX epoch. - /// - /// \tparam T The return type of the function (default is int64_t). - /// \param ts Timestamp in seconds (default is current timestamp). - /// \return Number of minutes since the UNIX epoch. - template - constexpr T get_unix_min(ts_t ts = time_shield::ts()) { - return ts / SEC_PER_MIN; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the second of the day. - /// - /// This function returns a value from 0 to MAX_SEC_PER_DAY representing the second of the day. - /// - /// \tparam T The return type of the function (default is int). - /// \param ts Timestamp in seconds (default is current timestamp). - /// \return Second of the day. - template - constexpr T sec_of_day(ts_t ts = time_shield::ts()) noexcept { - return ts % SEC_PER_DAY; - } - - /// \brief Get the second of the day from milliseconds timestamp. - /// - /// This function returns a value from 0 to MAX_SEC_PER_DAY representing the second of the day, given a timestamp in milliseconds. - /// - /// \tparam T The return type of the function (default is int). - /// \param ts_ms Timestamp in milliseconds. - /// \return Second of the day. - template - constexpr T sec_of_day_ms(ts_ms_t ts_ms) noexcept { - return sec_of_day(ms_to_sec(ts_ms)); - } - - /// \brief Get the second of the day. - /// - /// This function returns a value between 0 and MAX_SEC_PER_DAY representing the second of the day, given the hour, minute, and second. - /// - /// \tparam T1 The return type of the function (default is int). - /// \tparam T2 The type of the hour, minute, and second parameters (default is int). - /// \param hour Hour of the day. - /// \param min Minute of the hour. - /// \param sec Second of the minute. - /// \return Second of the day. - template - constexpr T1 sec_of_day( - T2 hour, - T2 min, - T2 sec) noexcept { - return hour * SEC_PER_HOUR + min * SEC_PER_MIN + sec; - } - - /// \brief Get the second of the minute. - /// - /// This function returns a value between 0 and 59 representing the second of the minute. - /// - /// \tparam T The return type of the function (default is int). - /// \param ts Timestamp in seconds (default is current timestamp). - /// \return Second of the minute. - template - constexpr T sec_of_min(ts_t ts = time_shield::ts()) { - return (ts % SEC_PER_MIN); - } - - /// \brief Get the second of the hour. - /// - /// This function returns a value between 0 and 3599 representing the second of the hour. - /// - /// \tparam T The return type of the function (default is int). - /// \param ts Timestamp in seconds (default is current timestamp). - /// \return Second of the hour. - template - constexpr T sec_of_hour(ts_t ts = time_shield::ts()) { - return (ts % SEC_PER_HOUR); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the year from the timestamp. - /// - /// This function returns the year of the specified timestamp in seconds since the Unix epoch. - /// - /// \tparam T The return type of the function (default is year_t). - /// \param ts Timestamp in seconds (default is current timestamp). - /// \return Year of the specified timestamp. - template - TIME_SHIELD_CONSTEXPR inline T get_year(ts_t ts = time_shield::ts()) { - return get_unix_year(ts) + UNIX_EPOCH; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the year from the timestamp in milliseconds. - /// - /// This function returns the year of the specified timestamp in milliseconds since the Unix epoch. - /// - /// \tparam T The return type of the function (default is year_t). - /// \param ts_ms Timestamp in milliseconds (default is current timestamp). - /// \return Year of the specified timestamp. - template - TIME_SHIELD_CONSTEXPR inline T get_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { - return get_year(ms_to_sec(ts_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the start of the year timestamp. - /// - /// This function resets the days, months, hours, minutes, and seconds of the given timestamp - /// to the beginning of the year. - /// - /// \param ts Timestamp. - /// \return Start of the year timestamp. - TIME_SHIELD_CONSTEXPR inline ts_t start_of_year(ts_t ts) noexcept { - constexpr ts_t BIAS_2100 = 4102444800; - if (ts < BIAS_2100) { - constexpr ts_t SEC_PER_YEAR_X2 = SEC_PER_YEAR * 2; - ts_t year_start_ts = ts % SEC_PER_4_YEARS; - if (year_start_ts < SEC_PER_YEAR) { - return ts - year_start_ts; - } else - if (year_start_ts < SEC_PER_YEAR_X2) { - return ts + SEC_PER_YEAR - year_start_ts; - } else - if (year_start_ts < (SEC_PER_YEAR_X2 + SEC_PER_LEAP_YEAR)) { - return ts + SEC_PER_YEAR_X2 - year_start_ts; - } - return ts + (SEC_PER_YEAR_X2 + SEC_PER_LEAP_YEAR) - year_start_ts; - } - - constexpr ts_t BIAS_2000 = 946684800; - ts_t secs = ts - BIAS_2000; - - ts_t offset_y400 = secs % SEC_PER_400_YEARS; - ts_t start_ts = secs - offset_y400 + BIAS_2000; - secs = offset_y400; - - if (secs >= SEC_PER_FIRST_100_YEARS) { - secs -= SEC_PER_FIRST_100_YEARS; - start_ts += SEC_PER_FIRST_100_YEARS; - while (secs >= SEC_PER_100_YEARS) { - secs -= SEC_PER_100_YEARS; - start_ts += SEC_PER_100_YEARS; - } - - constexpr ts_t SEC_PER_4_YEARS_V2 = 4 * SEC_PER_YEAR; - if (secs >= SEC_PER_4_YEARS_V2) { - secs -= SEC_PER_4_YEARS_V2; - start_ts += SEC_PER_4_YEARS_V2; - } else { - start_ts += secs - secs % SEC_PER_YEAR; - return start_ts; - } - } - - ts_t offset_4y = secs % SEC_PER_4_YEARS; - start_ts += secs - offset_4y; - secs = offset_4y; - - if (secs >= SEC_PER_LEAP_YEAR) { - secs -= SEC_PER_LEAP_YEAR; - start_ts += SEC_PER_LEAP_YEAR; - start_ts += secs - secs % SEC_PER_YEAR; - } - return start_ts; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the start of the year timestamp in milliseconds. - /// - /// This function resets the days, months, hours, minutes, and seconds of the given timestamp - /// to the beginning of the year. - /// - /// \param ts_ms Timestamp in milliseconds. - /// \return Start of year timestamp in milliseconds. - TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return sec_to_ms(start_of_year(ms_to_sec(ts_ms))); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the start of the year. - /// - /// This function returns the timestamp at the start of the specified year. - /// - /// \param year Year. - /// \return Timestamp of the start of the year. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline ts_t start_of_year_date(T year) { - if (year < 2100) { - const ts_t year_diff = year >= UNIX_EPOCH ? year - UNIX_EPOCH : UNIX_EPOCH - year; - const ts_t year_start_ts = (year_diff / 4) * SEC_PER_4_YEARS; - const ts_t year_remainder = year_diff % 4; - constexpr ts_t SEC_PER_YEAR_X2 = 2 * SEC_PER_YEAR; - constexpr ts_t SEC_PER_YEAR_V2 = SEC_PER_YEAR_X2 + SEC_PER_LEAP_YEAR; - switch (year_remainder) { - case 0: return year_start_ts; - case 1: return year_start_ts + SEC_PER_YEAR; - case 2: return year_start_ts + SEC_PER_YEAR_X2; - default: break; - }; - return year_start_ts + SEC_PER_YEAR_V2; - } - return to_timestamp(year, 1, 1); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp in milliseconds of the start of the year. - /// - /// This function returns the timestamp at the start of the specified year in milliseconds. - /// - /// \param year Year. - /// \return Timestamp of the start of the year in milliseconds. - /// \throws std::invalid_argument if the date-time combination is invalid. - template - TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_year_date_ms(T year) { - return sec_to_ms(start_of_year_date(year)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the end-of-year timestamp. - /// - /// This function finds the last timestamp of the current year. - /// - /// \param ts Timestamp. - /// \return End-of-year timestamp. - TIME_SHIELD_CONSTEXPR inline ts_t end_of_year(ts_t ts = time_shield::ts()) { - constexpr ts_t BIAS_2100 = 4102444800; - if (ts < BIAS_2100) { - constexpr ts_t SEC_PER_YEAR_X2 = SEC_PER_YEAR * 2; - constexpr ts_t SEC_PER_YEAR_X3 = SEC_PER_YEAR * 3; - constexpr ts_t SEC_PER_YEAR_X3_V2 = SEC_PER_YEAR_X2 + SEC_PER_LEAP_YEAR; - ts_t year_end_ts = ts % SEC_PER_4_YEARS; - if (year_end_ts < SEC_PER_YEAR) { - return ts + SEC_PER_YEAR - year_end_ts - 1; - } else - if (year_end_ts < SEC_PER_YEAR_X2) { - return ts + SEC_PER_YEAR_X2 - year_end_ts - 1; - } else - if (year_end_ts < SEC_PER_YEAR_X3_V2) { - return ts + SEC_PER_YEAR_X3_V2 - year_end_ts - 1; - } - return ts + (SEC_PER_YEAR_X3 + SEC_PER_LEAP_YEAR) - year_end_ts - 1; - } - - constexpr ts_t BIAS_2000 = 946684800; - ts_t secs = ts - BIAS_2000; - - ts_t offset_y400 = secs % SEC_PER_400_YEARS; - ts_t end_ts = secs - offset_y400 + BIAS_2000; - secs = offset_y400; - - if (secs >= SEC_PER_FIRST_100_YEARS) { - secs -= SEC_PER_FIRST_100_YEARS; - end_ts += SEC_PER_FIRST_100_YEARS; - while (secs >= SEC_PER_100_YEARS) { - secs -= SEC_PER_100_YEARS; - end_ts += SEC_PER_100_YEARS; - } - - constexpr ts_t SEC_PER_4_YEARS_V2 = 4 * SEC_PER_YEAR; - if (secs >= SEC_PER_4_YEARS_V2) { - secs -= SEC_PER_4_YEARS_V2; - end_ts += SEC_PER_4_YEARS_V2; - } else { - end_ts += secs - secs % SEC_PER_YEAR; - return end_ts + SEC_PER_YEAR - 1; - } - } - - ts_t offset_4y = secs % SEC_PER_4_YEARS; - end_ts += secs - offset_4y; - secs = offset_4y; - - if (secs >= SEC_PER_LEAP_YEAR) { - secs -= SEC_PER_LEAP_YEAR; - end_ts += SEC_PER_LEAP_YEAR; - end_ts += secs - secs % SEC_PER_YEAR; - end_ts += SEC_PER_YEAR; - } else { - end_ts += SEC_PER_LEAP_YEAR; - } - return end_ts - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp in milliseconds of the end of the year. - /// - /// This function finds the last timestamp of the current year in milliseconds. - /// - /// \param ts_ms Timestamp in milliseconds. - /// \return End-of-year timestamp in milliseconds. - template - TIME_SHIELD_CONSTEXPR inline ts_ms_t end_of_year_ms(ts_ms_t ts_ms = time_shield::ts_ms()) { - return sec_to_ms(end_of_year(ms_to_sec(ts_ms))); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the day of the year. - /// - /// This function returns the day of the year for the specified timestamp. - /// - /// \param ts Timestamp. - /// \return Day of the year. - template - inline T day_of_year(ts_t ts = time_shield::ts()) { - return static_cast(((ts - start_of_year(ts)) / SEC_PER_DAY) + 1); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the month of the year. - /// - /// This function returns the month of the year for the specified timestamp. - /// - /// \param ts Timestamp. - /// \return Month of the year. - template - TIME_SHIELD_CONSTEXPR inline T month_of_year(ts_t ts) noexcept { - constexpr int JAN_AND_FEB_DAY_LEAP_YEAR = 60; - constexpr int TABLE_MONTH_OF_YEAR[] = { - 0, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 31 январь - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 28 февраль - 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 31 март - 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, // 30 апрель - 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, - 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, - 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, - 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11, - 12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, - }; - const size_t dy = day_of_year(ts); - return static_cast((is_leap_year(ts) && dy >= JAN_AND_FEB_DAY_LEAP_YEAR) ? TABLE_MONTH_OF_YEAR[dy - 1] : TABLE_MONTH_OF_YEAR[dy]); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the day of the month. - /// - /// This function returns the day of the month for the specified timestamp. - /// - /// \param ts Timestamp. - /// \return Day of the month. - template - TIME_SHIELD_CONSTEXPR inline T day_of_month(ts_t ts = time_shield::ts()) { - constexpr int JAN_AND_FEB_DAY_LEAP_YEAR = 60; - // таблица для обычного года, не високосного - constexpr int TABLE_DAY_OF_YEAR[] = { - 0, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 январь - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28, // 28 февраль - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, // 31 март - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, // 30 апрель - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, - }; - const size_t dy = day_of_year(ts); - if(is_leap_year(ts)) { - if(dy == JAN_AND_FEB_DAY_LEAP_YEAR) return TABLE_DAY_OF_YEAR[dy - 1] + 1; - if(dy > JAN_AND_FEB_DAY_LEAP_YEAR) return TABLE_DAY_OF_YEAR[dy - 1]; - } - return TABLE_DAY_OF_YEAR[dy]; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the number of days in a month. - /// - /// This function calculates and returns the number of days in the specified month and year. - /// - /// \param year Year as an integer. - /// \param month Month as an integer. - /// \return The number of days in the given month and year. - template - constexpr T1 num_days_in_month(T2 year, T3 month) noexcept { - if (month > MONTHS_PER_YEAR || month < 0) return 0; - constexpr T1 num_days[13] = {0,31,30,31,30,31,30,31,31,30,31,30,31}; - if (month == FEB) { - if (is_leap_year_date(year)) return 29; - return 28; - } - return num_days[month]; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the number of days in the month of the given timestamp. - /// - /// This function calculates and returns the number of days in the month of the specified timestamp. - /// - /// \param ts The timestamp to extract month and year from. - /// \return The number of days in the month of the given timestamp. - template - TIME_SHIELD_CONSTEXPR T1 num_days_in_month_ts(ts_t ts = time_shield::ts()) noexcept { - constexpr T1 num_days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; - const int month = month_of_year(ts); - if (month == FEB) { - return is_leap_year(ts) ? 29 : 28; - } - return num_days[month]; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the number of days in a given year. - /// - /// This function calculates and returns the number of days in the specified year. - /// - /// \param year Year. - /// \return Number of days in the given year. - template - constexpr T1 num_days_in_year(T2 year) noexcept { - if (is_leap_year_date(year)) return DAYS_PER_LEAP_YEAR; - return DAYS_PER_YEAR; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the number of days in the current year. - /// - /// This function calculates and returns the number of days in the current year based on the provided timestamp. - /// - /// \param ts Timestamp. - /// \return Number of days in the current year. - template - constexpr T num_days_in_year_ts(ts_t ts = time_shield::ts()) { - if (is_leap_year_ts(ts)) return DAYS_PER_LEAP_YEAR; - return DAYS_PER_YEAR; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the start of the day timestamp. - /// - /// This function returns the timestamp at the start of the day. - /// The function sets the hours, minutes, and seconds to zero. - /// - /// \param ts Timestamp. - /// \return Start of the day timestamp. - constexpr ts_t start_of_day(ts_t ts = time_shield::ts()) noexcept { - return ts - (ts % SEC_PER_DAY); - } - -//------------------------------------------------------------------------------ - - /// \brief Get timestamp of the start of the previous day. - /// - /// This function returns the timestamp at the start of the previous day. - /// - /// \param ts Timestamp of the current day. - /// \param days Number of days to go back (default is 1). - /// \return Timestamp of the start of the previous day. - template - constexpr ts_t start_of_prev_day(ts_t ts = time_shield::ts(), T days = 1) noexcept { - return ts - (ts % SEC_PER_DAY) - SEC_PER_DAY * days; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the start of the day timestamp in seconds. - /// - /// This function returns the timestamp at the start of the day in seconds. - /// The function sets the hours, minutes, and seconds to zero. - /// - /// \param ts_ms Timestamp in milliseconds. - /// \return Start of the day timestamp in seconds. - constexpr ts_t start_of_day_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return start_of_day(ms_to_sec(ts_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the start of the day timestamp in milliseconds. - /// - /// This function returns the timestamp at the start of the day in milliseconds. - /// The function sets the hours, minutes, seconds, and milliseconds to zero. - /// - /// \param ts_ms Timestamp in milliseconds. - /// \return Start of the day timestamp in milliseconds. - constexpr ts_ms_t start_of_day_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return ts_ms - (ts_ms % MS_PER_DAY); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the start of the day after a specified number of days. - /// - /// Calculates the timestamp for the beginning of the day after a specified number of days - /// relative to the given timestamp. - /// - /// \param ts The current timestamp in seconds. - /// \param days The number of days after the current day (default is 1). - /// \return The timestamp in seconds representing the beginning of the specified future day. - template - constexpr ts_t start_of_next_day(ts_t ts, T days = 1) noexcept { - return start_of_day(ts) + days * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the start of the day after a specified number of days. - /// - /// Calculates the timestamp for the beginning of the day after a specified number of days - /// relative to the given timestamp in milliseconds. - /// - /// \param ts_ms The current timestamp in milliseconds. - /// \param days The number of days after the current day (default is 1). - /// \return The timestamp in milliseconds representing the beginning of the specified future day. - template - constexpr ts_ms_t start_of_next_day_ms(ts_ms_t ts_ms, T days = 1) noexcept { - return start_of_day_ms(ts_ms) + days * MS_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Calculate the timestamp for a specified number of days in the future. - /// - /// Adds the given number of days to the provided timestamp, without adjusting to the start of the day. - /// - /// \param ts The current timestamp in seconds. - /// \param days The number of days to add to the current timestamp (default is 1). - /// \return The timestamp in seconds after adding the specified number of days. - template - constexpr ts_t next_day(ts_t ts, T days = 1) noexcept { - return ts + days * SEC_PER_DAY; - } - - /// \brief Calculate the timestamp for a specified number of days in the future (milliseconds). - /// - /// Adds the given number of days to the provided timestamp, without adjusting to the start of the day. - /// - /// \param ts_ms The current timestamp in milliseconds. - /// \param days The number of days to add to the current timestamp (default is 1). - /// \return The timestamp in milliseconds after adding the specified number of days. - template - constexpr ts_ms_t next_day_ms(ts_ms_t ts_ms, T days = 1) noexcept { - return ts_ms + days * MS_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the end of the day. - /// - /// This function sets the hour to 23, minute to 59, and second to 59. - /// - /// \param ts Timestamp. - /// \return Timestamp at the end of the day. - constexpr ts_t end_of_day(ts_t ts = time_shield::ts()) noexcept { - return ts - (ts % SEC_PER_DAY) + SEC_PER_DAY - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the end of the day in seconds. - /// - /// This function sets the hour to 23, minute to 59, and second to 59. - /// - /// \param ts_ms Timestamp in milliseconds. - /// \return Timestamp at the end of the day in seconds. - constexpr ts_t end_of_day_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return end_of_day(ms_to_sec(ts_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the end of the day in milliseconds. - /// - /// This function sets the hour to 23, minute to 59, second to 59, and millisecond to 999. - /// - /// \param ts_ms Timestamp in milliseconds. - /// \return Timestamp at the end of the day in milliseconds. - constexpr ts_ms_t end_of_day_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return ts_ms - (ts_ms % MS_PER_DAY) + MS_PER_DAY - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the day of the week. - /// \tparam T1 Return type (default: Weekday). - /// \tparam T2 Year type. - /// \tparam T3 Month type. - /// \tparam T4 Day type. - /// \param year Year. - /// \param month Month. - /// \param day Day. - /// \return Day of the week (SUN = 0, MON = 1, ... SAT = 6). - template - constexpr T1 day_of_week_date(T2 year, T3 month, T4 day) { - year_t a, y, m, R; - a = (14 - month) / MONTHS_PER_YEAR; - y = year - a; - m = month + MONTHS_PER_YEAR * a - 2; - R = 7000 + ( day + y + (y / 4) - (y / 100) + (y / 400) + (31 * m) / MONTHS_PER_YEAR); - return static_cast(R % DAYS_PER_WEEK); - } - -//------------------------------------------------------------------------------ - - /// \ingroup time_structures - /// \brief Get the day of the week from a date structure. - /// - /// This function takes a date structure with fields 'year', 'mon', and 'day', - /// and returns the day of the week (SUN = 0, MON = 1, ... SAT = 6). - /// - /// \param date Structure containing year, month, and day. - /// \return Day of the week (SUN = 0, MON = 1, ... SAT = 6). - template - constexpr T1 get_weekday_from_date(const T2& date) { - return day_of_week_date(date.year, date.mon, date.day); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the weekday from a timestamp. - /// \param ts Timestamp. - /// \return Weekday (SUN = 0, MON = 1, ... SAT = 6). - template - constexpr T get_weekday_from_ts(ts_t ts) noexcept { - return static_cast((ts / SEC_PER_DAY + THU) % DAYS_PER_WEEK); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the weekday from a timestamp in milliseconds. - /// \param ts_ms Timestamp in milliseconds. - /// \return Weekday (SUN = 0, MON = 1, ... SAT = 6). - template - constexpr T get_weekday_from_ts_ms(ts_ms_t ts_ms) { - return get_weekday_from_ts(ms_to_sec(ts_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the start of the current month. - /// - /// This function returns the timestamp at the start of the current month, - /// setting the day to the first day of the month and the time to 00:00:00. - /// - /// \param ts Timestamp (default is current timestamp) - /// \return Timestamp at the start of the current month - TIME_SHIELD_CONSTEXPR inline ts_t start_of_month(ts_t ts = time_shield::ts()) { - return start_of_day(ts) - (day_of_month(ts) - 1) * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the last timestamp of the current month. - /// - /// This function returns the last timestamp of the current month, - /// setting the day to the last day of the month and the time to 23:59:59. - /// - /// \param ts Timestamp (default is current timestamp) - /// \return Last timestamp of the current month - TIME_SHIELD_CONSTEXPR inline ts_t end_of_month(ts_t ts = time_shield::ts()) { - return end_of_day(ts) + (num_days_in_month_ts(ts) - day_of_month(ts)) * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the last Sunday of the current month. - /// - /// This function returns the timestamp of the last Sunday of the current month, - /// setting the day to the last Sunday and the time to 00:00:00. - /// - /// \param ts Timestamp (default is current timestamp) - /// \return Timestamp of the last Sunday of the current month - TIME_SHIELD_CONSTEXPR inline ts_t last_sunday_of_month(ts_t ts = time_shield::ts()) { - return end_of_month(ts) - get_weekday_from_ts(ts) * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the day of the last Sunday of the given month and year. - /// - /// This function returns the day of the last Sunday of the specified month and year. - /// - /// \param year Year - /// \param month Month (1 = January, 12 = December) - /// \return Day of the last Sunday of the given month and year - template - TIME_SHIELD_CONSTEXPR inline T1 last_sunday_month_day(T2 year, T3 month) { - const T1 days = num_days_in_month(year, month); - return days - day_of_week_date(year, month, days); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the start of the hour. - /// - /// This function sets the minute and second to zero. - /// - /// \param ts Timestamp (default: current timestamp). - /// \return Timestamp at the start of the hour. - constexpr ts_t start_of_hour(ts_t ts = time_shield::ts()) noexcept { - return ts - (ts % SEC_PER_HOUR); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the start of the hour. - /// - /// This function sets the minute and second to zero. - /// - /// \param ts_ms Timestamp in milliseconds (default: current timestamp in milliseconds). - /// \return Timestamp at the start of the hour in seconds. - constexpr ts_t start_of_hour_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return start_of_hour(ms_to_sec(ts_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the start of the hour. - /// This function sets the minute and second to zero. - /// \param ts_ms Timestamp in milliseconds (default: current timestamp in milliseconds). - /// \return Timestamp at the start of the hour in milliseconds. - constexpr ts_ms_t start_of_hour_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return ts_ms - (ts_ms % MS_PER_HOUR); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the end of the hour. - /// This function sets the minute and second to 59. - /// \param ts Timestamp (default: current timestamp). - /// \return Timestamp at the end of the hour. - constexpr ts_t end_of_hour(ts_t ts = time_shield::ts()) noexcept { - return ts - (ts % SEC_PER_HOUR) + SEC_PER_HOUR - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the end of the hour. - /// - /// This function sets the minute and second to 59. - /// - /// \param ts_ms Timestamp in milliseconds (default: current timestamp in milliseconds). - /// \return Timestamp at the end of the hour in seconds. - constexpr ts_t end_of_hour_sec(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return end_of_hour(ms_to_sec(ts_ms)); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp at the end of the hour. - /// - /// This function sets the minute and second to 59. - /// - /// \param ts_ms Timestamp in milliseconds (default: current timestamp in milliseconds). - /// \return Timestamp at the end of the hour in milliseconds. - constexpr ts_ms_t end_of_hour_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { - return ts_ms - (ts_ms % MS_PER_HOUR) + MS_PER_HOUR - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the hour of the day. - /// - /// This function returns a value between 0 to 23 representing the hour of the day. - /// - /// \param ts Timestamp (default: current timestamp). - /// \return Hour of the day. - template - constexpr T hour_of_day(ts_t ts = time_shield::ts()) noexcept { - return ((ts / SEC_PER_HOUR) % HOURS_PER_DAY); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the beginning of the week. - /// - /// This function finds the timestamp of the beginning of the week, - /// which corresponds to the start of Sunday. - /// - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the beginning of the week. - constexpr ts_t start_of_week(ts_t ts = time_shield::ts()) { - return start_of_day(ts) - get_weekday_from_ts(ts) * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the end of the week. - /// - /// This function finds the timestamp of the end of the week, - /// which corresponds to the end of Saturday. - /// - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the end of the week. - constexpr ts_t end_of_week(ts_t ts = time_shield::ts()) { - return start_of_day(ts) + (DAYS_PER_WEEK - get_weekday_from_ts(ts)) * SEC_PER_DAY - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the start of Saturday. - /// - /// This function finds the timestamp of the beginning of the day on Saturday, - /// which corresponds to the start of Saturday. - /// - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the start of Saturday. - constexpr ts_t start_of_saturday(ts_t ts = time_shield::ts()) { - return start_of_day(ts) + (SAT - get_weekday_from_ts(ts)) * SEC_PER_DAY; - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the beginning of the minute. - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the beginning of the minute. - constexpr ts_t start_of_min(ts_t ts = time_shield::ts()) noexcept { - return ts - (ts % SEC_PER_MIN); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the end of the minute. - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the end of the minute. - constexpr ts_t end_of_min(ts_t ts = time_shield::ts()) noexcept { - return ts - (ts % SEC_PER_MIN) + SEC_PER_MIN - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Get minute of day. - /// This function returns a value between 0 to 1439 (minute of day). - /// \param ts Timestamp in seconds (default: current timestamp). - /// \return Minute of day. - template - constexpr T min_of_day(ts_t ts = time_shield::ts()) noexcept { - return ((ts / SEC_PER_MIN) % MIN_PER_DAY); - } - -//------------------------------------------------------------------------------ - - /// \brief Get minute of hour. - /// This function returns a value between 0 to 59. - /// \param ts Timestamp in seconds (default: current timestamp). - /// \return Minute of hour. - template - constexpr T min_of_hour(ts_t ts = time_shield::ts()) noexcept { - return ((ts / SEC_PER_MIN) % MIN_PER_HOUR); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the start of the period. - /// \param p Period duration in seconds. - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the start of the period. - template - constexpr ts_t start_of_period(T p, ts_t ts = time_shield::ts()) { - return ts - (ts % p); - } - -//------------------------------------------------------------------------------ - - /// \brief Get the timestamp of the end of the period. - /// \param p Period duration in seconds. - /// \param ts Timestamp (default: current timestamp). - /// \return Returns the timestamp of the end of the period. - template - constexpr ts_t end_of_period(T p, ts_t ts = time_shield::ts()) { - return ts - (ts % p) + p - 1; - } - -//------------------------------------------------------------------------------ - - /// \brief Converts an integer to a time zone structure. - /// \tparam T The type of the time zone structure (default is TimeZoneStruct). - /// \param offset The integer to convert. - /// \return A time zone structure of type T represented by the given integer. - /// \details The function assumes that the type T has members `hour`, `min`, and `is_positive`. - template - inline T to_time_zone(tz_t offset) { - T tz; - int abs_val = std::abs(offset); - tz.hour = abs_val / SEC_PER_HOUR; - tz.min = (abs_val % SEC_PER_HOUR) / SEC_PER_MIN; - tz.is_positive = (offset >= 0); - return tz; - } - -/// \} +#include "time_zone_struct.hpp" -}; // namespace time_shield +#include "time_unit_conversions.hpp" +#include "unix_time_conversions.hpp" +#include "date_conversions.hpp" +#include "date_time_conversions.hpp" +#include "time_zone_offset_conversions.hpp" +#include "workday_conversions.hpp" +#include "ole_automation_conversions.hpp" +#include "astronomy_conversions.hpp" #include "time_conversion_aliases.hpp" #endif // _TIME_SHIELD_TIME_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/time_formatting.hpp b/include/time_shield/time_formatting.hpp index b86a04b9..8317646f 100644 --- a/include/time_shield/time_formatting.hpp +++ b/include/time_shield/time_formatting.hpp @@ -10,10 +10,13 @@ /// It provides utilities for custom formatting based on user-defined patterns /// and for standard date-time string representations. +#include "config.hpp" #include "date_time_struct.hpp" #include "time_zone_struct.hpp" #include "time_conversions.hpp" +#include + namespace time_shield { /// \ingroup time_formatting @@ -56,8 +59,10 @@ namespace time_shield { result += std::string(buffer); break; } - // %h: Equivalent to %b + + // fallthrough case 'b': + // %h: Equivalent to %b if (repeat_count > 1) break; result += to_str(static_cast(dt.mon), FormatType::SHORT_NAME); break; @@ -125,13 +130,13 @@ namespace time_shield { if (repeat_count == 1) { // %Y-%m-%d ISO 8601 date format char buffer[32] = {0}; - if (dt.year <= 9999 || dt.year >= 0) { + if (dt.year <= 9999 && dt.year >= 0) { snprintf(buffer, sizeof(buffer), "%.4d-%.2d-%.2d", (int)dt.year, dt.mon, dt.day); } else if (dt.year < 0) { - snprintf(buffer, sizeof(buffer), "-%lld-%.2d-%.2d", dt.year, dt.mon, dt.day); + snprintf(buffer, sizeof(buffer), "-%" PRId64 "-%.2d-%.2d", dt.year, dt.mon, dt.day); } else { - snprintf(buffer, sizeof(buffer), "+%lld-%.2d-%.2d", dt.year, dt.mon, dt.day); + snprintf(buffer, sizeof(buffer), "+%" PRId64 "-%.2d-%.2d", dt.year, dt.mon, dt.day); } result += std::string(buffer); } @@ -228,8 +233,10 @@ namespace time_shield { if (repeat_count == 3) { result += std::to_string(dt.ms); break; - } + } // to '%ss' + + // fallthrough case 'S': if (repeat_count <= 2) { char buffer[4] = {0}; @@ -304,15 +311,31 @@ namespace time_shield { const int64_t centuries = dt.year - mega_years * 1000000 - millennia * 1000; if (mega_years) { if (millennia) { - snprintf(buffer, sizeof(buffer), "%lldM%lldK%.3lld", mega_years, std::abs(millennia), std::abs(centuries)); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "M%" PRId64 "K%.3" PRId64, + mega_years, + static_cast(std::abs(millennia)), + static_cast(std::abs(centuries))); } else { - snprintf(buffer, sizeof(buffer), "%lldM%.3lld", mega_years, std::abs(centuries)); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "M%.3" PRId64, + mega_years, + static_cast(std::abs(centuries))); } } else if (millennia) { - snprintf(buffer, sizeof(buffer), "%lldK%.3lld", millennia, std::abs(centuries)); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "K%.3" PRId64, + millennia, + static_cast(std::abs(centuries))); } else { - snprintf(buffer, sizeof(buffer), "%.4lld", dt.year); + snprintf(buffer, sizeof(buffer), "%.4" PRId64, dt.year); } result += std::string(buffer); } else @@ -527,9 +550,28 @@ namespace time_shield { DateTimeStruct dt = to_date_time(ts); char buffer[32] = {0}; if TIME_SHIELD_IF_CONSTEXPR (std::is_floating_point::value) { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms); } else { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec); } return std::string(buffer); } @@ -545,7 +587,13 @@ namespace time_shield { inline const std::string to_iso8601_date(T ts) { DateTimeStruct dt = to_date_time(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2d", dt.year, dt.mon, dt.day); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2d", + dt.year, + dt.mon, + dt.day); return std::string(buffer); } @@ -599,9 +647,28 @@ namespace time_shield { DateTimeStruct dt = to_date_time(ts); char buffer[32] = {0}; if TIME_SHIELD_IF_CONSTEXPR (std::is_floating_point::value) { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3dZ", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3dZ", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms); } else { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2dZ", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2dZ", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec); } return std::string(buffer); } @@ -615,7 +682,17 @@ namespace time_shield { inline const std::string to_iso8601_utc_ms(ts_ms_t ts_ms) { DateTimeStruct dt = to_date_time_ms(ts_ms); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3dZ", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3dZ", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms); return std::string(buffer); } @@ -628,7 +705,17 @@ namespace time_shield { inline const std::string to_iso8601_ms(ts_ms_t ts_ms) { DateTimeStruct dt = to_date_time_ms(ts_ms); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms); return std::string(buffer); } @@ -647,15 +734,61 @@ namespace time_shield { char buffer[32] = {0}; if TIME_SHIELD_IF_CONSTEXPR (std::is_floating_point::value) { if (tz.is_positive) { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d+%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms, tz.hour, tz.min); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d+%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms, + tz.hour, + tz.min); } else { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d-%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms, tz.hour, tz.min); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d-%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms, + tz.hour, + tz.min); } } else { if (tz.is_positive) { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d+%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, tz.hour, tz.min); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d+%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + tz.hour, + tz.min); } else { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d-%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, tz.hour, tz.min); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d-%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + tz.hour, + tz.min); } } return std::string(buffer); @@ -673,9 +806,33 @@ namespace time_shield { DateTimeStruct dt = to_date_time_ms(ts_ms); char buffer[32] = {0}; if (tz.is_positive) { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d+%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms, tz.hour, tz.min); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d+%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms, + tz.hour, + tz.min); } else { - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d-%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms, tz.hour, tz.min); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2dT%.2d:%.2d:%.2d.%.3d-%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms, + tz.hour, + tz.min); } return std::string(buffer); } @@ -689,7 +846,16 @@ namespace time_shield { inline const std::string to_mql5_date_time(ts_t ts) { DateTimeStruct dt = to_date_time(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld.%.2d.%.2d %.2d:%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 ".%.2d.%.2d %.2d:%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec); return std::string(buffer); } @@ -708,7 +874,13 @@ namespace time_shield { inline const std::string to_mql5_date(ts_t ts) { DateTimeStruct dt = to_date_time(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld.%.2d.%.2d", dt.year, dt.mon, dt.day); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 ".%.2d.%.2d", + dt.year, + dt.mon, + dt.day); return std::string(buffer); } @@ -731,7 +903,16 @@ namespace time_shield { inline const std::string to_windows_filename(ts_t ts) { DateTimeStruct dt = to_date_time(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2d_%.2d-%.2d-%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2d_%.2d-%.2d-%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec); return std::string(buffer); } @@ -741,7 +922,17 @@ namespace time_shield { inline const std::string to_windows_filename_ms(ts_ms_t ts) { DateTimeStruct dt = to_date_time_ms(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2d_%.2d-%.2d-%.2d-%.3d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2d_%.2d-%.2d-%.2d-%.3d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms); return std::string(buffer); } @@ -751,7 +942,16 @@ namespace time_shield { inline std::string to_human_readable(ts_t ts) { DateTimeStruct dt = to_date_time_ms(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2d %.2d:%.2d:%.2d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2d %.2d:%.2d:%.2d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec); return std::string(buffer); } @@ -761,7 +961,17 @@ namespace time_shield { inline std::string to_human_readable_ms(ts_ms_t ts) { DateTimeStruct dt = to_date_time_ms(ts); char buffer[32] = {0}; - snprintf(buffer, sizeof(buffer), "%lld-%.2d-%.2d %.2d:%.2d:%.2d.%.3d", dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec, dt.ms); + snprintf( + buffer, + sizeof(buffer), + "%" PRId64 "-%.2d-%.2d %.2d:%.2d:%.2d.%.3d", + dt.year, + dt.mon, + dt.day, + dt.hour, + dt.min, + dt.sec, + dt.ms); return std::string(buffer); } diff --git a/include/time_shield/time_parser.hpp b/include/time_shield/time_parser.hpp index f8e03cf4..bb325ad6 100644 --- a/include/time_shield/time_parser.hpp +++ b/include/time_shield/time_parser.hpp @@ -8,6 +8,13 @@ /// /// This file contains functions for parsing ISO8601 date and time strings, extracting month numbers from month names, /// and converting parsed date and time information to different timestamp formats. +/// +/// Provides: +/// - Month name parsing (e.g. "Jan", "January") to month index (1..12). +/// - ISO8601 date/time parsing into DateTimeStruct + TimeZoneStruct. +/// - Convenience functions to convert ISO8601 strings to timestamps (sec/ms/float). +/// +/// \note If you need strict error handling, prefer the `str_to_*` functions that return bool. #include "enums.hpp" #include "constants.hpp" @@ -15,13 +22,18 @@ #include "time_zone_struct.hpp" #include "validation.hpp" #include "time_conversions.hpp" -#include +#include "iso_week_conversions.hpp" + #include #include #include #include -#include #include +#include + +#if __cplusplus >= 201703L +# include +#endif namespace time_shield { @@ -56,224 +68,1018 @@ namespace time_shield { /// \endcode /// /// \{ + + namespace detail { - /// \brief Get the month number by name. - /// \tparam T The return type, default is Month enum. - /// \param month The name of the month as a string. - /// \return The month number corresponding to the given name. - /// \throw std::invalid_argument if the month name is invalid. - template - T get_month_number(const std::string& month) { - if (month.empty()) throw std::invalid_argument("Invalid month name"); - - std::string month_copy = month; - std::transform(month_copy.begin(), month_copy.end(), month_copy.begin(), [](char &ch) { - return std::use_facet>(std::locale()).tolower(ch); - }); - month_copy[0] = static_cast(std::toupper(month_copy[0])); - - static const std::array short_names = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - }; - static const std::array full_names = { - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - }; - - for(int i = 0; i < MONTHS_PER_YEAR; ++i) { - if (month == short_names[i] || month == full_names[i]) { - return static_cast(i + 1); + /// \brief Trim ASCII whitespace from both ends. + inline std::string trim_copy_ascii(const std::string& s) { + size_t b = 0; + size_t e = s.size(); + while (b < e && std::isspace(static_cast(s[b])) != 0) ++b; + while (e > b && std::isspace(static_cast(s[e - 1])) != 0) --e; + return s.substr(b, e - b); + } + +# if __cplusplus >= 201703L + /// \brief Trim ASCII whitespace from both ends (string_view). + inline std::string_view trim_view_ascii(std::string_view v) { + size_t b = 0; + size_t e = v.size(); + while (b < e && std::isspace(static_cast(v[b])) != 0) ++b; + while (e > b && std::isspace(static_cast(v[e - 1])) != 0) --e; + return v.substr(b, e - b); + } +# endif + + /// \brief Normalize month token to lower-case ASCII using current locale facet. + /// \param month Input token. + /// \param output Output lower-case token (overwritten). + inline void normalise_month_token_lower(const std::string& month, std::string& output) { + output = trim_copy_ascii(month); + if (output.empty()) return; + + const auto& facet = std::use_facet>(std::locale()); + std::transform(output.begin(), output.end(), output.begin(), + [&facet](char ch) { return facet.tolower(ch); }); + } + +# if __cplusplus >= 201703L + /// \brief Normalize month token to lower-case ASCII using current locale facet (string_view). + /// \param month Input token view. + /// \param output Output lower-case token (overwritten). + inline void normalise_month_token_lower(std::string_view month, std::string& output) { + month = trim_view_ascii(month); + output.assign(month.begin(), month.end()); + if (output.empty()) return; + + const auto& facet = std::use_facet>(std::locale()); + std::transform(output.begin(), output.end(), output.begin(), + [&facet](char ch) { return facet.tolower(ch); }); + } +# endif + + /// \brief Try parse month name token into month index (1..12). + /// \param month Month token (e.g. "Jan", "January", case-insensitive). + /// \param value Output month index in range [1..12]. + /// \return True if token matches a supported month name, false otherwise. + inline bool try_parse_month_index(const std::string& month, int& value) { + if (month.empty()) return false; + + std::string month_copy; + normalise_month_token_lower(month, month_copy); + if (month_copy.empty()) return false; + + static const std::array short_names = { + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec" + }; + static const std::array full_names = { + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december" + }; + + for (std::size_t i = 0; i < short_names.size(); ++i) { + if (month_copy == short_names[i] || month_copy == full_names[i]) { + value = static_cast(i) + 1; + return true; + } + } + + return false; + } + +# if __cplusplus >= 201703L + /// \brief Try parse month name token into month index (1..12), string_view overload. + /// \param month Month token view (e.g. "Jan", "January", case-insensitive). + /// \param value Output month index in range [1..12]. + /// \return True if token matches a supported month name, false otherwise. + inline bool try_parse_month_index(std::string_view month, int& value) { + if (month.empty()) return false; + + std::string month_copy; + normalise_month_token_lower(month, month_copy); + if (month_copy.empty()) return false; + + static const std::array short_names = { + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec" + }; + static const std::array full_names = { + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december" + }; + + for (std::size_t i = 0; i < short_names.size(); ++i) { + if (month_copy == short_names[i] || month_copy == full_names[i]) { + value = static_cast(i) + 1; + return true; + } + } + + return false; + } +# endif + + /// \brief Parse month name token into month index (1..12). + /// \param month Month token. + /// \return Month index [1..12]. + /// \throw std::invalid_argument if token is invalid. + inline int parse_month_index(const std::string& month) { + int value = 0; + if (!try_parse_month_index(month, value)) { + throw std::invalid_argument("Invalid month name"); + } + return value; + } + +# if __cplusplus >= 201703L + /// \brief Parse month name token into month index (1..12), string_view overload. + /// \param month Month token view. + /// \return Month index [1..12]. + /// \throw std::invalid_argument if token is invalid. + inline int parse_month_index(std::string_view month) { + int value = 0; + if (!try_parse_month_index(month, value)) { + throw std::invalid_argument("Invalid month name"); + } + return value; + } +# endif + +//------------------------------------------------------------------------------ +// Small C-style helpers (no lambdas, no detail namespace) +//------------------------------------------------------------------------------ + + /// \brief Check whether character is ASCII whitespace. + TIME_SHIELD_CONSTEXPR inline bool is_ascii_space(char c) noexcept { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; + } + + /// \brief Check whether character is ASCII digit. + TIME_SHIELD_CONSTEXPR inline bool is_ascii_digit(char c) noexcept { + return c >= '0' && c <= '9'; + } + + /// \brief Skip ASCII whitespace. + TIME_SHIELD_CONSTEXPR inline void skip_spaces(const char*& p, const char* end) noexcept { + while (p < end && is_ascii_space(*p)) { + ++p; } } - throw std::invalid_argument("Invalid month name"); + + /// \brief Parse exactly 2 digits into int. + /// \return true on success. + TIME_SHIELD_CONSTEXPR inline bool parse_2digits(const char*& p, const char* end, int& out) noexcept { + if (end - p < 2) { + return false; + } + const char a = p[0]; + const char b = p[1]; + if (!is_ascii_digit(a) || !is_ascii_digit(b)) { + return false; + } + out = (a - '0') * 10 + (b - '0'); + p += 2; + return true; + } + + /// \brief Parse exactly 4 digits into year_t (via int). + /// \return true on success. + TIME_SHIELD_CONSTEXPR inline bool parse_4digits_year(const char*& p, const char* end, year_t& out) noexcept { + if (end - p < 4) { + return false; + } + const char a = p[0], b = p[1], c = p[2], d = p[3]; + if (!is_ascii_digit(a) || !is_ascii_digit(b) || !is_ascii_digit(c) || !is_ascii_digit(d)) { + return false; + } + const int v = (a - '0') * 1000 + (b - '0') * 100 + (c - '0') * 10 + (d - '0'); + out = static_cast(v); + p += 4; + return true; + } + + /// \brief Parse fractional seconds (1..9 digits) and convert to milliseconds. + /// \details Uses first 3 digits, scales if fewer. + /// \return true on success. + TIME_SHIELD_CONSTEXPR inline bool parse_fraction_to_ms(const char*& p, const char* end, int& ms_out) noexcept { + if (p >= end || !is_ascii_digit(*p)) { + return false; + } + + int ms = 0; + int digits = 0; + + while (p < end && is_ascii_digit(*p)) { + if (digits >= 3) { + return false; + } + ms = ms * 10 + (*p - '0'); + ++digits; + ++p; + } + + if (digits == 1) { + ms *= 100; + } else if (digits == 2) { + ms *= 10; + } + + ms_out = ms; + return true; + } + + } // namespace detail + +//------------------------------------------------------------------------------ +// Month helpers (public) +//------------------------------------------------------------------------------ + +// Canonical API (recommended): +// - parse_month(...) / try_parse_month(...): return month index as int [1..12] +// - parse_month_enum(...) / try_parse_month_enum(...): return month as enum Month (or any integral/enum T) + + /// \brief Try parse month name token into month index [1..12]. + /// \param month Month token (e.g. "Jan", "January"), case-insensitive. + /// \param value Output month index [1..12]. + /// \return True on success, false otherwise. + inline bool try_parse_month(const std::string& month, int& value) { + return detail::try_parse_month_index(month, value); + } + + /// \brief Parse month name token into month index [1..12]. + /// \param month Month token. + /// \return Month index [1..12]. + /// \throw std::invalid_argument if token is invalid. + inline int parse_month(const std::string& month) { + return detail::parse_month_index(month); + } + +#if __cplusplus >= 201703L + /// \brief Try parse month name token into month index [1..12], string_view overload. + /// \param month Month token view (e.g. "Jan", "January"), case-insensitive. + /// \param value Output month index [1..12]. + /// \return True on success, false otherwise. + inline bool try_parse_month(std::string_view month, int& value) { + return detail::try_parse_month_index(month, value); + } + + /// \brief Parse month name token into month index [1..12], string_view overload. + /// \param month Month token view. + /// \return Month index [1..12]. + /// \throw std::invalid_argument if token is invalid. + inline int parse_month(std::string_view month) { + return detail::parse_month_index(month); } +#endif - /// \brief Alias for get_month_number function. - /// \copydoc get_month_number +// Canonical: parse month -> enum Month (or any T) + + /// \brief Parse month name token into Month enum (throwing). + /// \tparam T Return type, default is Month enum. + /// \param month Month token. + /// \return Month number (1..12) converted to T. + /// \throw std::invalid_argument if token is invalid. template - T month_of_year(const std::string& month) { - return get_month_number(month); + inline T parse_month_enum(const std::string& month) { + return static_cast(detail::parse_month_index(month)); } -//------------------------------------------------------------------------------ + /// \brief Try parse month name token into Month enum (or any T). + /// \tparam T Output type, default is Month enum. + /// \param month Month token. + /// \param value Output month number (1..12) converted to T. + /// \return True if month token is valid, false otherwise. + template + inline bool try_parse_month_enum(const std::string& month, T& value) { + int idx = 0; + if (!detail::try_parse_month_index(month, idx)) return false; + value = static_cast(idx); + return true; + } - /// \brief Get the month number by name, with output parameter. - /// \tparam T The type for the month number, default is Month enum. - /// \param month The name of the month as a string. - /// \param value The reference to store the month number if found. - /// \return True if the month name is valid, false otherwise. +#if __cplusplus >= 201703L + /// \brief Parse month name token into Month enum (throwing), string_view overload. + /// \tparam T Return type, default is Month enum. + /// \param month Month token view. + /// \return Month number (1..12) converted to T. + /// \throw std::invalid_argument if token is invalid. template - bool try_get_month_number(const std::string& month, T& value) { - if (month.empty()) return false; - - std::string month_copy = month; - std::transform(month_copy.begin(), month_copy.end(), month_copy.begin(), [](char &ch) { - return std::use_facet>(std::locale()).tolower(ch); - }); - month_copy[0] = static_cast(std::toupper(month_copy[0])); - - static const std::array short_names = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - }; - static const std::array full_names = { - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - }; - - for (int i = 0; i < MONTHS_PER_YEAR; ++i) { - if (month_copy == short_names[i] || month_copy == full_names[i]) { - value = static_cast(i + 1); - return true; - } - } - return false; + inline T parse_month_enum(std::string_view month) { + return static_cast(detail::parse_month_index(month)); } - /// \brief Alias for try_get_month_number function. - /// \copydoc try_get_month_number + /// \brief Try parse month name token into Month enum (or any T), string_view overload. + /// \tparam T Output type, default is Month enum. + /// \param month Month token view. + /// \param value Output month number (1..12) converted to T. + /// \return True if month token is valid, false otherwise. template - bool get_month_number(const std::string& month, T& value) { - return try_get_month_number(month, value); + inline bool try_parse_month_enum(std::string_view month, T& value) { + int idx = 0; + if (!detail::try_parse_month_index(month, idx)) return false; + value = static_cast(idx); + return true; } +#endif - /// \brief Alias for try_get_month_number function. - /// \copydoc try_get_month_number +// Index aliases (int) + + /// \brief Try parse month name token into month index [1..12]. + /// \param month Month token (e.g. "Jan", "January"), case-insensitive. + /// \param value Output month index [1..12]. + /// \return True on success, false otherwise. + inline bool try_get_month_index(const std::string& month, int& value) { + return try_parse_month(month, value); + } + + /// \brief Parse month name token into month index [1..12]. + /// \param month Month token. + /// \return Month index [1..12]. + /// \throw std::invalid_argument if token is invalid. + inline int get_month_index(const std::string& month) { + return parse_month(month); + } + + /// \brief Parse month name token into Month enum. + /// \param month Month token. + /// \return Month enum value (1..12). + /// \throw std::invalid_argument if token is invalid. + inline Month get_month_index_enum(const std::string& month) { + return static_cast(detail::parse_month_index(month)); + } + +#if __cplusplus >= 201703L + /// \brief Try parse month name token into month index [1..12], string_view overload. + inline bool try_get_month_index(std::string_view month, int& value) { + return try_parse_month(month, value); + } + + /// \brief Parse month name token into month index [1..12], string_view overload. + inline int get_month_index(std::string_view month) { + return parse_month(month); + } + + /// \brief Parse month name token into Month enum, string_view overload. + inline Month get_month_index_enum(std::string_view month) { + return static_cast(detail::parse_month_index(month)); + } +#endif + +// Month number aliases (T) + + /// \brief Get the month number by name (throwing). + /// \tparam T Return type, default is Month enum. + /// \param month Month token. + /// \return Month number (1..12) converted to T. + /// \throw std::invalid_argument if token is invalid. + template + inline T get_month_number(const std::string& month) { + return parse_month_enum(month); + } + + /// \brief Alias for get_month_number (throwing). + template + inline T month_of_year(const std::string& month) { + return get_month_number(month); + } + + /// \brief Try get the month number by name, with output parameter. + /// \tparam T Output type, default is Month enum. + /// \param month Month token. + /// \param value Output month number (1..12) converted to T. + /// \return True if month token is valid, false otherwise. template - bool month_of_year(const std::string& month, T& value) { - return try_get_month_number(month, value); + inline bool try_get_month_number(const std::string& month, T& value) { + return try_parse_month_enum(month, value); } + /// \brief Alias for try_get_month_number (output parameter). + template + inline bool get_month_number(const std::string& month, T& value) { + return try_get_month_number(month, value); + } + + /// \brief Alias for try_get_month_number (output parameter). + template + inline bool month_of_year(const std::string& month, T& value) { + return try_get_month_number(month, value); + } + +#if __cplusplus >= 201703L + /// \brief Get the month number by name (throwing), string_view overload. + template + inline T get_month_number(std::string_view month) { + return parse_month_enum(month); + } + + /// \brief Alias for get_month_number (throwing), string_view overload. + template + inline T month_of_year(std::string_view month) { + return get_month_number(month); + } + + /// \brief Try get the month number by name, string_view overload. + template + inline bool try_get_month_number(std::string_view month, T& value) { + return try_parse_month_enum(month, value); + } + + /// \brief Alias for try_get_month_number, string_view overload. + template + inline bool get_month_number(std::string_view month, T& value) { + return try_get_month_number(month, value); + } + + /// \brief Alias for try_get_month_number, string_view overload. + template + inline bool month_of_year(std::string_view month, T& value) { + return try_get_month_number(month, value); + } +#endif + +// const char* overloads to avoid ambiguity with string vs string_view for literals + + /// \brief Get the month number by name (throwing), const char* overload. + /// \tparam T Return type, default is Month enum. + /// \param month Month token C-string. + /// \return Month number (1..12) converted to T. + /// \throw std::invalid_argument if token is invalid. + template + inline T get_month_number(const char* month) { +#if __cplusplus >= 201703L + return get_month_number(std::string_view(month)); +#else + return get_month_number(std::string(month)); +#endif + } + + /// \brief Try get the month number by name, const char* overload. + /// \tparam T Output type, default is Month enum. + /// \param month Month token C-string. + /// \param value Output month number (1..12) converted to T. + /// \return True if month token is valid, false otherwise. + template + inline bool try_get_month_number(const char* month, T& value) { +#if __cplusplus >= 201703L + return try_get_month_number(std::string_view(month), value); +#else + return try_get_month_number(std::string(month), value); +#endif + } + + /// \brief Alias for get_month_number (throwing), const char* overload. + template + inline T month_of_year(const char* month) { + return get_month_number(month); + } + + /// \brief Alias for try_get_month_number (output parameter), const char* overload. + template + inline bool get_month_number(const char* month, T& value) { + return try_get_month_number(month, value); + } + + /// \brief Alias for try_get_month_number (output parameter), const char* overload. + template + inline bool month_of_year(const char* month, T& value) { + return try_get_month_number(month, value); + } + +//------------------------------------------------------------------------------ +// Time zone parsing (C-style, high performance) //------------------------------------------------------------------------------ - /// \brief Parse a time zone string into a TimeZoneStruct. - /// \details This function parses a time zone string in the format "+hh:mm" or "Z" into a TimeZoneStruct. - /// If the string is empty or "Z", it sets the time zone to UTC. - /// \param tz_str The time zone string. - /// \param tz The TimeZoneStruct to be filled. - /// \return True if the parsing is successful and the time zone is valid, false otherwise. - inline bool parse_time_zone(const std::string& tz_str, TimeZoneStruct &tz) { - if (tz_str.empty()) { + /// \brief Parse timezone character buffer into TimeZoneStruct. + /// \details Supported formats: + /// - "" -> UTC (+00:00) + /// - "Z" -> UTC (+00:00) + /// - "+HH:MM" or "-HH:MM" + /// \param data Pointer to timezone buffer (may be not null-terminated). + /// \param length Number of characters in buffer. + /// \param tz Output time zone struct. + /// \return True if parsing succeeds and tz is valid, false otherwise. + inline bool parse_time_zone(const char* data, std::size_t length, TimeZoneStruct& tz) noexcept { + if (!data) { + return false; + } + + if (length == 0) { tz.hour = 0; tz.min = 0; tz.is_positive = true; return true; } - if (tz_str == "Z") { + + if (length == 1 && (data[0] == 'Z' || data[0] == 'z')) { tz.hour = 0; tz.min = 0; tz.is_positive = true; return true; } - tz.is_positive = (tz_str[0] == '+'); - tz.hour = std::stoi(tz_str.substr(1, 2)); - tz.min = std::stoi(tz_str.substr(4, 2)); + + if (length != 6) { + return false; + } + + const char sign = data[0]; + if (sign != '+' && sign != '-') { + return false; + } + if (data[3] != ':') { + return false; + } + if (!detail::is_ascii_digit(data[1]) || !detail::is_ascii_digit(data[2]) || + !detail::is_ascii_digit(data[4]) || !detail::is_ascii_digit(data[5])) { + return false; + } + + tz.is_positive = (sign == '+'); + tz.hour = (data[1] - '0') * 10 + (data[2] - '0'); + tz.min = (data[4] - '0') * 10 + (data[5] - '0'); + return is_valid_time_zone(tz); } - /// \brief Alias for parse_time_zone function. - /// \copydoc parse_time_zone - inline bool parse_tz(const std::string& tz_str, TimeZoneStruct &tz) { + /// \brief Parse timezone string into TimeZoneStruct. + /// \details Wrapper over parse_time_zone(const char*, std::size_t, TimeZoneStruct&). + inline bool parse_time_zone(const std::string& tz_str, TimeZoneStruct& tz) noexcept { + return parse_time_zone(tz_str.c_str(), tz_str.size(), tz); + } + + /// \brief Alias for parse_time_zone. + inline bool parse_tz(const std::string& tz_str, TimeZoneStruct& tz) noexcept { return parse_time_zone(tz_str, tz); } + /// \brief Alias for parse_time_zone (buffer overload). + inline bool parse_tz(const char* data, std::size_t length, TimeZoneStruct& tz) noexcept { + return parse_time_zone(data, length, tz); + } + +//------------------------------------------------------------------------------ +// ISO8601 parsing (C-style, no regex, no allocations) //------------------------------------------------------------------------------ - /// \brief Parse a date and time string in ISO8601 format. - /// \details This function parses a date and time string in the ISO8601 format and fills the provided - /// DateTimeStruct and TimeZoneStruct with the parsed values. - /// \param input The input string in ISO8601 format. - /// \param dt The DateTimeStruct to be filled with the parsed date and time values. - /// \param tz The TimeZoneStruct to be filled with the parsed time zone values. - /// \return True if the parsing is successful and the date and time values are valid, false otherwise. - inline bool parse_iso8601( - const std::string& input, - DateTimeStruct &dt, - TimeZoneStruct &tz) { - // Регулярное выражение для даты в формате ISO8601 - static const std::regex date_regex(R"((\d{4})[-\/\.](\d{2})[-\/\.](\d{2}))"); - std::smatch date_match; - - // Регулярное выражение для времени в формате ISO8601 с часовым поясом и без - static const std::regex time_regex(R"((\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?)"); - std::smatch time_match; + /// \brief Parse ISO8601 character buffer into DateTimeStruct and TimeZoneStruct. + /// \details Supported inputs: + /// - "YYYY-MM-DD" + /// - "YYYY-MM-DDThh:mm" + /// - "YYYY-MM-DDThh:mm:ss" + /// - "YYYY-MM-DDThh:mm:ss.fff" (1..9 digits fraction; milliseconds from first 3 digits, scaled if fewer) + /// - Any of the above time forms with "Z" or "+HH:MM"/"-HH:MM" + /// - Separator between date and time: 'T' or ASCII whitespace. + /// + /// Date separators supported: '-', '/', '.' (as in original regex). + /// + /// \param input Pointer to buffer (may be not null-terminated). + /// \param length Buffer length. + /// \param dt Output DateTimeStruct (filled). On success, dt is always initialized. + /// \param tz Output TimeZoneStruct (filled). If timezone is not present, UTC is used. + /// \return True if parsing succeeds and dt is valid, false otherwise. + inline bool parse_iso8601(const char* input, std::size_t length, + DateTimeStruct& dt, TimeZoneStruct& tz) noexcept { + if (!input) { + return false; + } + + const char* p = input; + const char* end = input + length; + + detail::skip_spaces(p, end); dt = create_date_time_struct(0); tz = create_time_zone_struct(0, 0); + tz.is_positive = true; + + const char* const date_start = p; + const char* date_end = p; + while (date_end < end && *date_end != 'T' && *date_end != 't' && !detail::is_ascii_space(*date_end)) { + ++date_end; + } - // Парсинг даты - if (std::regex_search(input, date_match, date_regex)) { - dt.year = std::stoll(date_match[1].str()); - dt.mon = std::stoi(date_match[2].str()); - dt.day = std::stoi(date_match[3].str()); + bool parsed_iso_week_date = false; + if (date_end > date_start) { + IsoWeekDateStruct iso_date{}; + if (parse_iso_week_date(date_start, static_cast(date_end - date_start), iso_date)) { + const DateStruct calendar_date = iso_week_date_to_date(iso_date); + dt.year = calendar_date.year; + dt.mon = calendar_date.mon; + dt.day = calendar_date.day; + p = date_end; + parsed_iso_week_date = true; + } + } + + if (!parsed_iso_week_date) { + // ---- Date: YYYYMMDD + if (!detail::parse_4digits_year(p, end, dt.year)) { + return false; + } + if (p >= end) { + return false; + } + const char sep1 = *p; + if (sep1 != '-' && sep1 != '/' && sep1 != '.') { + return false; + } + ++p; + + if (!detail::parse_2digits(p, end, dt.mon)) { + return false; + } + if (p >= end) { + return false; + } + const char sep2 = *p; + if (sep2 != '-' && sep2 != '/' && sep2 != '.') { + return false; + } + ++p; + + if (!detail::parse_2digits(p, end, dt.day)) { + return false; + } + } + + if (!is_valid_date(dt.year, dt.mon, dt.day)) { + return false; + } + + // Date-only? + { + const char* q = p; + detail::skip_spaces(q, end); + if (q == end) { + // dt already has time=0 ms=0 + return is_valid_date_time(dt); + } + } + + // ---- Date/time separator: 'T' or whitespace + if (p >= end) { + return false; + } + + if (*p == 'T' || *p == 't') { + ++p; + } else + if (detail::is_ascii_space(*p)) { + // allow one or more spaces + detail::skip_spaces(p, end); } else { return false; } - // Парсинг времени и часового пояса - if (std::regex_search(input, time_match, time_regex)) { - dt.hour = std::stoi(time_match[1].str()); - dt.min = std::stoi(time_match[2].str()); - dt.sec = std::stoi(time_match[3].str()); - if (time_match[4].matched) { - dt.ms = std::stoi(time_match[4].str()); + // ---- Time: hh:mm[:ss][.frac] + if (!detail::parse_2digits(p, end, dt.hour)) { + return false; + } + if (p >= end || *p != ':') { + return false; + } + ++p; + + if (!detail::parse_2digits(p, end, dt.min)) { + return false; + } + + dt.sec = 0; + dt.ms = 0; + bool has_seconds = false; + + // Optional :ss + if (p < end && *p == ':') { + ++p; + if (!detail::parse_2digits(p, end, dt.sec)) { + return false; } - if (time_match[5].matched) { - if (!parse_time_zone(time_match[5].str(), tz)) return false; + has_seconds = true; + } + + // Optional .fraction (allowed only if we had seconds in original regex, + // but we accept it when seconds are present; for hh:mm (no seconds) we keep it strict). + if (p < end && *p == '.') { + // require seconds field to exist (avoid accepting YYYY-MM-DDThh:mm.xxx) + if (!has_seconds) { + // Ambiguous: could be "hh:mm.fff" which is not in your original formats. + // Keep strict to preserve behavior. + return false; } - return is_valid_date_time(dt); + + ++p; + int ms = 0; + if (!detail::parse_fraction_to_ms(p, end, ms)) { + return false; + } + dt.ms = ms; } - return true; + + // ---- Optional timezone: [spaces] (Z | ±HH:MM) + detail::skip_spaces(p, end); + + if (p < end) { + if (*p == 'Z' || *p == 'z') { + tz.hour = 0; + tz.min = 0; + tz.is_positive = true; + ++p; + } else if (*p == '+' || *p == '-') { + // need 6 chars + if (static_cast(end - p) < 6) { + return false; + } + if (!parse_time_zone(p, 6, tz)) { + return false; + } + p += 6; + } + } + + detail::skip_spaces(p, end); + if (p != end) { + return false; + } + + return is_valid_date_time(dt); + } + + /// \brief Parse ISO8601 string into DateTimeStruct and TimeZoneStruct. + /// \details Wrapper over parse_iso8601(const char*, std::size_t, DateTimeStruct&, TimeZoneStruct&). + inline bool parse_iso8601(const std::string& input, DateTimeStruct& dt, TimeZoneStruct& tz) noexcept { + return parse_iso8601(input.c_str(), input.size(), dt, tz); } + +//------------------------------------------------------------------------------ +// ISO8601 -> timestamps +//------------------------------------------------------------------------------ /// \brief Convert an ISO8601 string to a timestamp (ts_t). - /// \details This function parses a string in ISO8601 format and converts it to a timestamp. - /// \param str The ISO8601 string. - /// \param ts The timestamp to be filled. - /// \return True if the parsing and conversion are successful, false otherwise. - inline bool str_to_ts(const std::string &str, ts_t& ts) { + /// \param str ISO8601 string. + /// \param ts Output timestamp (seconds). + /// \return True if parsing and conversion succeed, false otherwise. + inline bool str_to_ts(const std::string& str, ts_t& ts) { DateTimeStruct dt; TimeZoneStruct tz; if (!parse_iso8601(str, dt, tz)) return false; try { - ts = to_timestamp(dt) + to_offset(tz); + ts = dt_to_timestamp(dt) + to_offset(tz); return true; - } catch(...) {} + } catch (...) {} return false; } + + /// \brief Parse ISO8601 character buffer and convert to timestamp (seconds). + /// \param data Pointer to character buffer. + /// \param length Buffer length in bytes. + /// \param ts Output timestamp in seconds. + /// \return true if parsing succeeds, false otherwise. + inline bool str_to_ts(const char* data, std::size_t length, ts_t& ts) { + if (!data || length == 0) { + ts = 0; + return false; + } + // Без string_view (C++11) просто собираем std::string по длине. + return str_to_ts(std::string(data, length), ts); + } /// \brief Convert an ISO8601 string to a millisecond timestamp (ts_ms_t). - /// \details This function parses a string in ISO8601 format and converts it to a millisecond timestamp. - /// \param str The ISO8601 string. - /// \param ts The millisecond timestamp to be filled. - /// \return True if the parsing and conversion are successful, false otherwise. - inline bool str_to_ts_ms(const std::string &str, ts_ms_t& ts) { + /// \param str ISO8601 string. + /// \param ts Output timestamp (milliseconds). + /// \return True if parsing and conversion succeed, false otherwise. + inline bool str_to_ts_ms(const std::string& str, ts_ms_t& ts) { DateTimeStruct dt; TimeZoneStruct tz; if (!parse_iso8601(str, dt, tz)) return false; try { - ts = to_timestamp_ms(dt) + sec_to_ms(to_offset(tz)); + ts = static_cast(dt_to_timestamp_ms(dt)) + sec_to_ms(to_offset(tz)); return true; - } catch(...) {} + } catch (...) {} return false; } + + /// \brief Convert ISO8601 character buffer to millisecond timestamp (ts_ms_t). + /// \param data Pointer to character buffer. + /// \param length Number of characters in buffer. + /// \param ts Output timestamp in milliseconds. + /// \return True if parsing and conversion succeed, false otherwise. + inline bool str_to_ts_ms(const char* data, std::size_t length, ts_ms_t& ts) { + if (!data || length == 0) { + ts = 0; + return false; + } + return str_to_ts_ms(std::string(data, length), ts); + } /// \brief Convert an ISO8601 string to a floating-point timestamp (fts_t). - /// \details This function parses a string in ISO8601 format and converts it to a floating-point timestamp. - /// \param str The ISO8601 string. - /// \param ts The floating-point timestamp to be filled. - /// \return True if the parsing and conversion are successful, false otherwise. - inline bool str_to_fts(const std::string &str, fts_t& ts) { + /// \param str ISO8601 string. + /// \param ts Output timestamp (floating-point seconds). + /// \return True if parsing and conversion succeed, false otherwise. + inline bool str_to_fts(const std::string& str, fts_t& ts) { DateTimeStruct dt; TimeZoneStruct tz; if (!parse_iso8601(str, dt, tz)) return false; try { - ts = to_ftimestamp(dt) + static_cast(to_offset(tz)); + ts = dt_to_ftimestamp(dt) + static_cast(to_offset(tz)); return true; - } catch(...) {} + } catch (...) {} return false; } + + /// \brief Convert ISO8601 character buffer to floating-point timestamp (fts_t). + /// \param data Pointer to character buffer. + /// \param length Number of characters in buffer. + /// \param ts Output timestamp in floating-point seconds. + /// \return True if parsing and conversion succeed, false otherwise. + inline bool str_to_fts(const char* data, std::size_t length, fts_t& ts) { + if (!data || length == 0) { + ts = 0; + return false; + } + return str_to_fts(std::string(data, length), ts); + } + +//------------------------------------------------------------------------------ +// Convenience string -> predicates (workdays) +//------------------------------------------------------------------------------ + + /// \brief Parse ISO8601 string and check if it falls on a workday (seconds precision). + inline bool is_workday(const std::string& str) { + ts_t ts = 0; + if (!str_to_ts(str, ts)) return false; + return is_workday(ts); + } + + /// \brief Parse ISO8601 string and check if it falls on a workday (milliseconds precision). + inline bool is_workday_ms(const std::string& str) { + ts_ms_t ts = 0; + if (!str_to_ts_ms(str, ts)) return false; + return is_workday_ms(ts); + } + + /// \brief Alias for is_workday(const std::string&). + /// \copydoc is_workday(const std::string&) + inline bool workday(const std::string& str) { + return is_workday(str); + } + + /// \brief Alias for is_workday_ms(const std::string&). + /// \copydoc is_workday_ms(const std::string&) + inline bool workday_ms(const std::string& str) { + return is_workday_ms(str); + } + + /// \brief Parse ISO8601 string and check if it is the first workday of its month (seconds). + /// \param str ISO8601 formatted string. + /// \return true if parsing succeeds and the timestamp corresponds to the first workday of the month, false otherwise. + inline bool is_first_workday_of_month(const std::string& str) { + ts_t ts = 0; + if (!str_to_ts(str, ts)) return false; + return is_first_workday_of_month(ts); + } + + /// \brief Parse an ISO8601 string and check if it is the first workday of its month (millisecond precision). + /// \param str ISO8601 formatted string. + /// \return true if parsing succeeds and the timestamp corresponds to the first workday of the month, false otherwise. + inline bool is_first_workday_of_month_ms(const std::string& str) { + ts_ms_t ts = 0; + if (!str_to_ts_ms(str, ts)) return false; + return is_first_workday_of_month_ms(ts); + } + + /// \brief Parse an ISO8601 string and check if it is the last workday of its month (seconds). + /// \param str ISO8601 formatted string. + /// \return true if parsing succeeds and the timestamp corresponds to the last workday of the month, false otherwise. + inline bool is_last_workday_of_month(const std::string& str) { + ts_t ts = 0; + if (!str_to_ts(str, ts)) return false; + return is_last_workday_of_month(ts); + } + + /// \brief Parse an ISO8601 string and check if it is the last workday of its month (millisecond). + /// \param str ISO8601 formatted string. + /// \return true if parsing succeeds and the timestamp corresponds to the last workday of the month, false otherwise. + inline bool is_last_workday_of_month_ms(const std::string& str) { + ts_ms_t ts = 0; + if (!str_to_ts_ms(str, ts)) return false; + return is_last_workday_of_month_ms(ts); + } + + /// \brief Parse an ISO8601 string and check if it falls within the first N workdays of its month. + /// \param str ISO8601 formatted string. + /// \param count Number of leading workdays to test against. + /// \return true if parsing succeeds and the timestamp corresponds to a workday ranked within the first N positions, false otherwise. + inline bool is_within_first_workdays_of_month(const std::string& str, int count) { + ts_t ts = 0; + if (!str_to_ts(str, ts)) return false; + return is_within_first_workdays_of_month(ts, count); + } + + /// \brief Parse an ISO8601 string and check if it falls within the first N workdays of its month (millisecond precision). + /// \param str ISO8601 formatted string. + /// \param count Number of leading workdays to test against. + /// \return true if parsing succeeds and the timestamp corresponds to a workday ranked within the first N positions, false otherwise. + inline bool is_within_first_workdays_of_month_ms(const std::string& str, int count) { + ts_ms_t ts = 0; + if (!str_to_ts_ms(str, ts)) return false; + return is_within_first_workdays_of_month_ms(ts, count); + } + + /// \brief Parse ISO8601 string and check if it is within last N workdays of its month (seconds). + /// \param str ISO8601 formatted string. + /// \param count Number of trailing workdays to test against. + /// \return true if parsing succeeds and the timestamp corresponds to a workday ranked within the final N positions, false otherwise. + inline bool is_within_last_workdays_of_month(const std::string& str, int count) { + ts_t ts = 0; + if (!str_to_ts(str, ts)) return false; + return is_within_last_workdays_of_month(ts, count); + } + + /// \brief Parse ISO8601 string and check if it is within last N workdays of its month (milliseconds). + /// \param str ISO8601 formatted string. + /// \param count Number of trailing workdays to test against. + /// \return true if parsing succeeds and the timestamp corresponds to a workday ranked within the final N positions, false otherwise. + inline bool is_within_last_workdays_of_month_ms(const std::string& str, int count) { + ts_ms_t ts = 0; + if (!str_to_ts_ms(str, ts)) return false; + return is_within_last_workdays_of_month_ms(ts, count); + } + +//------------------------------------------------------------------------------ +// Convenience: C-string wrappers (non-throwing, ambiguous on failure) +//------------------------------------------------------------------------------ + + /// \brief Convert ISO8601 C-string to timestamp (seconds). + /// \details Returns 0 on failure (ambiguous if epoch is a valid value for your usage). + /// \param str C-style string with ISO8601 timestamp, may be nullptr. + /// \return Timestamp in seconds, or 0 if parsing fails. + inline ts_t ts(const char* str) { + ts_t out = 0; + str_to_ts(str ? std::string(str) : std::string(), out); + return out; + } + + /// \brief Convert ISO8601 character buffer to timestamp (seconds). + /// \details Does not require null terminator. Returns 0 on failure + /// (ambiguous if epoch is a valid value for your usage). + /// \param data Pointer to character buffer. + /// \param length Number of characters in buffer. + /// \return Timestamp in seconds, or 0 if parsing fails. + inline ts_t ts(const char* data, std::size_t length) { + ts_t out = 0; + if (!str_to_ts(data, length, out)) { + return 0; + } + return out; + } + + /// \brief Convert ISO8601 C-string to timestamp (milliseconds). + /// \details Returns 0 on failure (ambiguous if epoch is a valid value for your usage). + /// \param str C-style string with ISO8601 timestamp, may be nullptr. + /// \return Timestamp in milliseconds, or 0 if parsing fails. + inline ts_ms_t ts_ms(const char* str) { + ts_ms_t out = 0; + str_to_ts_ms(str ? std::string(str) : std::string(), out); + return out; + } + + /// \brief Convert ISO8601 character buffer to timestamp (milliseconds). + /// \details Does not require null terminator. Returns 0 on failure. + /// \param data Pointer to character buffer. + /// \param length Number of characters in buffer. + /// \return Timestamp in milliseconds, or 0 if parsing fails. + inline ts_ms_t ts_ms(const char* data, std::size_t length) { + ts_ms_t out = 0; + if (!str_to_ts_ms(data, length, out)) { + return 0; + } + return out; + } + + /// \brief Convert ISO8601 C-string to floating timestamp (seconds). + /// \details Returns 0 on failure. + /// \param str C-style string with ISO8601 timestamp, may be nullptr. + /// \return Timestamp in seconds (floating-point), or 0 if parsing fails. + inline fts_t fts(const char* str) { + fts_t out = 0; + str_to_fts(str ? std::string(str) : std::string(), out); + return out; + } + + /// \brief Convert ISO8601 character buffer to floating timestamp (seconds). + /// \details Does not require null terminator. Returns 0 on failure. + /// \param data Pointer to character buffer. + /// \param length Number of characters in buffer. + /// \return Timestamp in seconds (floating-point), or 0 if parsing fails. + inline fts_t fts(const char* data, std::size_t length) { + fts_t out = 0; + if (!str_to_fts(data, length, out)) { + return 0.0; + } + return out; + } + +//------------------------------------------------------------------------------ /// \brief Convert an ISO8601 string to a timestamp (ts_t). /// \details This function parses a string in ISO8601 format and converts it to a timestamp. @@ -308,7 +1114,7 @@ namespace time_shield { return ts; } - //-------------------------------------------------------------------------- +//------------------------------------------------------------------------------ /// \brief Parse time of day string to seconds of day. /// @@ -378,40 +1184,6 @@ namespace time_shield { return static_cast(SEC_PER_DAY); } - - /// \brief Convert an ISO8601 C-style string to a timestamp (ts_t). - /// \details This function parses a string in ISO8601 format and converts it to a timestamp. - /// If parsing fails, it returns 0. - /// \param str The ISO8601 C-style string. - /// \return The timestamp value. Returns 0 if parsing fails. - inline ts_t ts(const char *str) { - ts_t ts = 0; - str_to_ts(str, ts); - return ts; - } - - /// \brief Convert an ISO8601 C-style string to a millisecond timestamp (ts_ms_t). - /// \details This function parses a string in ISO8601 format and converts it to a millisecond timestamp. - /// If parsing fails, it returns 0. - /// \param str The ISO8601 C-style string. - /// \return The parsed millisecond timestamp, or 0 if parsing fails. - inline ts_ms_t ts_ms(const char *str) { - ts_ms_t ts = 0; - str_to_ts_ms(str, ts); - return ts; - } - - /// \brief Convert an ISO8601 C-style string to a floating-point timestamp (fts_t). - /// \details This function parses a string in ISO8601 format and converts it to a floating-point timestamp. - /// If the parsing fails, it returns 0. - /// \param str The ISO8601 C-style string. - /// \return The floating-point timestamp if successful, 0 otherwise. - inline fts_t fts(const char *str) { - fts_t ts = 0; - str_to_fts(str, ts); - return ts; - } - /// \} }; diff --git a/include/time_shield/time_unit_conversions.hpp b/include/time_shield/time_unit_conversions.hpp new file mode 100644 index 00000000..a6a52cec --- /dev/null +++ b/include/time_shield/time_unit_conversions.hpp @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_TIME_UNIT_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_TIME_UNIT_CONVERSIONS_HPP_INCLUDED + +/// \file time_unit_conversions.hpp +/// \brief Helper functions for unit conversions between seconds, minutes, hours, and milliseconds. + +#include "config.hpp" +#include "constants.hpp" +#include "detail/floor_math.hpp" +#include "types.hpp" + +#include +#include + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + /// \brief Get the nanosecond part of the second from a floating-point timestamp. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in floating-point seconds. + /// \return T Nanosecond part of the second. + template + TIME_SHIELD_CONSTEXPR T ns_of_sec(fts_t ts) noexcept { + const int64_t ns = static_cast(std::floor(ts * static_cast(NS_PER_SEC))); + return static_cast(detail::floor_mod(ns, NS_PER_SEC)); + } + + /// \brief Get the microsecond part of the second from a floating-point timestamp. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in floating-point seconds. + /// \return T Microsecond part of the second. + template + TIME_SHIELD_CONSTEXPR T us_of_sec(fts_t ts) noexcept { + const int64_t us = static_cast(std::floor(ts * static_cast(US_PER_SEC))); + return static_cast(detail::floor_mod(us, US_PER_SEC)); + } + + /// \brief Get the millisecond part of the second from a floating-point timestamp. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in floating-point seconds. + /// \return T Millisecond part of the second. + template + TIME_SHIELD_CONSTEXPR T ms_of_sec(fts_t ts) noexcept { + const int64_t ms = static_cast(std::floor(ts * static_cast(MS_PER_SEC))); + return static_cast(detail::floor_mod(ms, MS_PER_SEC)); + } + + /// \brief Get the nanosecond part of the second from a floating-point timestamp (truncating). + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in floating-point seconds. + /// \return T Nanosecond part of the second, truncating toward zero. + template + TIME_SHIELD_CONSTEXPR T ns_of_sec_signed(fts_t ts) noexcept { + fts_t temp = 0; + return static_cast(std::round(std::modf(ts, &temp) * static_cast(NS_PER_SEC))); + } + + /// \brief Get the microsecond part of the second from a floating-point timestamp (truncating). + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in floating-point seconds. + /// \return T Microsecond part of the second, truncating toward zero. + template + TIME_SHIELD_CONSTEXPR T us_of_sec_signed(fts_t ts) noexcept { + fts_t temp = 0; + return static_cast(std::round(std::modf(ts, &temp) * static_cast(US_PER_SEC))); + } + + /// \brief Get the millisecond part of the second from a floating-point timestamp (truncating). + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in floating-point seconds. + /// \return T Millisecond part of the second, truncating toward zero. + template + TIME_SHIELD_CONSTEXPR T ms_of_sec_signed(fts_t ts) noexcept { + fts_t temp = 0; + return static_cast(std::round(std::modf(ts, &temp) * static_cast(MS_PER_SEC))); + } + + /// \brief Get the millisecond part of the timestamp. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in milliseconds. + /// \return T Millisecond part of the timestamp. + template + constexpr T ms_part(ts_ms_t ts) noexcept { + return static_cast(detail::floor_mod(static_cast(ts), MS_PER_SEC)); + } + + /// \brief Alias for ms_part. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in milliseconds. + /// \return T Millisecond part of the timestamp. + template + constexpr T ms_of_ts(ts_ms_t ts) noexcept { + return ms_part(ts); + } + + /// \brief Get the microsecond part of the timestamp. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in microseconds. + /// \return T Microsecond part of the timestamp. + template + constexpr T us_part(ts_us_t ts) noexcept { + return static_cast(detail::floor_mod(static_cast(ts), US_PER_SEC)); + } + + /// \brief Alias for us_part. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in microseconds. + /// \return T Microsecond part of the timestamp. + template + constexpr T us_of_ts(ts_us_t ts) noexcept { + return us_part(ts); + } + + /// \brief Get the nanosecond part of the timestamp. + /// \tparam T Type of the returned value (default is int). + /// \param ts Timestamp in nanoseconds. + /// \return T Nanosecond part of the timestamp. + template + constexpr T ns_part(T2 ts) noexcept { + return static_cast(detail::floor_mod(static_cast(ts), NS_PER_SEC)); + } + +# ifndef TIME_SHIELD_CPP17 + /// \brief Helper function for converting seconds to milliseconds (floating-point version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in seconds. + /// \param tag std::true_type indicates a floating-point type. + /// \return ts_ms_t Timestamp in milliseconds. + template + constexpr ts_ms_t sec_to_ms_impl(T t, std::true_type) noexcept { + return static_cast(std::round(t * static_cast(MS_PER_SEC))); + } + + /// \brief Helper function for converting seconds to milliseconds (integral version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in seconds. + /// \param tag std::false_type indicates a non-floating-point type. + /// \return ts_ms_t Timestamp in milliseconds. + template + constexpr ts_ms_t sec_to_ms_impl(T t, std::false_type) noexcept { + return static_cast(t) * static_cast(MS_PER_SEC); + } +# endif // TIME_SHIELD_CPP17 + + /// \brief Converts a timestamp from seconds to milliseconds. + /// \tparam T1 The type of the output timestamp (default is ts_ms_t). + /// \tparam T2 The type of the input timestamp. + /// \param ts Timestamp in seconds. + /// \return T1 Timestamp in milliseconds. + template + constexpr T1 sec_to_ms(T2 ts) noexcept { +# ifdef TIME_SHIELD_CPP17 + if constexpr (std::is_floating_point_v) { + return static_cast(std::round(ts * static_cast(MS_PER_SEC))); + } else { + return static_cast(ts) * static_cast(MS_PER_SEC); + } +# else + return static_cast(sec_to_ms_impl(ts, typename std::conditional< + (std::is_same::value || std::is_same::value), + std::true_type, + std::false_type + >::type{})); +# endif + } + + /// \brief Converts a floating-point timestamp from seconds to milliseconds. + /// \param ts Timestamp in floating-point seconds. + /// \return ts_ms_t Timestamp in milliseconds. + inline ts_ms_t fsec_to_ms(fts_t ts) noexcept { + return static_cast(std::round(ts * static_cast(MS_PER_SEC))); + } + + /// \brief Converts a timestamp from milliseconds to seconds. + /// \tparam T1 The type of the output timestamp (default is ts_t). + /// \tparam T2 The type of the input timestamp (default is ts_ms_t). + /// \param ts_ms Timestamp in milliseconds. + /// \return T1 Timestamp in seconds. + template + constexpr T1 ms_to_sec(T2 ts_ms) noexcept { + return static_cast(detail::floor_div( + static_cast(ts_ms), + static_cast(MS_PER_SEC))); + } + + /// \brief Converts a timestamp from milliseconds to floating-point seconds. + /// \tparam T The type of the input timestamp (default is ts_ms_t). + /// \param ts_ms Timestamp in milliseconds. + /// \return fts_t Timestamp in floating-point seconds. + template + constexpr fts_t ms_to_fsec(T ts_ms) noexcept { + return static_cast(ts_ms) / static_cast(MS_PER_SEC); + } + +//----------------------------------------------------------------------------// +// Minutes -> Milliseconds +//----------------------------------------------------------------------------// +# ifndef TIME_SHIELD_CPP17 + /// \brief Helper function for converting minutes to milliseconds (floating-point version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in minutes. + /// \param tag std::true_type indicates a floating-point type (double or float). + /// \return ts_ms_t Timestamp in milliseconds. + template + constexpr ts_ms_t min_to_ms_impl(T t, std::true_type) noexcept { + return static_cast(std::round(t * static_cast(MS_PER_MIN))); + } + + /// \brief Helper function for converting minutes to milliseconds (integral version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in minutes. + /// \param tag std::false_type indicates a non-floating-point type. + /// \return ts_ms_t Timestamp in milliseconds. + template + constexpr ts_ms_t min_to_ms_impl(T t, std::false_type) noexcept { + return static_cast(t) * static_cast(MS_PER_MIN); + } +# endif // TIME_SHIELD_CPP17 + + /// \brief Converts a timestamp from minutes to milliseconds. + /// \tparam T1 The type of the output timestamp (default is ts_ms_t). + /// \tparam T2 The type of the input timestamp. + /// \param ts Timestamp in minutes. + /// \return T1 Timestamp in milliseconds. + template + constexpr T1 min_to_ms(T2 ts) noexcept { +# ifdef TIME_SHIELD_CPP17 + if constexpr (std::is_floating_point_v) { + return static_cast(std::round(ts * static_cast(MS_PER_MIN))); + } else { + return static_cast(ts) * static_cast(MS_PER_MIN); + } +# else + return static_cast(min_to_ms_impl(ts, typename std::conditional< + (std::is_same::value || std::is_same::value), + std::true_type, + std::false_type + >::type{})); +# endif + } + + /// \brief Converts a timestamp from milliseconds to minutes. + /// \tparam T1 The type of the output timestamp (default is int). + /// \tparam T2 The type of the input timestamp (default is ts_ms_t). + /// \param ts Timestamp in milliseconds. + /// \return T1 Timestamp in minutes. + template + constexpr T1 ms_to_min(T2 ts) noexcept { + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(MS_PER_MIN))); + } + +//----------------------------------------------------------------------------// +// Minutes -> Seconds +//----------------------------------------------------------------------------// +# ifndef TIME_SHIELD_CPP17 + /// \brief Helper function for converting minutes to seconds (floating-point version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in minutes. + /// \param tag std::true_type indicates a floating-point type (double or float). + /// \return ts_t Timestamp in seconds. + template + constexpr ts_t min_to_sec_impl(T t, std::true_type) noexcept { + return static_cast(std::round(t * static_cast(SEC_PER_MIN))); + } + + /// \brief Helper function for converting minutes to seconds (integral version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in minutes. + /// \param tag std::false_type indicates a non-floating-point type. + /// \return ts_t Timestamp in seconds. + template + constexpr ts_t min_to_sec_impl(T t, std::false_type) noexcept { + return static_cast(t) * static_cast(SEC_PER_MIN); + } +# endif // TIME_SHIELD_CPP17 + + /// \brief Converts a timestamp from minutes to seconds. + /// \tparam T1 The type of the output timestamp (default is ts_t). + /// \tparam T2 The type of the input timestamp. + /// \param ts Timestamp in minutes. + /// \return T1 Timestamp in seconds. + template + constexpr T1 min_to_sec(T2 ts) noexcept { +# ifdef TIME_SHIELD_CPP17 + if constexpr (std::is_floating_point_v) { + return static_cast(std::round(ts * static_cast(SEC_PER_MIN))); + } else { + return static_cast(ts) * static_cast(SEC_PER_MIN); + } +# else + return static_cast(min_to_sec_impl(ts, typename std::conditional< + (std::is_same::value || std::is_same::value), + std::true_type, + std::false_type + >::type{})); +# endif + } + + /// \brief Converts a timestamp from seconds to minutes. + /// \tparam T1 The type of the output timestamp (default is int). + /// \tparam T2 The type of the input timestamp (default is ts_t). + /// \param ts Timestamp in seconds. + /// \return T1 Timestamp in minutes. + template + constexpr T1 sec_to_min(T2 ts) noexcept { + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(SEC_PER_MIN))); + } + + /// \brief Converts a timestamp from minutes to floating-point seconds. + /// \tparam T The type of the input timestamp (default is int). + /// \param min Timestamp in minutes. + /// \return fts_t Timestamp in floating-point seconds. + template + constexpr fts_t min_to_fsec(T min) noexcept { + return static_cast(min) * static_cast(SEC_PER_MIN); + } + + /// \brief Converts a timestamp from seconds to floating-point minutes. + /// \tparam T The type of the input timestamp (default is ts_t). + /// \param ts Timestamp in seconds. + /// \return double Timestamp in floating-point minutes. + template + constexpr double sec_to_fmin(T ts) noexcept { + return static_cast(ts) / static_cast(SEC_PER_MIN); + } + +//----------------------------------------------------------------------------// +// Hours -> Milliseconds +//----------------------------------------------------------------------------// + +# ifndef TIME_SHIELD_CPP17 + /// \brief Helper function for converting hours to milliseconds (floating-point version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in hours. + /// \param tag std::true_type indicates a floating-point type (double or float). + /// \return ts_ms_t Timestamp in milliseconds. + template + constexpr ts_ms_t hour_to_ms_impl(T t, std::true_type) noexcept { + return static_cast(std::round(t * static_cast(MS_PER_HOUR))); + } + + /// \brief Helper function for converting hours to milliseconds (integral version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in hours. + /// \param tag Type tag used to select the integral overload (must be std::false_type). + /// \return ts_ms_t Timestamp in milliseconds. + template + constexpr ts_ms_t hour_to_ms_impl(T t, std::false_type) noexcept { + return static_cast(t) * static_cast(MS_PER_HOUR); + } +# endif // TIME_SHIELD_CPP17 + + /// \brief Converts a timestamp from hours to milliseconds. + /// \tparam T1 The type of the output timestamp (default is ts_ms_t). + /// \tparam T2 The type of the input timestamp. + /// \param ts Timestamp in hours. + /// \return T1 Timestamp in milliseconds. + template + constexpr T1 hour_to_ms(T2 ts) noexcept { +# ifdef TIME_SHIELD_CPP17 + if constexpr (std::is_floating_point_v) { + return static_cast(std::round(ts * static_cast(MS_PER_HOUR))); + } else { + return static_cast(ts) * static_cast(MS_PER_HOUR); + } +# else + return static_cast(hour_to_ms_impl(ts, typename std::conditional< + (std::is_same::value || std::is_same::value), + std::true_type, + std::false_type + >::type{})); +# endif + } + + /// \brief Converts a timestamp from milliseconds to hours. + /// \tparam T1 The type of the output timestamp (default is int). + /// \tparam T2 The type of the input timestamp (default is ts_ms_t). + /// \param ts Timestamp in milliseconds. + /// \return T1 Timestamp in hours. + template + constexpr T1 ms_to_hour(T2 ts) noexcept { + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(MS_PER_HOUR))); + } + +//----------------------------------------------------------------------------// +// Hours -> Seconds +//----------------------------------------------------------------------------// + +# ifndef TIME_SHIELD_CPP17 + /// \brief Helper function for converting hours to seconds (floating-point version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in hours. + /// \param tag std::true_type indicates a floating-point type (double or float). + /// \return ts_t Timestamp in seconds. + template + constexpr ts_t hour_to_sec_impl(T t, std::true_type) noexcept { + return static_cast(std::round(t * static_cast(SEC_PER_HOUR))); + } + + /// \brief Helper function for converting hours to seconds (integral version). + /// \tparam T Type of the input timestamp. + /// \param t Timestamp in hours. + /// \param tag std::false_type indicates a non-floating-point type. + /// \return ts_t Timestamp in seconds. + template + constexpr ts_t hour_to_sec_impl(T t, std::false_type) noexcept { + return static_cast(t) * static_cast(SEC_PER_HOUR); + } +# endif // TIME_SHIELD_CPP17 + + /// \brief Converts a timestamp from hours to seconds. + /// \tparam T1 The type of the output timestamp (default is ts_t). + /// \tparam T2 The type of the input timestamp. + /// \param ts Timestamp in hours. + /// \return T1 Timestamp in seconds. + template + constexpr T1 hour_to_sec(T2 ts) noexcept { +# ifdef TIME_SHIELD_CPP17 + if constexpr (std::is_floating_point_v) { + return static_cast(std::round(ts * static_cast(SEC_PER_HOUR))); + } else { + return static_cast(ts) * static_cast(SEC_PER_HOUR); + } +# else + return static_cast(hour_to_sec_impl(ts, typename std::conditional< + (std::is_same::value || std::is_same::value), + std::true_type, + std::false_type + >::type{})); +# endif + } + + /// \brief Converts a timestamp from seconds to hours. + /// \tparam T1 The type of the output timestamp (default is int). + /// \tparam T2 The type of the input timestamp (default is ts_t). + /// \param ts Timestamp in seconds. + /// \return T1 Timestamp in hours. + template + constexpr T1 sec_to_hour(T2 ts) noexcept { + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(SEC_PER_HOUR))); + } + + /// \brief Converts a timestamp from hours to floating-point seconds. + /// \tparam T The type of the input timestamp (default is int). + /// \param hr Timestamp in hours. + /// \return fts_t Timestamp in floating-point seconds. + template + constexpr fts_t hour_to_fsec(T hr) noexcept { + return static_cast(hr) * static_cast(SEC_PER_HOUR); + } + + /// \brief Converts a timestamp from seconds to floating-point hours. + /// \tparam T The type of the input timestamp (default is ts_t). + /// \param ts Timestamp in seconds. + /// \return double Timestamp in floating-point hours. + template + constexpr double sec_to_fhour(T ts) noexcept { + return static_cast(ts) / static_cast(SEC_PER_HOUR); + } + + /// \brief Converts a 24-hour format hour to a 12-hour format. + /// \tparam T Numeric type of the hour (default is int). + /// \param hour The hour in 24-hour format to convert. + /// \return The hour in 12-hour format. + template + TIME_SHIELD_CONSTEXPR inline T hour24_to_12(T hour) noexcept { + if (hour == 0 || hour > 12) return 12; + return hour; + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_TIME_UNIT_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/time_utils.hpp b/include/time_shield/time_utils.hpp index 5adc6278..7260ee39 100644 --- a/include/time_shield/time_utils.hpp +++ b/include/time_shield/time_utils.hpp @@ -48,49 +48,94 @@ namespace time_shield { } /// \ingroup time_utils - /// \brief Get current real time in microseconds using a hybrid method. + /// \brief Get current real time in microseconds using a platform-specific method. /// - /// This function combines `QueryPerformanceCounter` (high-resolution monotonic clock) - /// with `GetSystemTimeAsFileTime` to compute an accurate, stable UTC timestamp. - /// The base time is initialized only once per process (lazy init). + /// On Windows this function combines `QueryPerformanceCounter` + /// (high-resolution monotonic clock) with `GetSystemTimeAsFileTime` to compute an accurate, + /// stable UTC timestamp. The base time is initialized only once per process (lazy init). + /// On Unix-like systems a realtime anchor is captured once and combined with a + /// high-resolution monotonic clock to compute stable timestamps. /// /// \return Current UTC timestamp in microseconds. - /// \note Windows only. Not available on Unix-like systems. inline int64_t now_realtime_us() { -# if TIME_SHIELD_PLATFORM_WINDOWS +# if TIME_SHIELD_PLATFORM_WINDOWS static std::once_flag init_flag; static int64_t s_perf_freq = 0; static int64_t s_anchor_perf = 0; static int64_t s_anchor_realtime_us = 0; - std::call_once(init_flag, [] { - LARGE_INTEGER freq, counter; - QueryPerformanceFrequency(&freq); - QueryPerformanceCounter(&counter); + std::call_once(init_flag, []() { + LARGE_INTEGER freq = {}; + LARGE_INTEGER counter = {}; + ::QueryPerformanceFrequency(&freq); + ::QueryPerformanceCounter(&counter); - s_perf_freq = freq.QuadPart; - s_anchor_perf = counter.QuadPart; + s_perf_freq = static_cast(freq.QuadPart); + s_anchor_perf = static_cast(counter.QuadPart); FILETIME ft; - GetSystemTimeAsFileTime(&ft); + ::GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER uli; - uli.LowPart = ft.dwLowDateTime; + uli.LowPart = ft.dwLowDateTime; uli.HighPart = ft.dwHighDateTime; - // Convert from 100ns since 1601 to microseconds since 1970 - s_anchor_realtime_us = (uli.QuadPart - 116444736000000000ULL) / 10; + + // 100ns ticks since 1601-01-01 to 1970-01-01 (signed constant!) + const int64_t k_epoch_diff_100ns = 116444736000000000LL; + + const int64_t filetime_100ns = static_cast(uli.QuadPart); + // Convert 100ns since 1601 -> us since 1970 + s_anchor_realtime_us = (filetime_100ns - k_epoch_diff_100ns) / 10; }); - LARGE_INTEGER now; - QueryPerformanceCounter(&now); + LARGE_INTEGER now = {}; + ::QueryPerformanceCounter(&now); + + const int64_t now_ticks = static_cast(now.QuadPart); + const int64_t delta_ticks = now_ticks - s_anchor_perf; + + // Avoid overflow of (delta_ticks * 1000000) + const int64_t q = delta_ticks / s_perf_freq; + const int64_t r = delta_ticks % s_perf_freq; - int64_t delta_ticks = now.QuadPart - s_anchor_perf; - int64_t delta_us = (delta_ticks * 1000000) / s_perf_freq; + const int64_t delta_us = + q * 1000000LL + (r * 1000000LL) / s_perf_freq; return s_anchor_realtime_us + delta_us; -# else - // Stub implementation for non-Windows platforms. - return 0; -# endif +# else + static std::once_flag init_flag; + static int64_t s_anchor_realtime_us = 0; + static int64_t s_anchor_mono_ns = 0; + + std::call_once(init_flag, []() { + struct timespec realtime_ts{}; + struct timespec mono_ts{}; + +# if defined(CLOCK_MONOTONIC_RAW) + clock_gettime(CLOCK_MONOTONIC_RAW, &mono_ts); +# else + clock_gettime(CLOCK_MONOTONIC, &mono_ts); +# endif + clock_gettime(CLOCK_REALTIME, &realtime_ts); + + s_anchor_realtime_us = static_cast(realtime_ts.tv_sec) * 1000000LL + + realtime_ts.tv_nsec / 1000; + s_anchor_mono_ns = static_cast(mono_ts.tv_sec) * 1000000000LL + + mono_ts.tv_nsec; + }); + + struct timespec mono_now_ts{}; +# if defined(CLOCK_MONOTONIC_RAW) + clock_gettime(CLOCK_MONOTONIC_RAW, &mono_now_ts); +# else + clock_gettime(CLOCK_MONOTONIC, &mono_now_ts); +# endif + + const int64_t mono_now_ns = static_cast(mono_now_ts.tv_sec) * 1000000000LL + + mono_now_ts.tv_nsec; + const int64_t delta_ns = mono_now_ns - s_anchor_mono_ns; + return s_anchor_realtime_us + delta_ns / 1000; +# endif } /// \ingroup time_utils @@ -108,9 +153,9 @@ namespace time_shield { /// \tparam T Type of the returned value (default is int). /// \return T Microsecond part of the current second. template - T us_of_sec() noexcept { + inline T us_of_sec() noexcept { const struct timespec ts = get_timespec_impl(); - return ts.tv_nsec / NS_PER_US; + return static_cast(ts.tv_nsec / NS_PER_US); } /// \ingroup time_utils @@ -118,9 +163,9 @@ namespace time_shield { /// \tparam T Type of the returned value (default is int). /// \return T Millisecond part of the current second. template - T ms_of_sec() noexcept { + inline T ms_of_sec() noexcept { const struct timespec ts = get_timespec_impl(); - return ts.tv_nsec / NS_PER_MS; + return static_cast(ts.tv_nsec / NS_PER_MS); } /// \brief Get the current UTC timestamp in seconds. @@ -143,7 +188,7 @@ namespace time_shield { /// \return fts_t Current UTC timestamp in floating-point seconds. inline fts_t fts() noexcept { const struct timespec ts = get_timespec_impl(); - return ts.tv_sec + static_cast(ts.tv_nsec) / static_cast(NS_PER_SEC); + return static_cast(ts.tv_sec) + static_cast(ts.tv_nsec) / static_cast(NS_PER_SEC); } /// \ingroup time_utils @@ -151,7 +196,7 @@ namespace time_shield { /// \return fts_t Current UTC timestamp in floating-point seconds. inline fts_t ftimestamp() noexcept { const struct timespec ts = get_timespec_impl(); - return ts.tv_sec + static_cast(ts.tv_nsec) / static_cast(NS_PER_SEC); + return static_cast(ts.tv_sec) + static_cast(ts.tv_nsec) / static_cast(NS_PER_SEC); } /// \ingroup time_utils diff --git a/include/time_shield/time_zone_conversions.hpp b/include/time_shield/time_zone_conversions.hpp index c1d06e7c..51295af2 100644 --- a/include/time_shield/time_zone_conversions.hpp +++ b/include/time_shield/time_zone_conversions.hpp @@ -4,7 +4,7 @@ #define _TIME_SHIELD_TIME_ZONE_CONVERSIONS_HPP_INCLUDED /// \file time_zone_conversions.hpp -/// \brief Helpers for converting CET/EET timestamps to GMT. +/// \brief Helpers for converting CET/EET/ET timestamps to GMT (UTC). /// \ingroup time_zone_conversions #include "date_time_struct.hpp" @@ -95,6 +95,112 @@ namespace time_shield { return cet_to_gmt(eet - SEC_PER_HOUR); } + /// \brief Check if local US Eastern time uses DST. + /// \param dt Local time in ET. + /// \return True if DST applies for the provided local timestamp. + inline bool is_us_eastern_dst_local(const DateTimeStruct& dt) { + const int SWITCH_HOUR = 2; + int start_day = 0; + int end_day = 0; + int start_month = 0; + int end_month = 0; + + if(dt.year >= 2007) { + start_month = MAR; + end_month = NOV; + int first_sunday_march = static_cast( + 1 + (DAYS_PER_WEEK - day_of_week_date(dt.year, MAR, 1)) % DAYS_PER_WEEK); + start_day = first_sunday_march + 7; + end_day = static_cast( + 1 + (DAYS_PER_WEEK - day_of_week_date(dt.year, NOV, 1)) % DAYS_PER_WEEK); + } else { + start_month = APR; + end_month = OCT; + start_day = static_cast( + 1 + (DAYS_PER_WEEK - day_of_week_date(dt.year, APR, 1)) % DAYS_PER_WEEK); + end_day = last_sunday_month_day(dt.year, OCT); + } + + if(dt.mon > start_month && dt.mon < end_month) { + return true; + } + if(dt.mon < start_month || dt.mon > end_month) { + return false; + } + if(dt.mon == start_month) { + if(dt.day > start_day) { + return true; + } + if(dt.day < start_day) { + return false; + } + return dt.hour >= SWITCH_HOUR; + } + if(dt.mon == end_month) { + if(dt.day < end_day) { + return true; + } + if(dt.day > end_day) { + return false; + } + return dt.hour < SWITCH_HOUR; + } + return false; + } + + /// \brief Convert US Eastern Time (New York, EST/EDT) to GMT (UTC). + /// \param et Timestamp in seconds in ET. + /// \return Timestamp in seconds in GMT (UTC). + /// + /// GMT in this library uses UTC. DST rules are guaranteed for 1987+; + /// earlier years use 1987-2006 rules as a best-effort approximation. + inline ts_t et_to_gmt(ts_t et) { + DateTimeStruct dt = to_date_time(et); + bool is_dst = is_us_eastern_dst_local(dt); + return et + SEC_PER_HOUR * (is_dst ? 4 : 5); + } + + /// \brief Convert GMT (UTC) to US Eastern Time (New York, EST/EDT). + /// \param gmt Timestamp in seconds in GMT (UTC). + /// \return Timestamp in seconds in ET. + /// + /// GMT in this library uses UTC. DST rules are guaranteed for 1987+; + /// earlier years use 1987-2006 rules as a best-effort approximation. + inline ts_t gmt_to_et(ts_t gmt) { + ts_t et_standard = gmt - SEC_PER_HOUR * 5; + DateTimeStruct dt_local = to_date_time(et_standard); + bool is_dst = is_us_eastern_dst_local(dt_local); + return gmt - SEC_PER_HOUR * (is_dst ? 4 : 5); + } + + /// \brief Convert New York Time to GMT (UTC). + /// \param ny Timestamp in seconds in ET. + /// \return Timestamp in seconds in GMT (UTC). + inline ts_t ny_to_gmt(ts_t ny) { + return et_to_gmt(ny); + } + + /// \brief Convert GMT (UTC) to New York Time. + /// \param gmt Timestamp in seconds in GMT (UTC). + /// \return Timestamp in seconds in ET. + inline ts_t gmt_to_ny(ts_t gmt) { + return gmt_to_et(gmt); + } + + /// \brief Convert US Central Time (America/Chicago, CST/CDT) to GMT (UTC). + /// \param ct Timestamp in seconds in CT. + /// \return Timestamp in seconds in GMT (UTC). + inline ts_t ct_to_gmt(ts_t ct) { + return et_to_gmt(ct + SEC_PER_HOUR); + } + + /// \brief Convert GMT (UTC) to US Central Time (America/Chicago, CST/CDT). + /// \param gmt Timestamp in seconds in GMT (UTC). + /// \return Timestamp in seconds in CT. + inline ts_t gmt_to_ct(ts_t gmt) { + return gmt_to_et(gmt) - SEC_PER_HOUR; + } + /// \brief Convert Greenwich Mean Time to Central European Time. /// \param gmt Timestamp in seconds in GMT. /// \return Timestamp in seconds in CET. diff --git a/include/time_shield/time_zone_offset.hpp b/include/time_shield/time_zone_offset.hpp new file mode 100644 index 00000000..82db917c --- /dev/null +++ b/include/time_shield/time_zone_offset.hpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_TIME_ZONE_OFFSET_HPP_INCLUDED +#define _TIME_SHIELD_TIME_ZONE_OFFSET_HPP_INCLUDED + +/// \file time_zone_offset.hpp +/// \brief UTC offset arithmetic helpers (UTC <-> local) and TimeZoneStruct offset extraction. +/// \ingroup time_zone_conversions +/// +/// This header provides simple, allocation-free conversions between: +/// - UTC timestamp and local timestamp using a numeric UTC offset (in seconds). +/// - UTC milliseconds and local milliseconds using the same offset. +/// +/// \note The offset is interpreted as an UTC offset in seconds, i.e.: +/// local = utc + utc_offset +/// utc = local - utc_offset +/// +/// \note If the input equals ERROR_TIMESTAMP, the functions return ERROR_TIMESTAMP unchanged. + +#include "config.hpp" +#include "types.hpp" +#include "time_conversions.hpp" +#include "time_zone_struct.hpp" + +namespace time_shield { + + /// \ingroup time_conversions_time_zone_conversions + /// \{ + + /// \brief Convert local timestamp (seconds) to UTC using UTC offset. + /// \param local Local timestamp in seconds. + /// \param utc_offset UTC offset in seconds (e.g. CET=+3600, MSK=+10800, EST=-18000). + /// \return UTC timestamp in seconds. If \p local equals ERROR_TIMESTAMP, returns ERROR_TIMESTAMP. + TIME_SHIELD_CONSTEXPR inline ts_t to_utc(ts_t local, tz_t utc_offset) noexcept { + return local == ERROR_TIMESTAMP ? ERROR_TIMESTAMP + : local - static_cast(utc_offset); + } + + /// \brief Convert UTC timestamp (seconds) to local time using UTC offset. + /// \param utc UTC timestamp in seconds. + /// \param utc_offset UTC offset in seconds (e.g. CET=+3600, MSK=+10800, EST=-18000). + /// \return Local timestamp in seconds. If \p utc equals ERROR_TIMESTAMP, returns ERROR_TIMESTAMP. + TIME_SHIELD_CONSTEXPR inline ts_t to_local(ts_t utc, tz_t utc_offset) noexcept { + return utc == ERROR_TIMESTAMP ? ERROR_TIMESTAMP + : utc + static_cast(utc_offset); + } + + /// \brief Convert local timestamp (milliseconds) to UTC using UTC offset. + /// \param local_ms Local timestamp in milliseconds. + /// \param utc_offset UTC offset in seconds (will be converted to milliseconds). + /// \return UTC timestamp in milliseconds. If \p local_ms equals ERROR_TIMESTAMP, returns ERROR_TIMESTAMP. + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_utc_ms(ts_ms_t local_ms, tz_t utc_offset) noexcept { + return local_ms == ERROR_TIMESTAMP ? ERROR_TIMESTAMP + : local_ms - sec_to_ms(utc_offset); + } + + /// \brief Convert UTC timestamp (milliseconds) to local time using UTC offset. + /// \param utc_ms UTC timestamp in milliseconds. + /// \param utc_offset UTC offset in seconds (will be converted to milliseconds). + /// \return Local timestamp in milliseconds. If \p utc_ms equals ERROR_TIMESTAMP, returns ERROR_TIMESTAMP. + TIME_SHIELD_CONSTEXPR inline ts_ms_t to_local_ms(ts_ms_t utc_ms, tz_t utc_offset) noexcept { + return utc_ms == ERROR_TIMESTAMP ? ERROR_TIMESTAMP + : utc_ms + sec_to_ms(utc_offset); + } + + /// \brief Extract numeric UTC offset (in seconds) from TimeZoneStruct. + /// \param tz Time zone descriptor. + /// \return UTC offset in seconds (local = utc + offset). + TIME_SHIELD_CONSTEXPR inline tz_t utc_offset_of(const TimeZoneStruct& tz) noexcept { + return time_zone_struct_to_offset(tz); + } + + /// \} + +} // namespace time_shield + +#endif diff --git a/include/time_shield/time_zone_offset_conversions.hpp b/include/time_shield/time_zone_offset_conversions.hpp new file mode 100644 index 00000000..116b947b --- /dev/null +++ b/include/time_shield/time_zone_offset_conversions.hpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_TIME_ZONE_OFFSET_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_TIME_ZONE_OFFSET_CONVERSIONS_HPP_INCLUDED + +/// \file time_zone_offset_conversions.hpp +/// \brief Conversions between numeric offsets and TimeZoneStruct. + +#include "config.hpp" +#include "constants.hpp" +#include "time_zone_struct.hpp" +#include "types.hpp" + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + /// \brief Converts an integer to a time zone structure. + /// \tparam T The type of the time zone structure (default is TimeZoneStruct). + /// \param offset The integer to convert. + /// \return A time zone structure of type T represented by the given integer. + /// \details The function assumes that the type T has members `hour`, `min`, and `is_positive`. + template + inline T to_time_zone(tz_t offset) { + const int64_t off = static_cast(offset); + const int64_t abs_val = (off < 0) ? -off : off; + + T tz; + tz.hour = static_cast(abs_val / static_cast(SEC_PER_HOUR)); + tz.min = static_cast( + (abs_val % static_cast(SEC_PER_HOUR)) / static_cast(SEC_PER_MIN) + ); + tz.is_positive = (off >= 0); + return tz; + } + + /// \brief Convert time zone struct to offset in seconds. + /// \details Expects fields: hour, min, is_positive. + template + TIME_SHIELD_CONSTEXPR inline tz_t to_tz_offset(const T& tz) noexcept { + const int sign = tz.is_positive ? 1 : -1; + const int64_t sec = static_cast(tz.hour) * SEC_PER_HOUR + + static_cast(tz.min) * SEC_PER_MIN; + return static_cast(sign * sec); + } + + /// \brief Build offset in seconds from hours/minutes. + /// \param hour Signed hours (e.g. -3, +5). + /// \param min Minutes (0..59). + TIME_SHIELD_CONSTEXPR inline tz_t tz_offset_hm(int hour, int min = 0) noexcept { + const int sign = (hour < 0) ? -1 : 1; + const int64_t ah = (hour < 0) ? -static_cast(hour) : static_cast(hour); + const int64_t am = (min < 0) ? -static_cast(min) : static_cast(min); + return static_cast(sign * (ah * SEC_PER_HOUR + am * SEC_PER_MIN)); + } + + /// \brief Check if a numeric offset is within supported bounds. + /// \details Conservative range: [-12:00, +14:00]. + TIME_SHIELD_CONSTEXPR inline bool is_valid_tz_offset(tz_t off) noexcept { + // conservative range: [-12:00, +14:00] + return off % 60 == 0 + && off >= -12 * SEC_PER_HOUR + && off <= 14 * SEC_PER_HOUR; + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_TIME_ZONE_OFFSET_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/time_zone_struct.hpp b/include/time_shield/time_zone_struct.hpp index d50974ee..c685209b 100644 --- a/include/time_shield/time_zone_struct.hpp +++ b/include/time_shield/time_zone_struct.hpp @@ -6,6 +6,7 @@ /// \file time_zone_struct.hpp /// \brief Header for time zone structure and related functions. +#include "config.hpp" #include "types.hpp" #include "constants.hpp" @@ -44,13 +45,17 @@ namespace time_shield { /// \param offset The integer to convert. /// \return A TimeZoneStruct represented by the given integer. inline TimeZoneStruct to_time_zone_struct(tz_t offset) { - int abs_val = std::abs(offset); - int hour = abs_val / SEC_PER_HOUR; - int min = (abs_val % SEC_PER_HOUR) / SEC_PER_MIN; - bool is_positive = (offset >= 0); - return TimeZoneStruct{hour, min, is_positive}; + const int64_t off = static_cast(offset); + const int64_t abs_val = (off < 0) ? -off : off; + + const int hour = static_cast(abs_val / static_cast(SEC_PER_HOUR)); + const int min = static_cast((abs_val % static_cast(SEC_PER_HOUR)) / + static_cast(SEC_PER_MIN)); + + return TimeZoneStruct{hour, min, off >= 0}; } + /// \ingroup time_structures_time_conversions /// \brief Alias for to_time_zone_struct function. /// \copydoc to_time_zone_struct @@ -86,24 +91,28 @@ namespace time_shield { //------------------------------------------------------------------------------ /// \ingroup time_structures_time_conversions - /// \brief Converts a TimeZoneStruct to a single integer representation. - /// \param tz The TimeZoneStruct to convert. - /// \return An integer representing the TimeZoneStruct. - inline tz_t time_zone_struct_to_offset(const TimeZoneStruct& tz) { - return (tz.is_positive ? 1 : -1) * (tz.hour * SEC_PER_HOUR + tz.min * SEC_PER_MIN); + /// \brief Convert a TimeZoneStruct to a numeric UTC offset (seconds). + /// \param tz Time zone descriptor. + /// \return UTC offset in seconds (local = utc + offset). + TIME_SHIELD_CONSTEXPR inline tz_t time_zone_struct_to_offset(const TimeZoneStruct& tz) noexcept { + return tz.is_positive + ? static_cast( static_cast(tz.hour) * static_cast(SEC_PER_HOUR) + + static_cast(tz.min) * static_cast(SEC_PER_MIN) ) + : static_cast(-( static_cast(tz.hour) * static_cast(SEC_PER_HOUR) + + static_cast(tz.min) * static_cast(SEC_PER_MIN) )); } /// \ingroup time_structures_time_conversions - /// \brief Alias for time_zone_struct_to_offset function. + /// \brief Alias for time_zone_struct_to_offset. /// \copydoc time_zone_struct_to_offset - inline tz_t tz_to_offset(const TimeZoneStruct& tz) { + TIME_SHIELD_CONSTEXPR inline tz_t tz_to_offset(const TimeZoneStruct& tz) noexcept { return time_zone_struct_to_offset(tz); } /// \ingroup time_structures_time_conversions - /// \brief Alias for time_zone_struct_to_offset function. + /// \brief Alias for time_zone_struct_to_offset. /// \copydoc time_zone_struct_to_offset - inline tz_t to_offset(const TimeZoneStruct& tz) { + TIME_SHIELD_CONSTEXPR inline tz_t to_offset(const TimeZoneStruct& tz) noexcept { return time_zone_struct_to_offset(tz); } diff --git a/include/time_shield/types.hpp b/include/time_shield/types.hpp index b8d26bdf..7ce88fea 100644 --- a/include/time_shield/types.hpp +++ b/include/time_shield/types.hpp @@ -25,7 +25,7 @@ namespace time_shield { /// - **Unix-based timestamps**: `ts_t`, `ts_ms_t`, `ts_us_t` /// - **Fractional and floating-point time**: `fts_t`, `oadate_t`, `jd_t` /// - **Julian date types**: `jd_t`, `mjd_t`, `jdn_t` -/// - **Utility units**: `year_t`, `uday_t`, `tz_t` +/// - **Utility units**: `year_t`, `dse_t`, `tz_t` /// /// ### Example Usage /// ```cpp @@ -39,8 +39,11 @@ namespace time_shield { // --- Calendar & Year Types --- typedef int64_t year_t; ///< Year as an integer (e.g., 2024). - typedef int64_t uday_t; ///< Unix day count since 1970‑01‑01 (days since epoch). - using unixday_t = uday_t; ///< Alias for Unix day count type. + typedef int64_t dse_t; ///< Unix day count since 1970‑01‑01 (days since epoch). + using unix_day_t = dse_t; ///< Alias for Unix day count type. + using unixday_t = dse_t; ///< Alias for Unix day count type. + typedef int32_t iso_week_t; ///< ISO week number type (1-52/53). + typedef int32_t iso_weekday_t; ///< ISO weekday number type (1=Monday .. 7=Sunday). // --- Unix Timestamp Types --- typedef int64_t ts_t; ///< Unix timestamp in seconds since 1970‑01‑01T00:00:00Z. diff --git a/include/time_shield/unix_time_conversions.hpp b/include/time_shield/unix_time_conversions.hpp new file mode 100644 index 00000000..2e0b51ad --- /dev/null +++ b/include/time_shield/unix_time_conversions.hpp @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_UNIX_TIME_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_UNIX_TIME_CONVERSIONS_HPP_INCLUDED + +/// \file unix_time_conversions.hpp +/// \brief Conversions related to UNIX-based time units and epochs. + +#include "config.hpp" +#include "constants.hpp" +#include "detail/fast_date.hpp" +#include "time_unit_conversions.hpp" +#include "time_utils.hpp" +#include "types.hpp" + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + namespace legacy { + + /// \brief Converts a UNIX timestamp to a year. + /// \tparam T The type of the year (default is year_t). + /// \param ts UNIX timestamp. + /// \return T Year corresponding to the given timestamp. + template + TIME_SHIELD_CONSTEXPR T years_since_epoch(ts_t ts) noexcept { + // 9223372029693630000 - значение на момент 292277024400 от 2000 года + // Такое значение приводит к неправильному вычислению умножения n_400_years * SEC_PER_400_YEARS + // Поэтому пришлось снизить до 9223371890843040000 + constexpr int64_t BIAS_292277022000 = 9223371890843040000LL; + constexpr int64_t BIAS_2000 = 946684800LL; + + int64_t y = MAX_YEAR; + int64_t secs = -((ts - BIAS_2000) - BIAS_292277022000); + + const int64_t n_400_years = secs / SEC_PER_400_YEARS; + secs -= n_400_years * SEC_PER_400_YEARS; + y -= n_400_years * 400; + + const int64_t n_100_years = secs / SEC_PER_100_YEARS; + secs -= n_100_years * SEC_PER_100_YEARS; + y -= n_100_years * 100; + + const int64_t n_4_years = secs / SEC_PER_4_YEARS; + secs -= n_4_years * SEC_PER_4_YEARS; + y -= n_4_years * 4; + + const int64_t n_1_years = secs / SEC_PER_YEAR; + secs -= n_1_years * SEC_PER_YEAR; + y -= n_1_years; + + y = secs == 0 ? y : y - 1; + return y - UNIX_EPOCH; + } + + } // namespace legacy + + /// \brief Converts a UNIX timestamp to a year. + /// \tparam T The type of the year (default is year_t). + /// \param ts UNIX timestamp. + /// \return T Year corresponding to the given timestamp. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + template + TIME_SHIELD_CONSTEXPR T years_since_epoch(ts_t ts) noexcept { + const detail::DaySplit split = detail::split_unix_day(ts); + const int64_t year = detail::fast_year_from_days_constexpr(split.days); + return static_cast(year - UNIX_EPOCH); + } + + namespace legacy { + + /// \brief Convert a calendar date to UNIX day count. + /// + /// Calculates the number of days since the UNIX epoch (January 1, 1970) + /// for the provided calendar date components. + /// + /// \tparam Year Type of the year component. + /// \tparam Month Type of the month component. + /// \tparam Day Type of the day component. + /// \param year Year component of the date. + /// \param month Month component of the date. + /// \param day Day component of the date. + /// \return Number of days since the UNIX epoch. + template + TIME_SHIELD_CONSTEXPR inline dse_t date_to_unix_day( + Year year, + Month month, + Day day) noexcept { + const int64_t y = static_cast(year) - (static_cast(month) <= 2 ? 1 : 0); + const int64_t m = static_cast(month) <= 2 + ? static_cast(month) + 9 + : static_cast(month) - 3; + const int64_t era = (y >= 0 ? y : y - 399) / 400; + const int64_t yoe = y - era * 400; + const int64_t doy = (153 * m + 2) / 5 + static_cast(day) - 1; + const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era * 146097 + doe - 719468); + } + + } // namespace legacy + + /// \brief Convert a calendar date to UNIX day count. + /// + /// Calculates the number of days since the UNIX epoch (January 1, 1970) + /// for the provided calendar date components. + /// \note Inspired by the algorithm described in: + /// https://www.benjoffe.com/fast-date-64 + /// This implementation is written from scratch (no code copied). + /// + /// \tparam Year Type of the year component. + /// \tparam Month Type of the month component. + /// \tparam Day Type of the day component. + /// \param year Year component of the date. + /// \param month Month component of the date. + /// \param day Day component of the date. + /// \return Number of days since the UNIX epoch. + template + TIME_SHIELD_CONSTEXPR inline dse_t date_to_unix_day( + Year year, + Month month, + Day day) noexcept { + return static_cast( + detail::fast_days_from_date_constexpr( + static_cast(year), + static_cast(month), + static_cast(day))); + } + + /// \brief Get UNIX day. + /// + /// This function returns the number of days elapsed since the UNIX epoch. + /// + /// \tparam T The return type of the function (default is unixday_t). + /// \param ts Timestamp in seconds (default is current timestamp). + /// \return Number of days since the UNIX epoch. + template + constexpr T days_since_epoch(ts_t ts = time_shield::ts()) noexcept { + return ts / SEC_PER_DAY; + } + + /// \brief Get UNIX day from milliseconds timestamp. + /// + /// This function returns the number of days elapsed since the UNIX epoch, given a timestamp in milliseconds. + /// + /// \tparam T The return type of the function (default is unixday_t). + /// \param t_ms Timestamp in milliseconds (default is current timestamp in milliseconds). + /// \return Number of days since the UNIX epoch. + template + constexpr T days_since_epoch_ms(ts_ms_t t_ms = time_shield::ts_ms()) noexcept { + return days_since_epoch(ms_to_sec(t_ms)); + } + + /// \brief Get the number of days between two timestamps. + /// + /// This function calculates the number of days between two timestamps. + /// + /// \tparam T The type of the return value, defaults to int. + /// \param start The timestamp of the start of the period. + /// \param stop The timestamp of the end of the period. + /// \return The number of days between start and stop. + template + constexpr T days_between(ts_t start, ts_t stop) noexcept { + return static_cast((stop - start) / SEC_PER_DAY); + } + + /// \brief Converts a UNIX day to a timestamp in seconds. + /// + /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding + /// timestamp in seconds at the start of the specified day. + /// + /// \tparam T The return type of the function (default is ts_t). + /// \param unix_day Number of days since the UNIX epoch. + /// \return The timestamp in seconds representing the beginning of the specified UNIX day. + template + constexpr T unix_day_to_ts(dse_t unix_day) noexcept { + return unix_day * SEC_PER_DAY; + } + + /// \brief Converts a UNIX day to a timestamp in milliseconds. + /// + /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding timestamp + /// in milliseconds at the start of the specified day. + /// + /// \tparam T The return type of the function (default is ts_t). + /// \param unix_day Number of days since the UNIX epoch. + /// \return The timestamp in milliseconds representing the beginning of the specified UNIX day. + template + constexpr T unix_day_to_ts_ms(dse_t unix_day) noexcept { + return unix_day * MS_PER_DAY; + } + + /// \brief Converts a UNIX day to a timestamp representing the end of the day in seconds. + /// + /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding + /// timestamp in seconds at the end of the specified day (23:59:59). + /// + /// \tparam T The return type of the function (default is ts_t). + /// \param unix_day The number of days since the UNIX epoch. + /// \return The timestamp in seconds representing the end of the specified UNIX day. + template + constexpr T end_of_day_from_unix_day(dse_t unix_day) noexcept { + return unix_day * SEC_PER_DAY + SEC_PER_DAY - 1; + } + + /// \brief Converts a UNIX day to a timestamp representing the end of the day in milliseconds. + /// + /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding + /// timestamp in milliseconds at the end of the specified day (23:59:59.999). + /// + /// \tparam T The return type of the function (default is ts_ms_t). + /// \param unix_day The number of days since the UNIX epoch. + /// \return The timestamp in milliseconds representing the end of the specified UNIX day. + template + constexpr T end_of_day_from_unix_day_ms(dse_t unix_day) noexcept { + return unix_day * MS_PER_DAY + MS_PER_DAY - 1; + } + + /// \brief Converts a UNIX day to a timestamp representing the start of the next day in seconds. + /// + /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding + /// timestamp in seconds at the start of the next day (00:00:00). + /// + /// \tparam T The return type of the function (default is ts_t). + /// \param unix_day The number of days since the UNIX epoch. + /// \return The timestamp in seconds representing the beginning of the next UNIX day. + template + constexpr T start_of_next_day_from_unix_day(dse_t unix_day) noexcept { + return unix_day * SEC_PER_DAY + SEC_PER_DAY; + } + + /// \brief Converts a UNIX day to a timestamp representing the start of the next day in milliseconds. + /// + /// Converts a number of days since the UNIX epoch (January 1, 1970) to the corresponding + /// timestamp in milliseconds at the start of the next day (00:00:00.000). + /// + /// \tparam T The return type of the function (default is ts_ms_t). + /// \param unix_day The number of days since the UNIX epoch. + /// \return The timestamp in milliseconds representing the beginning of the next UNIX day. + template + constexpr T start_of_next_day_from_unix_day_ms(dse_t unix_day) noexcept { + return unix_day * MS_PER_DAY + MS_PER_DAY; + } + + /// \brief Get UNIX minute. + /// + /// This function returns the number of minutes elapsed since the UNIX epoch. + /// + /// \tparam T The return type of the function (default is int64_t). + /// \param ts Timestamp in seconds (default is current timestamp). + /// \return Number of minutes since the UNIX epoch. + template + constexpr T min_since_epoch(ts_t ts = time_shield::ts()) { + return ts / SEC_PER_MIN; + } + + /// \brief Get the second of the day. + /// + /// This function returns a value from 0 to MAX_SEC_PER_DAY representing the second of the day. + /// + /// \tparam T The return type of the function (default is int). + /// \param ts Timestamp in seconds (default is current timestamp). + /// \return Second of the day. + template + constexpr T sec_of_day(ts_t ts = time_shield::ts()) noexcept { + return static_cast(ts % SEC_PER_DAY); + } + + /// \brief Get the second of the day from milliseconds timestamp. + /// + /// This function returns a value from 0 to MAX_SEC_PER_DAY representing the second of the day, given a timestamp in milliseconds. + /// + /// \tparam T The return type of the function (default is int). + /// \param ts_ms Timestamp in milliseconds. + /// \return Second of the day. + template + constexpr T sec_of_day_ms(ts_ms_t ts_ms) noexcept { + return sec_of_day(ms_to_sec(ts_ms)); + } + + /// \brief Get the second of the day. + /// + /// This function returns a value between 0 and MAX_SEC_PER_DAY representing the second of the day, given the hour, minute, and second. + /// + /// \tparam T1 The return type of the function (default is int). + /// \tparam T2 The type of the hour, minute, and second parameters (default is int). + /// \param hour Hour of the day. + /// \param min Minute of the hour. + /// \param sec Second of the minute. + /// \return Second of the day. + template + constexpr T1 sec_of_day( + T2 hour, + T2 min, + T2 sec) noexcept { + return static_cast(hour) * static_cast(SEC_PER_HOUR) + + static_cast(min) * static_cast(SEC_PER_MIN) + + static_cast(sec); + } + + /// \brief Get the second of the minute. + /// + /// This function returns a value between 0 and 59 representing the second of the minute. + /// + /// \tparam T The return type of the function (default is int). + /// \param ts Timestamp in seconds (default is current timestamp). + /// \return Second of the minute. + template + constexpr T sec_of_min(ts_t ts = time_shield::ts()) { + return static_cast(ts % SEC_PER_MIN); + } + + /// \brief Get the second of the hour. + /// + /// This function returns a value between 0 and 3599 representing the second of the hour. + /// + /// \tparam T The return type of the function (default is int). + /// \param ts Timestamp in seconds (default is current timestamp). + /// \return Second of the hour. + template + constexpr T sec_of_hour(ts_t ts = time_shield::ts()) { + return static_cast(ts % SEC_PER_HOUR); + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_UNIX_TIME_CONVERSIONS_HPP_INCLUDED diff --git a/include/time_shield/validation.hpp b/include/time_shield/validation.hpp index 0fbe40bf..25747ef3 100644 --- a/include/time_shield/validation.hpp +++ b/include/time_shield/validation.hpp @@ -11,6 +11,7 @@ #include "config.hpp" #include "types.hpp" #include "constants.hpp" +#include "enums.hpp" #include "time_zone_struct.hpp" namespace time_shield { @@ -233,6 +234,7 @@ namespace time_shield { if (day > 31 && year <= 31) { return is_valid_date((T1)day, month, (T2)year); } + if (year < MIN_YEAR) return false; if (year > MAX_YEAR) return false; if (month < 1 || month > 12) return false; if (day < 1 || day > 31) return false; @@ -312,7 +314,10 @@ namespace time_shield { /// \param ts Timestamp to check (default: current timestamp). /// \return true if the day is a weekend day, false otherwise. TIME_SHIELD_CONSTEXPR inline bool is_day_off(ts_t ts) noexcept { - const int wd = ((ts / SEC_PER_DAY + THU) % DAYS_PER_WEEK); + const int64_t day = static_cast(ts) / static_cast(SEC_PER_DAY); + int64_t wd64 = (day + static_cast(THU)) % static_cast(DAYS_PER_WEEK); + if (wd64 < 0) wd64 += static_cast(DAYS_PER_WEEK); // for ts < 0 + const int wd = static_cast(wd64); return (wd == SUN || wd == SAT); } @@ -329,21 +334,64 @@ namespace time_shield { /// which is either Saturday or Sunday. /// \param unix_day Day to check (number of days since Unix epoch). /// \return true if the day is a weekend day, false otherwise. - template + template TIME_SHIELD_CONSTEXPR inline bool is_day_off_unix_day(T unix_day) noexcept { - const int wd = (unix_day + THU) % DAYS_PER_WEEK; + int64_t wd = (static_cast(unix_day) + THU) % DAYS_PER_WEEK; + wd += (wd < 0) ? DAYS_PER_WEEK : 0; return (wd == SUN || wd == SAT); } /// \brief Alias for is_day_off_unix_day function. /// \copydoc is_day_off_unix_day - template + template TIME_SHIELD_CONSTEXPR inline bool is_weekend_unix_day(T unix_day) noexcept { return is_day_off_unix_day(unix_day); } +//------------------------------------------------------------------------------ + + /// \brief Check if a given timestamp corresponds to a workday (Monday to Friday). + /// \param ts Timestamp to check. + /// \return true if the day is a workday, false otherwise. + TIME_SHIELD_CONSTEXPR inline bool is_workday(ts_t ts) noexcept { + return !is_day_off(ts); + } + + /// \brief Check if a given timestamp in milliseconds corresponds to a workday (Monday to Friday). + /// \param ts_ms Timestamp in milliseconds to check. + /// \return true if the day is a workday, false otherwise. + TIME_SHIELD_CONSTEXPR inline bool is_workday_ms(ts_ms_t ts_ms) noexcept { + return is_workday(static_cast(ts_ms / MS_PER_SEC)); + } + + /// \brief Check if a calendar date corresponds to a workday (Monday to Friday). + /// \param year Year component of the date. + /// \param month Month component of the date. + /// \param day Day component of the date. + /// \return true if the date is valid and a workday, false otherwise. + TIME_SHIELD_CONSTEXPR inline bool is_workday(year_t year, int month, int day) noexcept { + const auto y = static_cast(year); + const auto m = static_cast(month); + const auto d = static_cast(day); + if (!is_valid_date(y, m, d)) { + return false; + } + + const int64_t adj_y = static_cast(y) - (static_cast(m) <= 2 ? 1 : 0); + const int64_t adj_m = static_cast(m) <= 2 + ? static_cast(m) + 9 + : static_cast(m) - 3; + const int64_t era = (adj_y >= 0 ? adj_y : adj_y - 399) / 400; + const int64_t yoe = adj_y - era * 400; + const int64_t doy = (153 * adj_m + 2) / 5 + static_cast(d) - 1; + const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + const dse_t unix_day = static_cast(era * 146097 + doe - 719468); + + return !is_day_off_unix_day(unix_day); + } + /// \} -}; // namespace time_shield +} // namespace time_shield #endif // _TIME_SHIELD_VALIDATION_HPP_INCLUDED diff --git a/include/time_shield/workday_conversions.hpp b/include/time_shield/workday_conversions.hpp new file mode 100644 index 00000000..c72b9aa7 --- /dev/null +++ b/include/time_shield/workday_conversions.hpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT +#pragma once +#ifndef _TIME_SHIELD_WORKDAY_CONVERSIONS_HPP_INCLUDED +#define _TIME_SHIELD_WORKDAY_CONVERSIONS_HPP_INCLUDED + +/// \file workday_conversions.hpp +/// \brief Helpers for computing workday-related timestamps. + +#include "config.hpp" +#include "date_conversions.hpp" +#include "date_time_conversions.hpp" +#include "time_unit_conversions.hpp" +#include "validation.hpp" + +namespace time_shield { + +/// \ingroup time_conversions +/// \{ + + /// \brief Finds the first workday number within a month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline int first_workday_day(year_t year, int month) noexcept { + const int days = num_days_in_month(year, month); + if (days <= 0) { + return 0; + } + for (int day = 1; day <= days; ++day) { + if (is_workday(year, month, day)) { + return day; + } + } + return 0; + } + + /// \brief Finds the last workday number within a month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline int last_workday_day(year_t year, int month) noexcept { + const int days = num_days_in_month(year, month); + if (days <= 0) { + return 0; + } + for (int day = days; day >= 1; --day) { + if (is_workday(year, month, day)) { + return day; + } + } + return 0; + } + + /// \brief Counts workdays within a month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline int count_workdays_in_month(year_t year, int month) noexcept { + const int days = num_days_in_month(year, month); + if (days <= 0) { + return 0; + } + int total = 0; + for (int day = 1; day <= days; ++day) { + if (is_workday(year, month, day)) { + ++total; + } + } + return total; + } + + /// \brief Returns workday position in month starting from 1. + /// \param year Target year. + /// \param month Target month (1-12). + /// \param day Day of month (1-based). + TIME_SHIELD_CONSTEXPR inline int workday_index_in_month(year_t year, int month, int day) noexcept { + if (!is_workday(year, month, day)) { + return 0; + } + const int days = num_days_in_month(year, month); + if (days <= 0) { + return 0; + } + int index = 0; + for (int current = 1; current <= days; ++current) { + if (is_workday(year, month, current)) { + ++index; + if (current == day) { + return index; + } + } + } + return 0; + } + + /// \brief Checks whether date is the first workday of the month. + /// \param year Target year. + /// \param month Target month (1-12). + /// \param day Day of month (1-based). + TIME_SHIELD_CONSTEXPR inline bool is_first_workday_of_month(year_t year, int month, int day) noexcept { + return is_workday(year, month, day) && first_workday_day(year, month) == day; + } + + /// \brief Checks if date falls within the first N workdays of the month. + /// \param year Target year. + /// \param month Target month (1-12). + /// \param day Day of month (1-based). + /// \param count Number of leading workdays to include. + TIME_SHIELD_CONSTEXPR inline bool is_within_first_workdays_of_month(year_t year, int month, int day, int count) noexcept { + if (count <= 0) { + return false; + } + const int total = count_workdays_in_month(year, month); + if (count > total) { + return false; + } + const int index = workday_index_in_month(year, month, day); + return index > 0 && index <= count; + } + + /// \brief Checks whether date is the last workday of the month. + /// \param year Target year. + /// \param month Target month (1-12). + /// \param day Day of month (1-based). + TIME_SHIELD_CONSTEXPR inline bool is_last_workday_of_month(year_t year, int month, int day) noexcept { + return is_workday(year, month, day) && last_workday_day(year, month) == day; + } + + /// \brief Checks if date falls within the last N workdays of the month. + /// \param year Target year. + /// \param month Target month (1-12). + /// \param day Day of month (1-based). + /// \param count Number of trailing workdays to include. + TIME_SHIELD_CONSTEXPR inline bool is_within_last_workdays_of_month(year_t year, int month, int day, int count) noexcept { + if (count <= 0) { + return false; + } + const int total = count_workdays_in_month(year, month); + if (count > total) { + return false; + } + const int index = workday_index_in_month(year, month, day); + return index > 0 && index >= (total - count + 1); + } + + /// \brief Checks whether timestamp is the first workday of the month. + /// \param ts Timestamp in seconds. + TIME_SHIELD_CONSTEXPR inline bool is_first_workday_of_month(ts_t ts) noexcept { + return is_first_workday_of_month(year_of(ts), month_of_year(ts), day_of_month(ts)); + } + + /// \brief Checks whether millisecond timestamp is the first workday of the month. + /// \param ts_ms Timestamp in milliseconds. + TIME_SHIELD_CONSTEXPR inline bool is_first_workday_of_month_ms(ts_ms_t ts_ms) noexcept { + return is_workday_ms(ts_ms) && is_first_workday_of_month(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms)), day_of_month(ms_to_sec(ts_ms))); + } + + /// \brief Checks if timestamp falls within the first N workdays of the month. + /// \param ts Timestamp in seconds. + /// \param count Number of leading workdays to include. + TIME_SHIELD_CONSTEXPR inline bool is_within_first_workdays_of_month(ts_t ts, int count) noexcept { + return is_within_first_workdays_of_month(year_of(ts), month_of_year(ts), day_of_month(ts), count); + } + + /// \brief Checks if millisecond timestamp falls within the first N workdays of the month. + /// \param ts_ms Timestamp in milliseconds. + /// \param count Number of leading workdays to include. + TIME_SHIELD_CONSTEXPR inline bool is_within_first_workdays_of_month_ms(ts_ms_t ts_ms, int count) noexcept { + return is_workday_ms(ts_ms) && is_within_first_workdays_of_month(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms)), day_of_month(ms_to_sec(ts_ms)), count); + } + + /// \brief Checks whether timestamp is the last workday of the month. + /// \param ts Timestamp in seconds. + TIME_SHIELD_CONSTEXPR inline bool is_last_workday_of_month(ts_t ts) noexcept { + return is_last_workday_of_month(year_of(ts), month_of_year(ts), day_of_month(ts)); + } + + /// \brief Checks whether millisecond timestamp is the last workday of the month. + /// \param ts_ms Timestamp in milliseconds. + TIME_SHIELD_CONSTEXPR inline bool is_last_workday_of_month_ms(ts_ms_t ts_ms) noexcept { + return is_workday_ms(ts_ms) && is_last_workday_of_month(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms)), day_of_month(ms_to_sec(ts_ms))); + } + + /// \brief Checks if timestamp falls within the last N workdays of the month. + /// \param ts Timestamp in seconds. + /// \param count Number of trailing workdays to include. + TIME_SHIELD_CONSTEXPR inline bool is_within_last_workdays_of_month(ts_t ts, int count) noexcept { + return is_within_last_workdays_of_month(year_of(ts), month_of_year(ts), day_of_month(ts), count); + } + + /// \brief Checks if millisecond timestamp falls within the last N workdays of the month. + /// \param ts_ms Timestamp in milliseconds. + /// \param count Number of trailing workdays to include. + TIME_SHIELD_CONSTEXPR inline bool is_within_last_workdays_of_month_ms(ts_ms_t ts_ms, int count) noexcept { + return is_workday_ms(ts_ms) && is_within_last_workdays_of_month(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms)), day_of_month(ms_to_sec(ts_ms)), count); + } + + /// \brief Returns start-of-day timestamp for the first workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_t start_of_first_workday_month(year_t year, int month) noexcept { + const int day = first_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + return to_timestamp(year, month, day); + } + + /// \brief Returns start-of-day millisecond timestamp for the first workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_first_workday_month_ms(year_t year, int month) noexcept { + const int day = first_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + const ts_t day_start = to_timestamp(year, month, day); + return sec_to_ms(day_start); + } + + /// \brief Returns start-of-day timestamp for the first workday of month derived from timestamp. + /// \param ts Timestamp in seconds. + TIME_SHIELD_CONSTEXPR inline ts_t start_of_first_workday_month(ts_t ts = time_shield::ts()) noexcept { + return start_of_first_workday_month(year_of(ts), month_of_year(ts)); + } + + /// \brief Returns start-of-day millisecond timestamp for the first workday of month derived from millisecond timestamp. + /// \param ts_ms Timestamp in milliseconds. + TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_first_workday_month_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return start_of_first_workday_month_ms(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Returns end-of-day timestamp for the first workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_t end_of_first_workday_month(year_t year, int month) noexcept { + const int day = first_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + const ts_t day_start = to_timestamp(year, month, day); + return end_of_day(day_start); + } + + /// \brief Returns end-of-day millisecond timestamp for the first workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_ms_t end_of_first_workday_month_ms(year_t year, int month) noexcept { + const int day = first_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + const ts_t day_start = to_timestamp(year, month, day); + const ts_ms_t day_start_ms = sec_to_ms(day_start); + return end_of_day_ms(day_start_ms); + } + + /// \brief Returns end-of-day timestamp for the first workday of month derived from timestamp. + /// \param ts Timestamp in seconds. + TIME_SHIELD_CONSTEXPR inline ts_t end_of_first_workday_month(ts_t ts = time_shield::ts()) noexcept { + return end_of_first_workday_month(year_of(ts), month_of_year(ts)); + } + + /// \brief Returns end-of-day millisecond timestamp for the first workday of month derived from millisecond timestamp. + /// \param ts_ms Timestamp in milliseconds. + TIME_SHIELD_CONSTEXPR inline ts_ms_t end_of_first_workday_month_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return end_of_first_workday_month_ms(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Returns start-of-day timestamp for the last workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_t start_of_last_workday_month(year_t year, int month) noexcept { + const int day = last_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + return to_timestamp(year, month, day); + } + + /// \brief Returns start-of-day millisecond timestamp for the last workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_last_workday_month_ms(year_t year, int month) noexcept { + const int day = last_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + const ts_t day_start = to_timestamp(year, month, day); + return sec_to_ms(day_start); + } + + /// \brief Returns start-of-day timestamp for the last workday of month derived from timestamp. + /// \param ts Timestamp in seconds. + TIME_SHIELD_CONSTEXPR inline ts_t start_of_last_workday_month(ts_t ts = time_shield::ts()) noexcept { + return start_of_last_workday_month(year_of(ts), month_of_year(ts)); + } + + /// \brief Returns start-of-day millisecond timestamp for the last workday of month derived from millisecond timestamp. + /// \param ts_ms Timestamp in milliseconds. + TIME_SHIELD_CONSTEXPR inline ts_ms_t start_of_last_workday_month_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return start_of_last_workday_month_ms(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms))); + } + + /// \brief Returns end-of-day timestamp for the last workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_t end_of_last_workday_month(year_t year, int month) noexcept { + const int day = last_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + const ts_t day_start = to_timestamp(year, month, day); + return end_of_day(day_start); + } + + /// \brief Returns end-of-day millisecond timestamp for the last workday of month. + /// \param year Target year. + /// \param month Target month (1-12). + TIME_SHIELD_CONSTEXPR inline ts_ms_t end_of_last_workday_month_ms(year_t year, int month) noexcept { + const int day = last_workday_day(year, month); + if (day <= 0) { + return ERROR_TIMESTAMP; + } + const ts_t day_start = to_timestamp(year, month, day); + const ts_ms_t day_start_ms = sec_to_ms(day_start); + return end_of_day_ms(day_start_ms); + } + + /// \brief Returns end-of-day timestamp for the last workday of month derived from timestamp. + /// \param ts Timestamp in seconds. + TIME_SHIELD_CONSTEXPR inline ts_t end_of_last_workday_month(ts_t ts = time_shield::ts()) noexcept { + return end_of_last_workday_month(year_of(ts), month_of_year(ts)); + } + + /// \brief Returns end-of-day millisecond timestamp for the last workday of month derived from millisecond timestamp. + /// \param ts_ms Timestamp in milliseconds. + TIME_SHIELD_CONSTEXPR inline ts_ms_t end_of_last_workday_month_ms(ts_ms_t ts_ms = time_shield::ts_ms()) noexcept { + return end_of_last_workday_month_ms(year_of_ms(ts_ms), month_of_year(ms_to_sec(ts_ms))); + } + +/// \} + +}; // namespace time_shield + +#endif // _TIME_SHIELD_WORKDAY_CONVERSIONS_HPP_INCLUDED diff --git a/tests/astronomy_ole_conversions_test.cpp b/tests/astronomy_ole_conversions_test.cpp new file mode 100644 index 00000000..c18e988e --- /dev/null +++ b/tests/astronomy_ole_conversions_test.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +int main() { + using namespace time_shield; + + const double epsilon = 1e-9; + + const oadate_t oa_epoch = ts_to_oadate(static_cast(0)); + assert(std::fabs(oa_epoch - static_cast(OLE_EPOCH)) < epsilon); + + const oadate_t oa_half_day = ts_to_oadate(static_cast(SEC_PER_DAY / 2)); + assert(std::fabs(oa_half_day - (static_cast(OLE_EPOCH) + 0.5)) < epsilon); + + const oadate_t oa_zero = to_oadate(1899, 12, 30, 0, 0, 0, 0); + assert(std::fabs(oa_zero) < epsilon); + + const ts_t round_trip_ts = oadate_to_ts(oa_half_day); + assert(round_trip_ts == static_cast(SEC_PER_DAY / 2)); + + const ts_ms_t day_ms = static_cast(MS_PER_DAY); + const oadate_t oa_ms = ts_ms_to_oadate(day_ms); + assert(std::llabs(oadate_to_ts_ms(oa_ms) - day_ms) <= 1); + + const jd_t jd_epoch = ts_to_jd(static_cast(0)); + assert(std::fabs(jd_epoch - 2440587.5) < epsilon); + + const mjd_t mjd_epoch = ts_to_mjd(static_cast(0)); + assert(std::fabs(mjd_epoch - 40587.0) < epsilon); + + const jd_t jd_year2000 = gregorian_to_jd(1U, 1U, 2000U, 12U, 0U, 0U, 0U); + assert(std::fabs(jd_year2000 - 2451545.0) < epsilon); + + const jdn_t jdn_unix_epoch = gregorian_to_jdn(1U, 1U, 1970U); + assert(jdn_unix_epoch == static_cast(2440588)); + + const double phase_epoch = moon_phase(static_cast(0)); + assert(std::fabs(phase_epoch - 0.7520754628736458) < 1e-12); + + const double age_epoch = moon_age_days(static_cast(0)); + assert(std::fabs(age_epoch - 22.209231150442246) < 1e-9); + + (void)epsilon; + (void)oa_epoch; + (void)oa_half_day; + (void)oa_zero; + (void)round_trip_ts; + (void)day_ms; + (void)oa_ms; + (void)jd_epoch; + (void)mjd_epoch; + (void)jd_year2000; + (void)jdn_unix_epoch; + (void)phase_epoch; + (void)age_epoch; + + return 0; +} diff --git a/tests/date_time_test.cpp b/tests/date_time_test.cpp new file mode 100644 index 00000000..9b6b09a3 --- /dev/null +++ b/tests/date_time_test.cpp @@ -0,0 +1,123 @@ +#include + +#include +#include + +/// \brief Tests for DateTime wrapper utilities. +int main() { + using namespace time_shield; + + bool all_ok = true; + auto expect = [&](bool condition, const char* message) { + if (!condition) { + std::cerr << message << std::endl; + all_ok = false; + } + }; + + { + DateTime parsed; + const std::string input = "2025-12-16T10:20:30.123+02:30"; + if (DateTime::try_parse_iso8601(input, parsed)) { + expect(parsed.to_iso8601() == input, "Parsed ISO8601 should round-trip"); + } else { + expect(false, "Failed to parse expected ISO8601 string"); + } + } + + { + const DateTime utc = DateTime::from_components(2025, 1, 1, 0, 0, 0, 0, 0); + const DateTime berlin = DateTime::from_components(2025, 1, 1, 1, 0, 0, 0, SEC_PER_HOUR); + expect(utc.unix_ms() == berlin.unix_ms(), "UTC and offset-adjusted instant must match"); + expect(utc == berlin, "Equality should compare UTC instant"); + } + + { + const DateTime shifted_epoch = DateTime::from_components(1970, 1, 1, 0, 0, 0, 0, SEC_PER_HOUR); + expect(shifted_epoch.unix_ms() == -sec_to_ms(SEC_PER_HOUR), "Offset should shift epoch"); + } + + { + const DateTime sample = DateTime::from_components(2024, 5, 15, 12, 0, 0, 0, SEC_PER_HOUR); + const IsoWeekDateStruct iso = sample.iso_week_date(); + const DateTime restored = DateTime::from_iso_week_date(iso, 12, 0, 0, 0, SEC_PER_HOUR); + expect(restored.year() == sample.year(), "ISO week-year conversion should preserve year"); + expect(restored.month() == sample.month(), "ISO week-year conversion should preserve month"); + expect(restored.day() == sample.day(), "ISO week-year conversion should preserve day"); + } + + { + const DateTime dt = DateTime::from_components(2025, 3, 14, 15, 9, 26, 500, 2 * SEC_PER_HOUR); + const DateTime start = dt.start_of_day(); + expect(start.hour() == 0, "Start of day should reset hour"); + expect(start.minute() == 0, "Start of day should reset minute"); + expect(start.second() == 0, "Start of day should reset second"); + expect(start.millisecond() == 0, "Start of day should reset millisecond"); + expect(start.unix_ms() <= dt.unix_ms(), "Start of day should not exceed original instant"); + } + + { + DateTime parsed_z; + DateTime parsed_plus; + DateTime parsed_negative; + expect(DateTime::try_parse_iso8601("2024-01-02T03:04:05Z", parsed_z), "Should parse Zulu offset"); + expect(DateTime::try_parse_iso8601("2024-01-02T03:04:05+00:00", parsed_plus), "Should parse +00:00 offset"); + expect(DateTime::try_parse_iso8601("2024-01-02T21:34:05-05:30", parsed_negative), "Should parse negative offset"); + expect(parsed_z.unix_ms() == parsed_plus.unix_ms(), "Equivalent zero-offset instants should match"); + expect(parsed_z.utc_offset() == 0, "Zulu offset should be zero"); + expect(parsed_negative.utc_offset() == -(5 * SEC_PER_HOUR + 30 * SEC_PER_MIN), "Parsed offset should match -05:30"); + } + + { + DateTime parsed; + if (DateTime::try_parse_iso8601("2024-07-01T12:00:00-05:30", parsed)) { + const DateTime reparsed = DateTime::parse_iso8601(parsed.to_iso8601()); + expect(parsed == reparsed, "Round-trip parse should preserve instant"); + expect(parsed.utc_offset() == reparsed.utc_offset(), "Round-trip parse should preserve offset"); + } else { + expect(false, "Should parse offset date"); + } + } + + { + DateTime parsed; + if (DateTime::try_parse_iso8601("2024-02-29", parsed)) { + expect(parsed.hour() == 0, "Date-only parse should default hour"); + expect(parsed.minute() == 0, "Date-only parse should default minute"); + expect(parsed.second() == 0, "Date-only parse should default second"); + } else { + expect(false, "Leap-day parse should succeed"); + } + } + + { + DateTime parsed; + expect(!DateTime::try_parse_iso8601("invalid-date", parsed), "Invalid ISO should fail to parse"); + } + + { + DateTime result; + expect(DateTime::try_from_components(2024, 2, 29, 23, 59, 59, 999, 0, result), "Valid leap day components should parse"); + expect(!DateTime::try_from_components(2023, 2, 29, 0, 0, 0, 0, 0, result), "Invalid leap day should fail"); + expect(!DateTime::try_from_components(2024, 1, 1, 25, 0, 0, 0, 0, result), "Out-of-range hour should fail"); + expect(!DateTime::try_from_components(2024, 1, 1, 0, 0, 0, 0, 30 * SEC_PER_HOUR, result), "Out-of-range offset should fail"); + } + + { + const DateTime iso_boundary = DateTime::from_components(2018, 12, 31); + const IsoWeekDateStruct iso = iso_boundary.iso_week_date(); + const DateTime restored = DateTime::from_iso_week_date(iso); + expect(restored.year() == 2018, "ISO boundary conversion should preserve year"); + expect(restored.month() == 12, "ISO boundary conversion should preserve month"); + expect(restored.day() == 31, "ISO boundary conversion should preserve day"); + } + + { + const DateTime utc = DateTime::from_components(2025, 6, 1, 10, 0, 0, 0, 0); + const DateTime shifted = utc.with_offset(SEC_PER_HOUR); + expect(utc == shifted, "Offset change should keep instant equal"); + expect(!utc.same_local(shifted), "Offset change should alter local representation"); + } + + return all_ok ? 0 : 1; +} diff --git a/tests/deadline_elapsed_timer_test.cpp b/tests/deadline_elapsed_timer_test.cpp new file mode 100644 index 00000000..5a3d270f --- /dev/null +++ b/tests/deadline_elapsed_timer_test.cpp @@ -0,0 +1,209 @@ +#include +#include + +#include +#include +#include +#include + +int main() { + using namespace time_shield; + + // DeadlineTimer basic behavior. + DeadlineTimer deadline; + assert(!deadline.is_running()); + assert(!deadline.has_expired()); + assert(deadline.remaining_time() == DeadlineTimer::duration::zero()); + + const auto start = DeadlineTimer::clock::now(); + const auto deadline_tp = start + std::chrono::milliseconds(50); + deadline.start(deadline_tp); + assert(deadline.is_running()); + assert(!deadline.has_expired(start)); + assert(deadline.remaining_time(start) == std::chrono::milliseconds(50)); + const auto expected_deadline_ms = std::chrono::duration_cast(deadline_tp.time_since_epoch()).count(); + const auto expected_deadline_sec = std::chrono::duration_cast(deadline_tp.time_since_epoch()).count(); + assert(deadline.deadline_ms() == expected_deadline_ms); + assert(deadline.deadline_sec() == expected_deadline_sec); + assert(deadline.remaining_time_ms() >= 0); + assert(deadline.remaining_time_ms() <= 60); + assert(deadline.remaining_time_sec() == 0); + assert(!deadline.has_expired(start + std::chrono::milliseconds(25))); + assert(deadline.has_expired(start + std::chrono::milliseconds(60))); + assert(deadline.remaining_time(start + std::chrono::milliseconds(60)) == DeadlineTimer::duration::zero()); + const auto now_25_ms = std::chrono::duration_cast((start + std::chrono::milliseconds(25)).time_since_epoch()).count(); + const auto now_60_ms = std::chrono::duration_cast((start + std::chrono::milliseconds(60)).time_since_epoch()).count(); + const auto now_25_sec = std::chrono::duration_cast((start + std::chrono::milliseconds(25)).time_since_epoch()).count(); + const auto now_2_sec = std::chrono::duration_cast((start + std::chrono::seconds(2)).time_since_epoch()).count(); + assert(!deadline.has_expired_ms(static_cast(now_25_ms))); + assert(deadline.has_expired_ms(static_cast(now_60_ms))); + assert(!deadline.has_expired_sec(static_cast(now_25_sec))); + assert(deadline.has_expired_sec(static_cast(now_2_sec))); + + deadline.set_forever(); + assert(deadline.is_running()); + assert(deadline.is_forever()); + assert(!deadline.has_expired()); + assert(deadline.deadline() == DeadlineTimer::time_point::max()); + + deadline.stop(); + assert(!deadline.is_running()); + assert(deadline.remaining_time() == DeadlineTimer::duration::zero()); + + deadline.start(std::chrono::milliseconds(-1)); + assert(deadline.is_running()); + assert(deadline.has_expired()); + + const auto before_short = DeadlineTimer::clock::now(); + deadline.start(std::chrono::nanoseconds(1)); + assert(deadline.is_running()); + assert(!deadline.has_expired(before_short)); + + auto factory = DeadlineTimer::from_timeout(std::chrono::milliseconds(20)); + assert(factory.is_running()); + assert(!factory.has_expired()); + assert(factory.remaining_time() > DeadlineTimer::duration::zero()); + + factory.add(std::chrono::milliseconds(30)); + assert(factory.remaining_time() >= std::chrono::milliseconds(25)); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + factory.add(std::chrono::milliseconds(10)); + assert(factory.remaining_time() >= std::chrono::milliseconds(5)); + + factory.add(DeadlineTimer::duration::zero()); + assert(factory.is_running()); + + DeadlineTimer expired = DeadlineTimer::from_timeout(std::chrono::milliseconds(-5)); + assert(expired.has_expired()); + expired.add(std::chrono::milliseconds(10)); + assert(expired.is_running()); + assert(!expired.has_expired()); + + DeadlineTimer near_max(DeadlineTimer::time_point::max() - DeadlineTimer::duration(1)); + near_max.add(DeadlineTimer::duration::max()); + assert(near_max.deadline() == DeadlineTimer::time_point::max()); + + DeadlineTimer seconds_factory = DeadlineTimer::from_timeout_sec(static_cast(1)); + assert(seconds_factory.is_running()); + seconds_factory.start_sec(static_cast(2)); + assert(seconds_factory.remaining_time() <= std::chrono::seconds(2)); + assert(seconds_factory.remaining_time_sec() <= static_cast(2)); + assert(seconds_factory.remaining_time_sec() >= static_cast(0)); + seconds_factory.add_sec(static_cast(1)); + assert(seconds_factory.remaining_time() >= std::chrono::seconds(1)); + assert(seconds_factory.remaining_time_sec() >= static_cast(1)); + + DeadlineTimer ms_factory = DeadlineTimer::from_timeout_ms(static_cast(50)); + assert(ms_factory.is_running()); + ms_factory.start_ms(static_cast(100)); + assert(ms_factory.remaining_time() <= std::chrono::milliseconds(100)); + assert(ms_factory.remaining_time_ms() <= static_cast(100)); + assert(ms_factory.remaining_time_ms() >= static_cast(0)); + ms_factory.add_ms(static_cast(10)); + assert(ms_factory.remaining_time() >= std::chrono::milliseconds(10)); + assert(ms_factory.remaining_time_ms() >= static_cast(10)); + + DeadlineTimer ms_ctor(static_cast(25)); + assert(ms_ctor.is_running()); + assert(ms_ctor.remaining_time() <= std::chrono::milliseconds(25)); + + // ElapsedTimer checks. + ElapsedTimer elapsed; + assert(!elapsed.is_running()); + assert(!elapsed.is_valid()); + assert(elapsed.elapsed() == ElapsedTimer::duration::zero()); + assert(elapsed.ms_since_reference() == 0); + + elapsed.start(); + assert(elapsed.is_running()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + const auto first_span = elapsed.elapsed(); + assert(first_span >= std::chrono::milliseconds(1)); + const auto first_ms = elapsed.elapsed_ms(); + assert(first_ms >= 1); + const auto first_ns = elapsed.elapsed_ns(); + assert(first_ns >= static_cast(first_ms - 1) * 1000000); + const auto first_count = elapsed.elapsed_count(); + assert(first_count == first_ms); + assert(!elapsed.has_expired(static_cast(1000))); + assert(elapsed.has_expired(static_cast(0))); + assert(!elapsed.has_expired_sec(static_cast(1))); + assert(elapsed.elapsed_sec() >= static_cast(0)); + const auto reference_ms = elapsed.ms_since_reference(); + assert(reference_ms == std::chrono::duration_cast(elapsed.start_time().time_since_epoch()).count()); + + const auto snapshot = ElapsedTimer::clock::now(); + const auto snapshot_ns = std::chrono::duration_cast(snapshot.time_since_epoch()).count(); + const auto snapshot_ms = std::chrono::duration_cast(snapshot.time_since_epoch()).count(); + const auto snapshot_sec = std::chrono::duration_cast(snapshot.time_since_epoch()).count(); + const auto truncated_snapshot_ns = ElapsedTimer::time_point(std::chrono::duration_cast(std::chrono::nanoseconds(snapshot_ns))); + const auto truncated_snapshot_ms = ElapsedTimer::time_point(std::chrono::duration_cast(std::chrono::milliseconds(snapshot_ms))); + const auto truncated_snapshot_sec = ElapsedTimer::time_point(std::chrono::duration_cast(std::chrono::seconds(snapshot_sec))); + const auto expected_ns = std::chrono::duration_cast(truncated_snapshot_ns - elapsed.start_time()).count(); + const auto expected_ms = std::chrono::duration_cast(truncated_snapshot_ms - elapsed.start_time()).count(); + const auto expected_sec = std::chrono::duration_cast(truncated_snapshot_sec - elapsed.start_time()).count(); + assert(elapsed.elapsed_ns(snapshot_ns) == expected_ns); + assert(elapsed.elapsed_ms(snapshot_ms) == expected_ms); + assert(elapsed.elapsed_sec(snapshot_sec) == expected_sec); + + const auto restarted_ms = elapsed.restart_ms(snapshot_ms); + assert(restarted_ms == expected_ms); + assert(elapsed.is_running()); + assert(elapsed.elapsed_ms(snapshot_ms) == 0); + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + const auto after_ms_restart = ElapsedTimer::clock::now(); + const auto after_ms_restart_sec = std::chrono::duration_cast(after_ms_restart.time_since_epoch()).count(); + const auto truncated_after_sec = ElapsedTimer::time_point(std::chrono::duration_cast(std::chrono::seconds(after_ms_restart_sec))); + const auto expected_after_sec = std::chrono::duration_cast(truncated_after_sec - elapsed.start_time()).count(); + const auto restarted_sec = elapsed.restart_sec(after_ms_restart_sec); + assert(restarted_sec == expected_after_sec); + assert(elapsed.elapsed_sec(after_ms_restart_sec) == 0); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + const auto delta = elapsed.restart(); + + // After restart_sec(), start_time can be quantized to a second boundary, + // so delta may be smaller than first_span. Just ensure it is non-negative + // and reasonably bounded. + assert(delta >= ElapsedTimer::duration::zero()); + assert(delta < std::chrono::seconds(2)); + + const auto after_restart = elapsed.elapsed(); + assert(after_restart < std::chrono::milliseconds(200)); + assert(!elapsed.has_expired(static_cast(50))); + assert(!elapsed.has_expired_sec(static_cast(1))); + + elapsed.invalidate(); + assert(!elapsed.is_running()); + assert(elapsed.elapsed() == ElapsedTimer::duration::zero()); + assert(elapsed.elapsed_ms() == 0); + assert(elapsed.elapsed_ns() == 0); + assert(!elapsed.has_expired(static_cast(1))); + assert(!elapsed.has_expired_sec(static_cast(1))); + assert(elapsed.ms_since_reference() == 0); + + ElapsedTimer autostart(true); + assert(autostart.is_running()); + assert(autostart.elapsed() >= ElapsedTimer::duration::zero()); + + (void)expected_deadline_ms; + (void)expected_deadline_sec; + (void)now_25_ms; + (void)now_60_ms; + (void)now_25_sec; + (void)now_2_sec; + (void)first_ms; + (void)first_ns; + (void)first_count; + (void)reference_ms; + (void)expected_ns; + (void)expected_ms; + (void)expected_sec; + (void)restarted_ms; + (void)expected_after_sec; + (void)restarted_sec; + + return 0; +} diff --git a/tests/gmt_time_zone_conversion_test.cpp b/tests/gmt_time_zone_conversion_test.cpp index d7ea0de6..702af512 100644 --- a/tests/gmt_time_zone_conversion_test.cpp +++ b/tests/gmt_time_zone_conversion_test.cpp @@ -42,6 +42,25 @@ int main() { ts_t eet_end_after = to_timestamp(year, int(OCT), end_day, 4, 0, 0); ts_t gmt_from_eet_end_after = eet_to_gmt(eet_end_after); assert(gmt_to_eet(gmt_from_eet_end_after) == eet_end_after); + + (void)start_day; + (void)cet_start_before; + (void)gmt_from_cet_before; + (void)cet_start_after; + (void)gmt_from_cet_after; + (void)eet_start_before; + (void)gmt_from_eet_before; + (void)eet_start_after; + (void)gmt_from_eet_after; + (void)end_day; + (void)cet_end_before; + (void)gmt_from_cet_end_before; + (void)cet_end_after; + (void)gmt_from_cet_end_after; + (void)eet_end_before; + (void)gmt_from_eet_end_before; + (void)eet_end_after; + (void)gmt_from_eet_end_after; } return 0; diff --git a/tests/iso8601_round_trip_test.cpp b/tests/iso8601_round_trip_test.cpp index 2b971f98..d062026f 100644 --- a/tests/iso8601_round_trip_test.cpp +++ b/tests/iso8601_round_trip_test.cpp @@ -49,5 +49,25 @@ int main() { is_ok = str_to_ts_ms("2024-03-20T12:34:56.789123-05:30", parsed_fail); assert(!is_ok); + (void)base_ts; + (void)str_z; + (void)parsed_z; + (void)str_pos; + (void)parsed_pos; + (void)offset_neg; + (void)str_neg; + (void)parsed_neg; + (void)base_ms; + (void)str_ms; + (void)parsed_ms; + (void)str_ms_z; + (void)parsed_ms_z; + (void)str_ms_pos; + (void)parsed_ms_pos; + (void)str_ms_neg; + (void)parsed_ms_neg; + (void)parsed_fail; + (void)is_ok; + return 0; } diff --git a/tests/iso_week_date_test.cpp b/tests/iso_week_date_test.cpp new file mode 100644 index 00000000..431c7497 --- /dev/null +++ b/tests/iso_week_date_test.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +#include +#include +#include + +/// \brief Checks for ISO week-date conversions, formatting, and parsing. +int main() { + using namespace time_shield; + + struct IsoCase { + int year; + int month; + int day; + int iso_year; + int iso_week; + int iso_weekday; + }; + + const std::array calendar_cases = {{ + {2005, 1, 1, 2004, 53, 6}, + {2014, 12, 29, 2015, 1, 1}, + {2015, 1, 1, 2015, 1, 4}, + {2016, 1, 3, 2015, 53, 7}, + {2020, 12, 31, 2020, 53, 4}, + {2021, 1, 1, 2020, 53, 5}, + {2021, 12, 31, 2021, 52, 5}, + {2022, 1, 1, 2021, 52, 6}, + {2025, 12, 16, 2025, 51, 2}, + }}; + + for (const auto& item : calendar_cases) { + const auto iso = to_iso_week_date(item.year, item.month, item.day); + assert(iso.year == item.iso_year); + assert(iso.week == item.iso_week); + assert(iso.weekday == item.iso_weekday); + + const auto date_back = iso_week_date_to_date(iso); + assert(date_back.year == item.year); + assert(date_back.mon == item.month); + assert(date_back.day == item.day); + + const auto ts_value = to_timestamp(item.year, item.month, item.day); + const auto iso_from_ts = to_iso_week_date(ts_value); + assert(iso_from_ts.year == item.iso_year); + assert(iso_from_ts.week == item.iso_week); + assert(iso_from_ts.weekday == item.iso_weekday); + } + + assert(iso_weeks_in_year(2004) == 53); + assert(iso_weeks_in_year(2005) == 52); + assert(iso_weeks_in_year(2015) == 53); + assert(iso_weeks_in_year(2020) == 53); + assert(iso_weeks_in_year(2021) == 52); + assert(iso_weeks_in_year(2025) == 52); + + IsoWeekDateStruct parsed{}; + assert(parse_iso_week_date("2025-W51-2", parsed)); + assert(parsed.year == 2025 && parsed.week == 51 && parsed.weekday == 2); + assert(format_iso_week_date(parsed) == "2025-W51-2"); + + assert(parse_iso_week_date("2025W512", parsed)); + assert(format_iso_week_date(parsed) == "2025-W51-2"); + + assert(parse_iso_week_date("2025-W51", parsed)); + assert(parsed.weekday == 1); + assert(format_iso_week_date(parsed, true, false) == "2025-W51"); + + const std::array iso_chars{{'2', '0', '2', '5', '-', 'W', '5', '1', '-', '2'}}; + assert(parse_iso_week_date(iso_chars.data(), iso_chars.size(), parsed)); + assert(parsed.year == 2025 && parsed.week == 51 && parsed.weekday == 2); + + assert(!parse_iso_week_date("2025-W00-1", parsed)); + assert(!parse_iso_week_date("2025-W54-1", parsed)); + assert(!parse_iso_week_date("2025-W51-0", parsed)); + assert(!parse_iso_week_date("2025-W51-8", parsed)); + + const IsoWeekDateStruct round_trip_iso = create_iso_week_date_struct(2020, 53, 4); + const auto round_trip_date = iso_week_date_to_date(round_trip_iso); + const auto iso_again = to_iso_week_date(round_trip_date.year, round_trip_date.mon, round_trip_date.day); + assert(iso_again.year == round_trip_iso.year); + assert(iso_again.week == round_trip_iso.week); + assert(iso_again.weekday == round_trip_iso.weekday); + + DateTimeStruct parsed_dt{}; + TimeZoneStruct parsed_tz{}; + + assert(parse_iso8601("2025-W51-2", parsed_dt, parsed_tz)); + assert(parsed_dt.year == 2025 && parsed_dt.mon == 12 && parsed_dt.day == 16); + assert(parsed_dt.hour == 0 && parsed_dt.min == 0 && parsed_dt.sec == 0 && parsed_dt.ms == 0); + + assert(parse_iso8601("2025W512T10:15:30Z", parsed_dt, parsed_tz)); + assert(parsed_dt.year == 2025 && parsed_dt.mon == 12 && parsed_dt.day == 16); + assert(parsed_dt.hour == 10 && parsed_dt.min == 15 && parsed_dt.sec == 30 && parsed_dt.ms == 0); + assert(parsed_tz.hour == 0 && parsed_tz.min == 0 && parsed_tz.is_positive); + + assert(parse_iso8601("2025-W51T23:45:00-02:30", parsed_dt, parsed_tz)); + assert(parsed_dt.year == 2025 && parsed_dt.mon == 12 && parsed_dt.day == 15); + assert(parsed_dt.hour == 23 && parsed_dt.min == 45 && parsed_dt.sec == 0); + assert(parsed_tz.hour == 2 && parsed_tz.min == 30 && !parsed_tz.is_positive); + + return 0; +} diff --git a/tests/moon_phase_test.cpp b/tests/moon_phase_test.cpp new file mode 100644 index 00000000..924abb59 --- /dev/null +++ b/tests/moon_phase_test.cpp @@ -0,0 +1,93 @@ +#include +#include + +#include +#include + +/// \brief Basic regression checks for the MoonPhase calculator. +int main() { + using namespace time_shield; + + astronomy::MoonPhase calculator{}; + + // 2024-01-01T00:00:00Z + const double ts = 1704067200.0; + const auto res = calculator.compute(ts); + + assert(res.phase >= 0.0 && res.phase < 1.0); + assert(res.illumination >= 0.0 && res.illumination <= 1.0); + assert(res.distance_km > 300000.0 && res.distance_km < 410000.0); + assert(res.diameter_deg > 0.48 && res.diameter_deg < 0.57); + assert(res.sun_distance_km > 140000000.0 && res.sun_distance_km < 160000000.0); + assert(res.sun_diameter_deg > 0.5 && res.sun_diameter_deg < 0.55); + + const auto signal = moon_phase_sincos(ts); + assert(std::abs(signal.phase_sin - res.phase_sin) < 1e-12); + assert(std::abs(signal.phase_cos - res.phase_cos) < 1e-12); + assert(std::abs(signal.phase_angle_rad - res.phase_angle_rad) < 1e-12); + assert(std::abs(moon_illumination(ts) - res.illumination) < 1e-12); + + const double approx_phase = moon_phase_jd_approx(ts); + assert(approx_phase >= 0.0 && approx_phase < 1.0); + assert(std::abs(approx_phase - res.phase) < 0.1); + assert(std::abs(moon_phase(ts) - res.phase) < 1e-12); + + const double approx_age_days = moon_age_days_jd_approx(ts); + assert(approx_age_days >= 0.0 && approx_age_days < 30.0); + assert(std::abs(approx_age_days - res.age_days) < 2.0); + assert(std::abs(moon_age_days(ts) - res.age_days) < 1e-12); + + assert(std::abs(res.phase - calculator.compute_phase(ts)) < 1e-12); + + const auto quarters_array = calculator.quarter_times_unix(ts); + for (std::size_t i = 1; i < quarters_array.size(); ++i) { + assert(quarters_array[i] > quarters_array[i - 1]); + } + + const auto quarters_struct = moon_quarters(ts); + assert(std::abs(quarters_struct.previous_new_unix_s - quarters_array[0]) < 1e-6); + assert(std::abs(quarters_struct.next_last_quarter_unix_s - quarters_array[7]) < 1e-6); + + const double lunar_cycle = quarters_array[4] - quarters_array[0]; + assert(lunar_cycle > 2300000.0 && lunar_cycle < 2700000.0); + assert(ts >= quarters_array[0] && ts <= quarters_array[4]); + + const double first_gap = quarters_array[1] - quarters_array[0]; + const double second_gap = quarters_array[2] - quarters_array[1]; + const double third_gap = quarters_array[3] - quarters_array[2]; + assert(first_gap > 450000.0 && first_gap < 750000.0); + assert(second_gap > 450000.0 && second_gap < 750000.0); + assert(third_gap > 450000.0 && third_gap < 750000.0); + + const double before_new = quarters_struct.previous_new_unix_s - 900.0; + const double after_new = quarters_struct.previous_new_unix_s + 900.0; + const auto signal_before = moon_phase_sincos(before_new); + const auto signal_after = moon_phase_sincos(after_new); + const double dot_product = signal_before.phase_cos * signal_after.phase_cos + + signal_before.phase_sin * signal_after.phase_sin; + assert(dot_product > 0.95); + + assert(is_new_moon_window(before_new)); + assert(is_new_moon_window(after_new)); + assert(!is_full_moon_window(before_new, 1800.0)); + assert(!is_last_quarter_window(before_new, 1800.0)); + + const double far_from_event = quarters_struct.previous_first_quarter_unix_s + 172800.0; + assert(!is_new_moon_window(far_from_event, 3600.0)); + + (void)ts; + (void)res; + (void)signal; + (void)approx_phase; + (void)approx_age_days; + (void)quarters_array; + (void)quarters_struct; + (void)lunar_cycle; + (void)first_gap; + (void)second_gap; + (void)third_gap; + (void)dot_product; + (void)far_from_event; + + return 0; +} diff --git a/tests/negative_time_boundaries_test.cpp b/tests/negative_time_boundaries_test.cpp new file mode 100644 index 00000000..2d55de12 --- /dev/null +++ b/tests/negative_time_boundaries_test.cpp @@ -0,0 +1,46 @@ +#include + +#include + +int main() { + using namespace time_shield; + + const ts_t pre_epoch = to_timestamp(1969, 12, 31, 23, 59, 59); + const ts_t pre_epoch_start = to_timestamp(1969, 12, 31, 0, 0, 0); + const ts_t pre_epoch_prev_day = to_timestamp(1969, 12, 30, 0, 0, 0); + + assert(pre_epoch == -1); + assert(start_of_day(pre_epoch) == pre_epoch_start); + assert(start_of_day(pre_epoch_start) == pre_epoch_start); + assert(start_of_day(-86400) == pre_epoch_start); + assert(start_of_prev_day(pre_epoch_start) == pre_epoch_prev_day); + assert(end_of_day(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59)); + assert(end_of_day(pre_epoch_start) == to_timestamp(1969, 12, 31, 23, 59, 59)); + + assert(start_of_hour(pre_epoch) == to_timestamp(1969, 12, 31, 23, 0, 0)); + assert(end_of_hour(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59)); + assert(start_of_min(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 0)); + assert(end_of_min(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59)); + + assert(min_of_day(pre_epoch) == 1439); + assert(hour_of_day(pre_epoch) == 23); + assert(min_of_hour(pre_epoch) == 59); + assert(weekday_of_ts(pre_epoch) == WED); + + assert(start_of_period(300, pre_epoch) == -300); + assert(end_of_period(300, pre_epoch) == -1); + + const ts_ms_t pre_epoch_ms = to_timestamp_ms(1969, 12, 31, 23, 59, 59, 999); + const ts_ms_t pre_epoch_start_ms = sec_to_ms(pre_epoch_start); + + assert(pre_epoch_ms == -1); + assert(start_of_day_ms(pre_epoch_ms) == pre_epoch_start_ms); + assert(start_of_day_ms(pre_epoch_start_ms - 1) == sec_to_ms(pre_epoch_prev_day)); + assert(end_of_day_ms(pre_epoch_ms) == pre_epoch_start_ms + MS_PER_DAY - 1); + assert(start_of_hour_ms(pre_epoch_ms) == sec_to_ms(start_of_hour(pre_epoch))); + assert(start_of_hour_ms(-1000) == sec_to_ms(to_timestamp(1969, 12, 31, 23, 0, 0))); + assert(start_of_hour_ms(-1001) == sec_to_ms(to_timestamp(1969, 12, 31, 23, 0, 0))); + assert(end_of_hour_ms(pre_epoch_ms) == sec_to_ms(end_of_hour(pre_epoch)) + 999); + + return 0; +} diff --git a/tests/ntp_client_core_test.cpp b/tests/ntp_client_core_test.cpp new file mode 100644 index 00000000..fb28adbd --- /dev/null +++ b/tests/ntp_client_core_test.cpp @@ -0,0 +1,137 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include +#include +#include +#include + +#include +#include +#include + +using namespace time_shield; + +class FakeUdpTransport : public detail::IUdpTransport { +public: + bool ok = true; + int error_code = 0; + detail::NtpPacket reply{}; + + bool transact(const detail::UdpRequest& req, int& out_error_code) noexcept override { + out_error_code = error_code; + if (!ok) { + return false; + } + if (req.recv_data && req.recv_size == sizeof(detail::NtpPacket)) { + std::memcpy(req.recv_data, &reply, sizeof(reply)); + } + out_error_code = 0; + return true; + } +}; + +static void unix_us_to_ntp_ts(uint64_t unix_us, uint32_t& sec_net, uint32_t& frac_net) { + const uint64_t sec = unix_us / 1000000 + 2208988800ULL; + const uint64_t frac = ((unix_us % 1000000) * 0x100000000ULL) / 1000000; + sec_net = htonl(static_cast(sec)); + frac_net = htonl(static_cast(frac)); +} + +int main() { + { + // Successful parse + FakeUdpTransport transport; + detail::NtpPacket pkt{}; + uint32_t sec = 0; + uint32_t frac = 0; + unix_us_to_ntp_ts(1000000ULL, sec, frac); // t1 + pkt.orig_ts_sec = sec; + pkt.orig_ts_frac = frac; + + unix_us_to_ntp_ts(1000100ULL, sec, frac); // t2 + pkt.recv_ts_sec = sec; + pkt.recv_ts_frac = frac; + + unix_us_to_ntp_ts(1000120ULL, sec, frac); // t3 + pkt.tx_ts_sec = sec; + pkt.tx_ts_frac = frac; + + pkt.li_vn_mode = static_cast((0 << 6) | (3 << 3) | 4); + pkt.stratum = 2; + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(ok); + assert(error == 0); + assert(offset != 0 || delay != 0); // basic sanity without relying on realtime arrival + assert(stratum == 2); + (void)ok; + (void)error; + (void)offset; + (void)delay; + (void)stratum; + } + + { + // Transport failure + FakeUdpTransport transport; + transport.ok = false; + transport.error_code = 42; + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!ok); + assert(error == 42); + (void)ok; + (void)error; + (void)offset; + (void)delay; + (void)stratum; + } + + { + // Parse failure + FakeUdpTransport transport; + std::memset(&transport.reply, 0, sizeof(transport.reply)); // origin/recv/tx sec => 0 (pre-1970) -> parse fails + transport.reply.li_vn_mode = static_cast((0 << 6) | (3 << 3) | 4); + transport.reply.stratum = 2; + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!ok); + assert(error == detail::NTP_E_BAD_TS); + (void)ok; + (void)error; + (void)offset; + (void)delay; + (void)stratum; + } + + { + // Fraction to microseconds + assert(detail::ntp_frac_to_us(htonl(0u)) == 0); + assert(detail::ntp_frac_to_us(htonl(0x80000000u)) >= 499999 && detail::ntp_frac_to_us(htonl(0x80000000u)) <= 500001); + assert(detail::ntp_frac_to_us(htonl(0xFFFFFFFFu)) >= 999998 && detail::ntp_frac_to_us(htonl(0xFFFFFFFFu)) <= 1000000); + } + + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/ntp_client_pool_runner_test.cpp b/tests/ntp_client_pool_runner_test.cpp new file mode 100644 index 00000000..bb0c4eb7 --- /dev/null +++ b/tests/ntp_client_pool_runner_test.cpp @@ -0,0 +1,97 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include + +#include +#include +#include +#include + +using namespace time_shield; + +struct FakePool { + std::atomic m_measure_calls{0}; + std::atomic m_off_us{0}; + + FakePool() = default; + FakePool(const FakePool&) = delete; + FakePool& operator=(const FakePool&) = delete; + + FakePool(FakePool&& other) noexcept { + m_measure_calls.store(other.m_measure_calls.load()); + m_off_us.store(other.m_off_us.load()); + } + + FakePool& operator=(FakePool&& other) noexcept { + if (this != &other) { + m_measure_calls.store(other.m_measure_calls.load()); + m_off_us.store(other.m_off_us.load()); + } + return *this; + } + + bool measure() { + const uint64_t calls = m_measure_calls.fetch_add(1) + 1; + m_off_us.store(static_cast(calls * 1000)); + return true; + } + + int64_t offset_us() const noexcept { return m_off_us.load(); } + int64_t utc_time_us() const noexcept { return m_off_us.load(); } + int64_t utc_time_ms() const noexcept { return m_off_us.load() / 1000; } + std::vector last_samples() const { return {}; } +}; + +int main() { + using Runner = BasicPoolRunner; + + { + Runner runner; + assert(runner.start(std::chrono::milliseconds(20), true)); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (runner.measure_count() < 3 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + assert(runner.running()); + assert(runner.last_measure_ok()); + assert(runner.measure_count() >= 3); + assert(runner.offset_us() > 0); + + const uint64_t before = runner.measure_count(); + runner.stop(); + const uint64_t after = runner.measure_count(); + assert(before == after); + assert(!runner.running()); + (void)before; + (void)after; + } + + { + Runner runner; + assert(runner.start(std::chrono::milliseconds(500), false)); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + assert(runner.measure_count() == 0); + + assert(runner.force_measure()); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (runner.measure_count() < 1 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + runner.stop(); + runner.stop(); + } + + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/ntp_client_pool_template_test.cpp b/tests/ntp_client_pool_template_test.cpp new file mode 100644 index 00000000..a1dad1be --- /dev/null +++ b/tests/ntp_client_pool_template_test.cpp @@ -0,0 +1,243 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include + +#include +#include +#include +#include +#include +#include + +using namespace time_shield; + +struct FakeReply { + bool ok = true; + int error_code = 0; + int stratum = 1; + int64_t offset_us = 0; + int64_t delay_us = 0; + bool throws_on_query = false; +}; + +class FakeNtpClient { +public: + FakeNtpClient(const std::string& host, int port) + : m_host(host) + , m_port(port) { + (void)m_port; + } + + bool query() { + const FakeReply& reply = scenario()[m_host]; + if (reply.throws_on_query) { + throw std::runtime_error("fake"); + } + m_last_error = reply.error_code; + return reply.ok; + } + + int last_error_code() const { return m_last_error; } + int64_t offset_us() const { return scenario()[m_host].offset_us; } + int64_t delay_us() const { return scenario()[m_host].delay_us; } + int stratum() const { return scenario()[m_host].stratum; } + + static void set_reply(const std::string& host, const FakeReply& reply) { + scenario()[host] = reply; + } + + static void clear() { + scenario().clear(); + } + +private: + static std::unordered_map& scenario() { + static std::unordered_map data; + return data; + } + + std::string m_host; + int m_port; + int m_last_error = 0; +}; + +using Pool = NtpClientPoolT; + +static NtpSample make_sample(bool ok, int64_t offset, int64_t delay, int64_t max_delay) { + NtpSample sample; + sample.is_ok = ok; + sample.offset_us = offset; + sample.delay_us = delay; + sample.max_delay_us = max_delay; + return sample; +} + +int main() { + { + NtpPoolConfig cfg; + cfg.min_valid_samples = 3; + cfg.aggregation = NtpPoolConfig::Aggregation::Median; + cfg.smoothing_alpha = 1.0; + Pool pool(cfg); + + std::vector samples; + samples.push_back(make_sample(true, 100, 0, 0)); + samples.push_back(make_sample(true, 200, 0, 0)); + samples.push_back(make_sample(true, 300, 0, 0)); + + assert(pool.apply_samples(samples)); + assert(pool.offset_us() == 200); + } + + { + NtpPoolConfig cfg; + cfg.min_valid_samples = 2; + cfg.aggregation = NtpPoolConfig::Aggregation::MedianMadTrim; + cfg.smoothing_alpha = 1.0; + Pool pool(cfg); + + std::vector samples; + samples.push_back(make_sample(true, 100, 0, 0)); + samples.push_back(make_sample(true, 105, 0, 0)); + samples.push_back(make_sample(true, 100000, 0, 0)); + + assert(pool.apply_samples(samples)); + assert(pool.offset_us() == 102); + } + + { + NtpPoolConfig cfg; + cfg.min_valid_samples = 1; + cfg.aggregation = NtpPoolConfig::Aggregation::BestDelay; + Pool pool(cfg); + + std::vector samples; + samples.push_back(make_sample(true, 10, 100000, 200000)); + samples.push_back(make_sample(true, 5, 300000, 200000)); + + assert(pool.apply_samples(samples)); + assert(pool.offset_us() == 10); + + samples.clear(); + samples.push_back(make_sample(true, 40, 50000, 200000)); + samples.push_back(make_sample(true, 20, 30000, 200000)); + assert(pool.apply_samples(samples)); + assert(pool.offset_us() == 20); + } + + { + NtpPoolConfig cfg; + cfg.min_valid_samples = 1; + cfg.smoothing_alpha = 2.0; + Pool pool(cfg); + + std::vector samples; + samples.push_back(make_sample(true, 500, 0, 0)); + assert(pool.apply_samples(samples)); + assert(pool.offset_us() == 500); + } + + { + NtpPoolConfig cfg; + cfg.min_valid_samples = 1; + cfg.smoothing_alpha = -0.5; + Pool pool(cfg); + + std::vector samples; + samples.push_back(make_sample(true, 700, 0, 0)); + assert(pool.apply_samples(samples)); + assert(pool.offset_us() == 0); + } + + { + NtpPoolConfig cfg; + cfg.min_valid_samples = 2; + cfg.smoothing_alpha = 1.0; + Pool pool(cfg); + + std::vector samples; + samples.push_back(make_sample(true, 123, 0, 0)); + assert(!pool.apply_samples(samples)); + assert(pool.offset_us() == 0); + } + + { + FakeNtpClient::clear(); + FakeReply a; + a.offset_us = 100; + a.delay_us = 10; + FakeReply b; + b.offset_us = 200; + b.delay_us = 20; + FakeReply c; + c.offset_us = 300; + c.delay_us = 30; + FakeNtpClient::set_reply("a", a); + FakeNtpClient::set_reply("b", b); + FakeNtpClient::set_reply("c", c); + + NtpPoolConfig cfg; + cfg.sample_servers = 3; + cfg.min_valid_samples = 3; + cfg.aggregation = NtpPoolConfig::Aggregation::Median; + cfg.smoothing_alpha = 1.0; + cfg.rng_seed = 1; + + Pool pool(cfg); + std::vector servers; + NtpServerConfig sa; + sa.host = "a"; + sa.port = 123; + servers.push_back(sa); + NtpServerConfig sb; + sb.host = "b"; + sb.port = 123; + servers.push_back(sb); + NtpServerConfig sc; + sc.host = "c"; + sc.port = 123; + servers.push_back(sc); + pool.set_servers(std::move(servers)); + + assert(pool.measure()); + assert(pool.offset_us() == 200); + } + + { + FakeNtpClient::clear(); + FakeReply x; + x.throws_on_query = true; + x.error_code = 5; + FakeNtpClient::set_reply("x", x); + + NtpPoolConfig cfg; + cfg.sample_servers = 1; + cfg.min_valid_samples = 1; + cfg.aggregation = NtpPoolConfig::Aggregation::Median; + cfg.smoothing_alpha = 1.0; + + Pool pool(cfg); + std::vector servers; + NtpServerConfig sx; + sx.host = "x"; + sx.port = 123; + servers.push_back(sx); + pool.set_servers(std::move(servers)); + + assert(!pool.measure_n(1)); + const std::vector samples = pool.last_samples(); + assert(!samples.empty()); + assert(!samples.front().is_ok); + assert(samples.front().error_code != 0); + } + + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/ntp_client_pool_test.cpp b/tests/ntp_client_pool_test.cpp new file mode 100644 index 00000000..bdafddff --- /dev/null +++ b/tests/ntp_client_pool_test.cpp @@ -0,0 +1,65 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT && TIME_SHIELD_PLATFORM_UNIX + +#include +#include + +#include +#include +#include +#include + +int main() { + using namespace time_shield; + + init(); + + NtpPoolConfig pool_cfg; + pool_cfg.sample_servers = 3; + pool_cfg.min_valid_samples = 1; + + NtpClientPool pool(pool_cfg); + pool.set_servers(NtpClientPool::build_default_servers()); + + auto measurement_future = std::async(std::launch::async, [&pool]() { return pool.measure(); }); + + const auto status = measurement_future.wait_for(std::chrono::minutes(5)); + if (status != std::future_status::ready) { + std::cerr << "NtpClientPool measurement timed out" << std::endl; + return 0; // treat as non-fatal in restricted environments + } + + const bool is_updated = measurement_future.get(); + if (!is_updated) { + std::cerr << "NtpClientPool measurement failed" << std::endl; + return 0; // non-fatal: network may be blocked + } + + const std::vector samples = pool.last_samples(); + if (samples.empty()) { + std::cerr << "No NTP samples collected" << std::endl; + return 0; + } + + bool is_any_ok = false; + for (const auto& sample : samples) { + if (sample.is_ok) { + is_any_ok = true; + break; + } + } + + if (!is_any_ok) { + std::cerr << "All NTP samples failed" << std::endl; + return 0; + } + + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/ntp_client_test.cpp b/tests/ntp_client_test.cpp new file mode 100644 index 00000000..31b9523e --- /dev/null +++ b/tests/ntp_client_test.cpp @@ -0,0 +1,163 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT && TIME_SHIELD_PLATFORM_UNIX + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + struct ntp_packet { + uint8_t li_vn_mode; + uint8_t stratum; + uint8_t poll; + uint8_t precision; + uint32_t root_delay; + uint32_t root_dispersion; + uint32_t ref_id; + uint32_t ref_ts_sec; + uint32_t ref_ts_frac; + uint32_t orig_ts_sec; + uint32_t orig_ts_frac; + uint32_t recv_ts_sec; + uint32_t recv_ts_frac; + uint32_t tx_ts_sec; + uint32_t tx_ts_frac; + }; + + constexpr int64_t NTP_TIMESTAMP_DELTA = 2208988800ll; + + uint64_t system_now_us() { + const auto now = std::chrono::system_clock::now(); + const auto us = std::chrono::duration_cast(now.time_since_epoch()); + return static_cast(us.count()); + } + + void pack_ts(uint64_t unix_us, uint32_t& sec, uint32_t& frac) { + const uint64_t seconds = unix_us / 1000000 + static_cast(NTP_TIMESTAMP_DELTA); + const uint64_t fraction = ((unix_us % 1000000) * 0x100000000ULL) / 1000000; + sec = htonl(static_cast(seconds)); + frac = htonl(static_cast(fraction)); + } + + void run_ntp_server(uint16_t port, std::atomic& is_ready) { + const int server_sock = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (server_sock < 0) { + is_ready = true; + return; + } + + sockaddr_in server_addr{}; + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + server_addr.sin_port = htons(port); + + if (bind(server_sock, reinterpret_cast(&server_addr), sizeof(server_addr)) != 0) { + ::close(server_sock); + is_ready = true; + return; + } + + is_ready = true; + + ntp_packet request{}; + sockaddr_in client_addr{}; + socklen_t client_len = sizeof(client_addr); + const ssize_t received = ::recvfrom( + server_sock, + &request, + sizeof(request), + 0, + reinterpret_cast(&client_addr), + &client_len); + + if (received < 0) { + ::close(server_sock); + return; + } + + const uint64_t receive_us = system_now_us(); + + ntp_packet response{}; + response.li_vn_mode = (0 << 6) | (4 << 3) | 4; // LI=0, VN=4, Mode=4 (server) + response.stratum = 1; + response.poll = 4; + response.precision = 0xFA; + response.ref_id = htonl(0x4c4f434c); // "LOCL" + response.orig_ts_sec = request.tx_ts_sec; + response.orig_ts_frac = request.tx_ts_frac; + pack_ts(receive_us, response.recv_ts_sec, response.recv_ts_frac); + pack_ts(system_now_us(), response.tx_ts_sec, response.tx_ts_frac); + + ::sendto( + server_sock, + &response, + sizeof(response), + 0, + reinterpret_cast(&client_addr), + client_len); + + ::close(server_sock); + } + +} // namespace + +int main() { + using namespace time_shield; + + init(); + + constexpr uint16_t port = 12345; + std::atomic is_server_ready{false}; + std::thread server_thread(run_ntp_server, port, std::ref(is_server_ready)); + + while (!is_server_ready.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + NtpClient client("127.0.0.1", port); + const bool is_query_successful = client.query(); + + server_thread.join(); + + if (!is_query_successful || !client.success()) { + std::cerr << "NTP query failed, error code: " << client.last_error_code() << std::endl; + return 1; + } + + const int64_t offset_us = client.offset_us(); + const int64_t tolerance_us = 50000; // 50 ms + if (std::llabs(offset_us) > tolerance_us) { + std::cerr << "Offset is too large: " << offset_us << std::endl; + return 1; + } + + const int64_t utc_us = client.utc_time_us(); + const int64_t host_us = static_cast(system_now_us()); + const int64_t drift_us = std::llabs(utc_us - host_us); + if (drift_us > 500000) { // 0.5s tolerance for local mock server + std::cerr << "UTC drift is too large: " << drift_us << std::endl; + return 1; + } + + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/ntp_time_service_test.cpp b/tests/ntp_time_service_test.cpp new file mode 100644 index 00000000..2e197660 --- /dev/null +++ b/tests/ntp_time_service_test.cpp @@ -0,0 +1,51 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT +#define TIME_SHIELD_TEST_FAKE_NTP +#define TIME_SHIELD_NTP_TIME_SERVICE_DEFINE +#include + +#include +#include +#include + +using namespace time_shield; + +int main() { + auto& service = NtpTimeService::instance(); + + service.shutdown(); + assert(!service.running()); + (void)service.utc_time_ms(); + assert(service.running()); + + service.shutdown(); + assert(service.init(std::chrono::milliseconds(20), true)); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (service.measure_count() < 3 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + service.shutdown(); + assert(!service.running()); + + assert(service.set_default_servers()); + NtpPoolConfig cfg; + cfg.sample_servers = 3; + cfg.min_valid_samples = 2; + assert(service.set_pool_config(cfg)); + assert(service.init()); + + assert(!service.set_default_servers()); + assert(!service.set_pool_config(cfg)); + + service.shutdown(); + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/parse_iso8601_test.cpp b/tests/parse_iso8601_test.cpp new file mode 100644 index 00000000..718641a7 --- /dev/null +++ b/tests/parse_iso8601_test.cpp @@ -0,0 +1,102 @@ +#include + +#include +#include + +int main() { + using namespace time_shield; + + auto check_dt = [](const DateTimeStruct& dt, + int year, + int month, + int day, + int hour, + int minute, + int second, + int millisecond) { + assert(dt.year == year); + assert(dt.mon == month); + assert(dt.day == day); + assert(dt.hour == hour); + assert(dt.min == minute); + assert(dt.sec == second); + assert(dt.ms == millisecond); + (void)dt; + (void)year; + (void)month; + (void)day; + (void)hour; + (void)minute; + (void)second; + (void)millisecond; + }; + + auto check_tz = [](const TimeZoneStruct& tz, bool is_positive, int hour, int minute) { + assert(tz.is_positive == is_positive); + assert(tz.hour == hour); + assert(tz.min == minute); + (void)tz; + (void)is_positive; + (void)hour; + (void)minute; + }; + + DateTimeStruct dt{}; + TimeZoneStruct tz{}; + + auto parse_and_check = [&](const std::string& input, + int year, + int month, + int day, + int hour, + int minute, + int second, + int millisecond, + bool is_positive_tz, + int tz_hour, + int tz_minute) { + dt = create_date_time_struct(0); + tz = create_time_zone_struct(0, 0); + tz.is_positive = true; + + const bool is_parsed = parse_iso8601(input, dt, tz); + assert(is_parsed); + (void)is_parsed; + + check_dt(dt, year, month, day, hour, minute, second, millisecond); + check_tz(tz, is_positive_tz, tz_hour, tz_minute); + }; + + // Date only. + parse_and_check("2024-07-08", 2024, 7, 8, 0, 0, 0, 0, true, 0, 0); + + // Date with time to minutes. + parse_and_check("2024-07-08T12:34", 2024, 7, 8, 12, 34, 0, 0, true, 0, 0); + + // Date/time to seconds. + parse_and_check("2024-07-08T12:34:56", 2024, 7, 8, 12, 34, 56, 0, true, 0, 0); + + // Date/time with fractional seconds. + parse_and_check("2024-07-08T12:34:56.789", 2024, 7, 8, 12, 34, 56, 789, true, 0, 0); + + // Zulu timezone. + parse_and_check("2024-07-08T12:34:56Z", 2024, 7, 8, 12, 34, 56, 0, true, 0, 0); + + // Positive timezone offset. + parse_and_check("2024-07-08T12:34:56+05:30", 2024, 7, 8, 12, 34, 56, 0, true, 5, 30); + + // Negative timezone offset. + parse_and_check("2024-07-08T12:34:56-02:15", 2024, 7, 8, 12, 34, 56, 0, false, 2, 15); + + // Whitespace separator and trailing spaces. + parse_and_check("2024-07-08 12:34:56 ", 2024, 7, 8, 12, 34, 56, 0, true, 0, 0); + + // Alternate date separators. + parse_and_check("2024/07/08T12:34", 2024, 7, 8, 12, 34, 0, 0, true, 0, 0); + parse_and_check("2024.07.08T12:34:00-03:00", 2024, 7, 8, 12, 34, 0, 0, false, 3, 0); + + // Leading spaces. + parse_and_check(" 2024-07-08T12:34:56Z", 2024, 7, 8, 12, 34, 56, 0, true, 0, 0); + + return 0; +} diff --git a/tests/test_fast_date64.cpp b/tests/test_fast_date64.cpp new file mode 100644 index 00000000..144946c0 --- /dev/null +++ b/tests/test_fast_date64.cpp @@ -0,0 +1,405 @@ +#include + +#if defined(_WIN32) +# ifdef min +# undef min +# endif +# ifdef max +# undef max +# endif +#endif + +#include +#include +#include +#include +#include +#include + +namespace { + + struct CivilDate { + int64_t year; + int month; + int day; + }; + + /// \brief Reference conversion from days since Unix epoch to civil date. + TIME_SHIELD_CONSTEXPR CivilDate civil_from_days(int64_t z) noexcept { + z += 719468; + const int64_t era = (z >= 0 ? z : z - 146096) / 146097; + const int64_t doe = z - era * 146097; + const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + int64_t y = static_cast(yoe) + era * 400; + const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + const int64_t mp = (5 * doy + 2) / 153; + const int64_t d = doy - (153 * mp + 2) / 5 + 1; + const int64_t m = mp + (mp < 10 ? 3 : -9); + y += (m <= 2); + return {y, static_cast(m), static_cast(d)}; + } + + /// \brief Reference conversion from civil date to days since Unix epoch. + TIME_SHIELD_CONSTEXPR int64_t days_from_civil(int64_t p_year, int p_month, int p_day) noexcept { + const int64_t y = p_year - (p_month <= 2 ? 1 : 0); + const int64_t era = (y >= 0 ? y : y - 399) / 400; + const int64_t yoe = y - era * 400; + const int64_t month = p_month + (p_month > 2 ? -3 : 9); + const int64_t doy = (153 * month + 2) / 5 + static_cast(p_day) - 1; + const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; + } + + struct DateParts { + int64_t year; + int month; + int day; + int hour; + int minute; + int second; + }; + + /// \brief Reference conversion from UNIX seconds to date parts. + DateParts reference_from_timestamp(int64_t ts) noexcept { + int64_t days = ts / time_shield::SEC_PER_DAY; + int64_t sec_of_day = ts % time_shield::SEC_PER_DAY; + if (sec_of_day < 0) { + sec_of_day += time_shield::SEC_PER_DAY; + days -= 1; + } + + const CivilDate civil = civil_from_days(days); + const int64_t hour = sec_of_day / time_shield::SEC_PER_HOUR; + const int64_t min = (sec_of_day - hour * time_shield::SEC_PER_HOUR) / time_shield::SEC_PER_MIN; + const int64_t sec = sec_of_day - hour * time_shield::SEC_PER_HOUR - min * time_shield::SEC_PER_MIN; + + return { + civil.year, + civil.month, + civil.day, + static_cast(hour), + static_cast(min), + static_cast(sec) + }; + } + + void assert_date_parts(const time_shield::DateTimeStruct& dt, const DateParts& ref) { + assert(dt.year == ref.year); + assert(dt.mon == ref.month); + assert(dt.day == ref.day); + assert(dt.hour == ref.hour); + assert(dt.min == ref.minute); + assert(dt.sec == ref.second); + (void)dt; + (void)ref; + } + + void assert_reference_match(int64_t ts) { + const DateParts ref = reference_from_timestamp(ts); + const time_shield::DateTimeStruct dt = time_shield::to_date_time(ts); + assert_date_parts(dt, ref); + + const int64_t year_ref = ref.year - time_shield::UNIX_EPOCH; + const int64_t year_fast = time_shield::years_since_epoch(ts); + assert(year_fast == year_ref); + (void)year_ref; + (void)year_fast; + } + + void assert_date_to_unix_day_match(int64_t year, int month, int day) { + const int64_t ref_day = days_from_civil(year, month, day); + const time_shield::dse_t fast_day = time_shield::date_to_unix_day(year, month, day); + const time_shield::dse_t legacy_day = time_shield::legacy::date_to_unix_day(year, month, day); + + assert(static_cast(fast_day) == ref_day); + assert(static_cast(legacy_day) == ref_day); + (void)ref_day; + (void)fast_day; + (void)legacy_day; + } + + void assert_to_timestamp_match(int64_t year, int month, int day, int hour, int minute, int second) { + const int64_t ref_day = days_from_civil(year, month, day); + const int64_t ref_ts = ref_day * time_shield::SEC_PER_DAY + + hour * time_shield::SEC_PER_HOUR + + minute * time_shield::SEC_PER_MIN + + second; + + const time_shield::ts_t fast_ts = time_shield::to_timestamp(year, month, day, hour, minute, second); + const time_shield::ts_t legacy_ts = time_shield::legacy::to_timestamp(year, month, day, hour, minute, second); + + assert(static_cast(fast_ts) == ref_ts); + assert(static_cast(legacy_ts) == ref_ts); + (void)ref_ts; + (void)fast_ts; + (void)legacy_ts; + } + + void test_known_cases() { + assert_reference_match(0); + assert_reference_match(time_shield::to_timestamp(2000, 2, 29, 0, 0, 0)); + assert_reference_match(time_shield::to_timestamp(1900, 2, 28, 0, 0, 0)); + assert_reference_match(time_shield::to_timestamp(1900, 3, 1, 0, 0, 0)); + assert_reference_match(time_shield::to_timestamp(2100, 2, 28, 0, 0, 0)); + assert_reference_match(time_shield::to_timestamp(2100, 3, 1, 0, 0, 0)); + assert_reference_match(-1); + assert_reference_match(-time_shield::SEC_PER_DAY); + assert_reference_match(-time_shield::SEC_PER_DAY - 1); + + assert_date_to_unix_day_match(1970, 1, 1); + assert_date_to_unix_day_match(2000, 2, 29); + assert_date_to_unix_day_match(1900, 2, 28); + assert_date_to_unix_day_match(1900, 3, 1); + assert_date_to_unix_day_match(2100, 2, 28); + assert_date_to_unix_day_match(2100, 3, 1); + + assert_to_timestamp_match(1970, 1, 1, 0, 0, 0); + assert_to_timestamp_match(2000, 2, 29, 23, 59, 59); + assert_to_timestamp_match(1900, 2, 28, 12, 0, 0); + assert_to_timestamp_match(1900, 3, 1, 0, 0, 1); + assert_to_timestamp_match(2100, 2, 28, 11, 22, 33); + assert_to_timestamp_match(2100, 3, 1, 4, 5, 6); + } + + void test_random_ranges() { + std::mt19937_64 rng(0x6c6f6e675f72616eULL); + std::uniform_int_distribution epoch_dist( + -static_cast(1ULL << 32), + static_cast(1ULL << 32)); + + const int64_t window = static_cast(1ULL << 32); + const int64_t min_ts = std::numeric_limits::min(); + const int64_t max_ts = std::numeric_limits::max(); + std::uniform_int_distribution lower_tail(min_ts, min_ts + window); + std::uniform_int_distribution upper_tail(max_ts - window, max_ts); + + const int64_t sample_count = 1000000; + for (int64_t i = 0; i < sample_count; ++i) { + assert_reference_match(epoch_dist(rng)); + assert_reference_match(lower_tail(rng)); + assert_reference_match(upper_tail(rng)); + } + + std::uniform_int_distribution year_dist(1600, 2400); + std::uniform_int_distribution month_dist(1, 12); + std::uniform_int_distribution hour_dist(0, 23); + std::uniform_int_distribution minute_dist(0, 59); + std::uniform_int_distribution second_dist(0, 59); + const int date_samples = 500000; + for (int i = 0; i < date_samples; ++i) { + const int year = year_dist(rng); + const int month = month_dist(rng); + const int day_limit = time_shield::days_in_month(year, month); + std::uniform_int_distribution day_dist(1, day_limit); + const int day = day_dist(rng); + + assert_date_to_unix_day_match(year, month, day); + + const int hour = hour_dist(rng); + const int minute = minute_dist(rng); + const int second = second_dist(rng); + assert_to_timestamp_match(year, month, day, hour, minute, second); + } + } + + void test_round_trip() { + std::mt19937_64 rng(0x726f756e645f7472ULL); + std::uniform_int_distribution year_dist(1600, 2400); + std::uniform_int_distribution month_dist(1, 12); + std::uniform_int_distribution hour_dist(0, 23); + std::uniform_int_distribution minute_dist(0, 59); + std::uniform_int_distribution second_dist(0, 59); + + const int iterations = 200000; + for (int i = 0; i < iterations; ++i) { + const int year = year_dist(rng); + const int month = month_dist(rng); + const int day_limit = time_shield::days_in_month(year, month); + std::uniform_int_distribution day_dist(1, day_limit); + const int day = day_dist(rng); + const int hour = hour_dist(rng); + const int minute = minute_dist(rng); + const int second = second_dist(rng); + + const time_shield::ts_t ts = time_shield::to_timestamp(year, month, day, hour, minute, second); + const time_shield::DateTimeStruct dt = time_shield::to_date_time(ts); + + assert(dt.year == year); + assert(dt.mon == month); + assert(dt.day == day); + assert(dt.hour == hour); + assert(dt.min == minute); + assert(dt.sec == second); + } + } + + void run_benchmark() { + constexpr int64_t iterations = 10000000; + int64_t accumulator_fast = 0; + int64_t accumulator_legacy = 0; + + const int64_t start_ts = -static_cast(1ULL << 32); + const int64_t step = 4093; + + const auto start_fast = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int64_t ts = start_ts + i * step; + const time_shield::DateTimeStruct dt = time_shield::to_date_time(ts); + accumulator_fast += dt.year + dt.mon + dt.day; + } + const auto end_fast = std::chrono::steady_clock::now(); + + const auto start_legacy = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int64_t ts = start_ts + i * step; + const time_shield::DateTimeStruct dt = time_shield::legacy::to_date_time(ts); + accumulator_legacy += dt.year + dt.mon + dt.day; + } + const auto end_legacy = std::chrono::steady_clock::now(); + + const auto fast_ns = std::chrono::duration_cast(end_fast - start_fast).count(); + const auto legacy_ns = std::chrono::duration_cast(end_legacy - start_legacy).count(); + + std::cout << "fast_date64 benchmark (1e7 timestamps)\n"; + std::cout << "fast_ns: " << fast_ns << " legacy_ns: " << legacy_ns << '\n'; + if (fast_ns > 0) { + const double ratio = static_cast(legacy_ns) / static_cast(fast_ns); + std::cout << "legacy/fast ratio: " << ratio << '\n'; + } + std::cout << "accumulators: " << accumulator_fast << " " << accumulator_legacy << '\n'; + + auto bench_date_range = [&](const char* label, int base_year, int year_span, int month_cycle) { + int64_t acc_day_fast = 0; + int64_t acc_day_legacy = 0; + const auto start_day_fast_local = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int year = base_year + static_cast(i % year_span); + const int month = 1 + static_cast(i % month_cycle); + const int day = 1 + static_cast(i % 28); + const time_shield::dse_t unix_day = time_shield::date_to_unix_day(year, month, day); + acc_day_fast += unix_day; + } + const auto end_day_fast_local = std::chrono::steady_clock::now(); + + const auto start_day_legacy_local = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int year = base_year + static_cast(i % year_span); + const int month = 1 + static_cast(i % month_cycle); + const int day = 1 + static_cast(i % 28); + const time_shield::dse_t unix_day = time_shield::legacy::date_to_unix_day(year, month, day); + acc_day_legacy += unix_day; + } + const auto end_day_legacy_local = std::chrono::steady_clock::now(); + + const auto day_fast_ns = std::chrono::duration_cast( + end_day_fast_local - start_day_fast_local).count(); + const auto day_legacy_ns = std::chrono::duration_cast( + end_day_legacy_local - start_day_legacy_local).count(); + + std::cout << "date_to_unix_day benchmark (" << label << ")\n"; + std::cout << "fast_ns: " << day_fast_ns << " legacy_ns: " << day_legacy_ns << '\n'; + if (day_fast_ns > 0) { + const double ratio = static_cast(day_legacy_ns) / static_cast(day_fast_ns); + std::cout << "legacy/fast ratio: " << ratio << '\n'; + } + std::cout << "accumulators: " << acc_day_fast << " " << acc_day_legacy << '\n'; + + int64_t acc_ts_fast = 0; + int64_t acc_ts_legacy = 0; + const auto start_ts_fast_local = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int year = base_year + static_cast(i % year_span); + const int month = 1 + static_cast(i % month_cycle); + const int day = 1 + static_cast(i % 28); + const int hour = static_cast(i % 24); + const int minute = static_cast(i % 60); + const int second = static_cast(i % 60); + acc_ts_fast += time_shield::to_timestamp(year, month, day, hour, minute, second); + } + const auto end_ts_fast_local = std::chrono::steady_clock::now(); + + const auto start_ts_legacy_local = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int year = base_year + static_cast(i % year_span); + const int month = 1 + static_cast(i % month_cycle); + const int day = 1 + static_cast(i % 28); + const int hour = static_cast(i % 24); + const int minute = static_cast(i % 60); + const int second = static_cast(i % 60); + acc_ts_legacy += time_shield::legacy::to_timestamp(year, month, day, hour, minute, second); + } + const auto end_ts_legacy_local = std::chrono::steady_clock::now(); + + const auto ts_fast_ns = std::chrono::duration_cast( + end_ts_fast_local - start_ts_fast_local).count(); + const auto ts_legacy_ns = std::chrono::duration_cast( + end_ts_legacy_local - start_ts_legacy_local).count(); + + std::cout << "to_timestamp benchmark (" << label << ")\n"; + std::cout << "fast_ns: " << ts_fast_ns << " legacy_ns: " << ts_legacy_ns << '\n'; + if (ts_fast_ns > 0) { + const double ratio = static_cast(ts_legacy_ns) / static_cast(ts_fast_ns); + std::cout << "legacy/fast ratio: " << ratio << '\n'; + } + std::cout << "accumulators: " << acc_ts_fast << " " << acc_ts_legacy << '\n'; + }; + + auto bench_timestamp_math_range = [&](const char* label, int base_year, int year_span, int month_cycle) { + int64_t acc_fast = 0; + int64_t acc_legacy = 0; + const auto start_fast_local = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int year = base_year + static_cast(i % year_span); + const int month = 1 + static_cast(i % month_cycle); + const int day = 1 + static_cast(i % 28); + const int hour = static_cast(i % 24); + const int minute = static_cast(i % 60); + const int second = static_cast(i % 60); + acc_fast += time_shield::to_timestamp_unchecked(year, month, day, hour, minute, second); + } + const auto end_fast_local = std::chrono::steady_clock::now(); + + const auto start_legacy_local = std::chrono::steady_clock::now(); + for (int64_t i = 0; i < iterations; ++i) { + const int year = base_year + static_cast(i % year_span); + const int month = 1 + static_cast(i % month_cycle); + const int day = 1 + static_cast(i % 28); + const int hour = static_cast(i % 24); + const int minute = static_cast(i % 60); + const int second = static_cast(i % 60); + acc_legacy += time_shield::legacy::to_timestamp_unchecked(year, month, day, hour, minute, second); + } + const auto end_legacy_local = std::chrono::steady_clock::now(); + + const auto unchecked_fast_ns = std::chrono::duration_cast( + end_fast_local - start_fast_local).count(); + const auto unchecked_legacy_ns = std::chrono::duration_cast( + end_legacy_local - start_legacy_local).count(); + + std::cout << "to_timestamp unchecked benchmark (" << label << ")\n"; + std::cout << "fast_ns: " << unchecked_fast_ns << " legacy_ns: " << unchecked_legacy_ns << '\n'; + if (unchecked_fast_ns > 0) { + const double ratio = static_cast(unchecked_legacy_ns) / static_cast(unchecked_fast_ns); + std::cout << "legacy/fast ratio: " << ratio << '\n'; + } + std::cout << "accumulators: " << acc_fast << " " << acc_legacy << '\n'; + }; + + bench_date_range("1970..2100", 1970, 131, 12); + bench_date_range("1600..1969", 1600, 370, 12); + bench_date_range("1970..2400", 1970, 431, 12); + bench_date_range("wide 1600..2400", 1600, 801, 12); + bench_date_range("negative -2400..-1600", -2400, 801, 12); + bench_timestamp_math_range("1970..2100", 1970, 131, 12); + bench_timestamp_math_range("1600..1969", 1600, 370, 12); + bench_timestamp_math_range("1970..2400", 1970, 431, 12); + } +} + +int main() { + test_known_cases(); + test_random_ranges(); + test_round_trip(); + run_benchmark(); + return 0; +} diff --git a/tests/test_ntp_client_protocol_validation.cpp b/tests/test_ntp_client_protocol_validation.cpp new file mode 100644 index 00000000..6b02fd70 --- /dev/null +++ b/tests/test_ntp_client_protocol_validation.cpp @@ -0,0 +1,247 @@ +#include + +#if TIME_SHIELD_ENABLE_NTP_CLIENT + +#include +#include +#include +#include + +#include +#include +#include + +using namespace time_shield; + +class FakeUdpTransport : public detail::IUdpTransport { +public: + bool is_ok = true; + int error_code = 0; + detail::NtpPacket reply{}; + + bool transact(const detail::UdpRequest& req, int& out_error_code) noexcept override { + assert(req.send_size == sizeof(detail::NtpPacket)); + assert(req.recv_size == sizeof(detail::NtpPacket)); + out_error_code = error_code; + if (!is_ok) { + return false; + } + if (req.recv_data && req.recv_size == sizeof(detail::NtpPacket)) { + std::memcpy(req.recv_data, &reply, sizeof(reply)); + } + out_error_code = 0; + return true; + } +}; + +static uint8_t make_li_vn_mode(uint8_t li, uint8_t vn, uint8_t mode) { + return static_cast((li << 6) | (vn << 3) | mode); +} + +static void unix_us_to_ntp_ts(uint64_t unix_us, uint32_t& sec_net, uint32_t& frac_net) { + const uint64_t sec = unix_us / 1000000 + 2208988800ULL; + const uint64_t frac = ((unix_us % 1000000) * 0x100000000ULL) / 1000000; + sec_net = htonl(static_cast(sec)); + frac_net = htonl(static_cast(frac)); +} + +static detail::NtpPacket build_base_packet(uint64_t base_us) { + detail::NtpPacket pkt{}; + pkt.li_vn_mode = make_li_vn_mode(0, 3, 4); + pkt.stratum = 2; + + uint32_t sec = 0; + uint32_t frac = 0; + unix_us_to_ntp_ts(base_us - 4000, sec, frac); + pkt.orig_ts_sec = sec; + pkt.orig_ts_frac = frac; + + unix_us_to_ntp_ts(base_us - 2000, sec, frac); + pkt.recv_ts_sec = sec; + pkt.recv_ts_frac = frac; + + unix_us_to_ntp_ts(base_us - 1000, sec, frac); + pkt.tx_ts_sec = sec; + pkt.tx_ts_frac = frac; + + return pkt; +} + +int main() { + const int64_t now_us = time_shield::now_realtime_us(); + assert(now_us > 0); + const uint64_t base_us = static_cast(now_us); + + { + // Valid server response + FakeUdpTransport transport; + transport.reply = build_base_packet(base_us); + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(is_ok); + assert(error == 0); + assert(stratum == 2); + assert(delay >= 0); + (void)is_ok; + (void)error; + (void)offset; + (void)delay; + (void)stratum; + } + + { + // Bad mode + FakeUdpTransport transport; + detail::NtpPacket pkt = build_base_packet(base_us); + pkt.li_vn_mode = make_li_vn_mode(0, 3, 3); + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_BAD_MODE); + (void)is_ok; + } + + { + // Bad version + FakeUdpTransport transport; + detail::NtpPacket pkt = build_base_packet(base_us); + pkt.li_vn_mode = make_li_vn_mode(0, 0, 4); + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_BAD_VERSION); + (void)is_ok; + } + + { + // LI alarm + FakeUdpTransport transport; + detail::NtpPacket pkt = build_base_packet(base_us); + pkt.li_vn_mode = make_li_vn_mode(3, 3, 4); + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_BAD_LI); + (void)is_ok; + } + + { + // KoD + FakeUdpTransport transport; + detail::NtpPacket pkt = build_base_packet(base_us); + pkt.stratum = 0; + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_KOD); + (void)is_ok; + } + + { + // Unsynchronized stratum + FakeUdpTransport transport; + detail::NtpPacket pkt = build_base_packet(base_us); + pkt.stratum = 16; + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_BAD_STRATUM); + (void)is_ok; + } + + { + // Bad timestamps + FakeUdpTransport transport; + detail::NtpPacket pkt = build_base_packet(base_us); + pkt.orig_ts_sec = 0; + pkt.orig_ts_frac = 0; + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_BAD_TS); + (void)is_ok; + } + + { + // Negative delay + FakeUdpTransport transport; + detail::NtpPacket pkt{}; + pkt.li_vn_mode = make_li_vn_mode(0, 3, 4); + pkt.stratum = 2; + + uint32_t sec = 0; + uint32_t frac = 0; + unix_us_to_ntp_ts(base_us - 1000000, sec, frac); + pkt.orig_ts_sec = sec; + pkt.orig_ts_frac = frac; + + unix_us_to_ntp_ts(base_us - 900000, sec, frac); + pkt.recv_ts_sec = sec; + pkt.recv_ts_frac = frac; + + unix_us_to_ntp_ts(base_us + 1000000, sec, frac); + pkt.tx_ts_sec = sec; + pkt.tx_ts_frac = frac; + + transport.reply = pkt; + + detail::NtpClientCore core; + int error = 0; + int64_t offset = 0; + int64_t delay = 0; + int stratum = -1; + const bool is_ok = core.query(transport, "example.com", 123, 5000, error, offset, delay, stratum); + assert(!is_ok); + assert(error == detail::NTP_E_BAD_TS); + (void)is_ok; + } + + return 0; +} + +#else +int main() { + return 0; +} +#endif diff --git a/tests/test_timestamp_ms_pre_epoch.cpp b/tests/test_timestamp_ms_pre_epoch.cpp new file mode 100644 index 00000000..557015d1 --- /dev/null +++ b/tests/test_timestamp_ms_pre_epoch.cpp @@ -0,0 +1,57 @@ +#include +#include + +#include +#include + +namespace { + int64_t expected_ts_ms(int64_t year, int month, int day, int hour, int min, int sec, int ms) { + const int64_t unix_day = static_cast(time_shield::date_to_unix_day(year, month, day)); + int64_t sec_value = unix_day * static_cast(time_shield::SEC_PER_DAY) + + static_cast(hour) * static_cast(time_shield::SEC_PER_HOUR) + + static_cast(min) * static_cast(time_shield::SEC_PER_MIN) + + static_cast(sec); + int64_t ms_value = static_cast(ms); + sec_value += time_shield::detail::floor_div(ms_value, static_cast(time_shield::MS_PER_SEC)); + ms_value = time_shield::detail::floor_mod(ms_value, static_cast(time_shield::MS_PER_SEC)); + return sec_value * static_cast(time_shield::MS_PER_SEC) + ms_value; + } +} + +int main() { + using namespace time_shield; + + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 0) == -1000); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 1) == -999); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 500) == -500); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 999) == -1); + assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, 0) == 0); + assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, 1) == 1); + + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 1000) == + expected_ts_ms(1969, 12, 31, 23, 59, 59, 1000)); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, -1) == + expected_ts_ms(1969, 12, 31, 23, 59, 59, -1)); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, -1000) == + expected_ts_ms(1969, 12, 31, 23, 59, 59, -1000)); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, -1001) == + expected_ts_ms(1969, 12, 31, 23, 59, 59, -1001)); + assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 1500) == + expected_ts_ms(1969, 12, 31, 23, 59, 59, 1500)); + assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, -1) == + expected_ts_ms(1970, 1, 1, 0, 0, 0, -1)); + assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, 1234567) == + expected_ts_ms(1970, 1, 1, 0, 0, 0, 1234567)); + + assert(to_timestamp_ms(1900, 3, 1, 0, 0, 0, 123) == + expected_ts_ms(1900, 3, 1, 0, 0, 0, 123)); + assert(to_timestamp_ms(2000, 2, 29, 12, 34, 56, 789) == + expected_ts_ms(2000, 2, 29, 12, 34, 56, 789)); + assert(to_timestamp_ms(2100, 3, 1, 0, 0, 0, 0) == + expected_ts_ms(2100, 3, 1, 0, 0, 0, 0)); + + DateTimeStruct dt{1969, 12, 31, 23, 59, 59, 500}; + assert(dt_to_timestamp_ms(dt) == to_timestamp_ms(1969, 12, 31, 23, 59, 59, 500)); + + return 0; +} diff --git a/tests/test_year_boundaries_ms.cpp b/tests/test_year_boundaries_ms.cpp new file mode 100644 index 00000000..b77dbb6b --- /dev/null +++ b/tests/test_year_boundaries_ms.cpp @@ -0,0 +1,77 @@ +#include +#include + +#include +#include +#include + +int main() { + using namespace time_shield; + + const auto check_ms = [](ts_ms_t ts_ms) { + const int64_t sec_floor = detail::floor_div( + static_cast(ts_ms), MS_PER_SEC); + const ts_t start_sec = start_of_year(static_cast(sec_floor)); + const ts_t end_sec = end_of_year(static_cast(sec_floor)); + const ts_ms_t ref_start_ms = static_cast(start_sec * MS_PER_SEC); + const ts_ms_t ref_end_ms = static_cast(end_sec * MS_PER_SEC + MS_PER_SEC - 1); + assert(start_of_year_ms(ts_ms) == ref_start_ms); + assert(end_of_year_ms(ts_ms) == ref_end_ms); + assert(ref_start_ms <= ts_ms); + assert(ts_ms <= ref_end_ms); + }; + + const std::array near_epoch_ms = { + -2001, -2000, -1001, -1000, -999, -2, -1, 0, 1, 999 + }; + for (ts_ms_t ts_ms : near_epoch_ms) { + check_ms(ts_ms); + } + check_ms(1000); + check_ms(1001); + + const std::array years = { + 1969, 1970, 1971, 1972, 1900, 2000, 2100, 2400, 1600, -2400 + }; + const std::array start_offsets = { + -1000, -999, -1, 0, 1, 999, 1000 + }; + const std::array end_offsets = { + -1000, -1, 0, 1, 1000 + }; + + for (int64_t year : years) { + const ts_t year_start_sec = to_timestamp(year, 1, 1, 0, 0, 0); + const ts_ms_t year_start_ms = static_cast(year_start_sec * MS_PER_SEC); + for (int64_t offset : start_offsets) { + check_ms(year_start_ms + offset); + } + + const ts_t year_mid_sec = to_timestamp(year, 6, 1, 0, 0, 0); + const ts_t year_end_sec = end_of_year(year_mid_sec); + const ts_ms_t year_end_ms = static_cast(year_end_sec * MS_PER_SEC + MS_PER_SEC - 1); + for (int64_t offset : end_offsets) { + check_ms(year_end_ms + offset); + } + } + + const std::array extremes = { + MIN_TIMESTAMP_MS + 1, + MIN_TIMESTAMP_MS + 999, + MIN_TIMESTAMP_MS + 1000, + MIN_TIMESTAMP_MS, + MIN_TIMESTAMP_MS - 1, + MIN_TIMESTAMP_MS - 999, + MIN_TIMESTAMP_MS - 1000, + MAX_TIMESTAMP_MS, + MAX_TIMESTAMP_MS - 1, + MAX_TIMESTAMP_MS - 999, + MAX_TIMESTAMP_MS - 1000 + }; + for (ts_ms_t ts_ms : extremes) { + check_ms(ts_ms); + } + + (void)detail::floor_mod(0, 1); + return 0; +} diff --git a/tests/time_boundaries_test.cpp b/tests/time_boundaries_test.cpp index 473b9da2..c58d6677 100644 --- a/tests/time_boundaries_test.cpp +++ b/tests/time_boundaries_test.cpp @@ -33,5 +33,20 @@ int main() { ts_ms_t start_ms = ts_ms("2020-01-01T00:00:00Z"); assert(frac_ms - start_ms == 123); + (void)feb29; + (void)march1; + (void)feb28; + (void)march1_2023; + (void)apr_end; + (void)may_start; + (void)dec_start; + (void)dec_end; + (void)jan_start; + (void)jan_end; + (void)start_day; + (void)end_day; + (void)frac_ms; + (void)start_ms; + return 0; } diff --git a/tests/time_conversions_coverage_test.cpp b/tests/time_conversions_coverage_test.cpp new file mode 100644 index 00000000..1048c56b --- /dev/null +++ b/tests/time_conversions_coverage_test.cpp @@ -0,0 +1,363 @@ +#include +#include + +#include +#include +#include + +int main() { + using namespace time_shield; + + // time_unit_conversions + assert(ns_of_sec(1.25) == 250000000); + assert(us_of_sec(1.5) == 500000); + assert(ms_of_sec(2.5) == 500); + assert(ms_of_ts(1234) == 234); + assert(ms_to_sec(-1) == -1); + assert(ms_to_sec(-1000) == -1); + assert(ms_to_sec(-1001) == -2); + assert(sec_to_ms<>(2) == 2000); + assert(sec_to_ms(3.5) == 3500); + assert(fsec_to_ms(1.1) == 1100); + assert(ms_to_sec<>(1500) == 1); + assert(ms_to_fsec(2500) == 2.5); + assert(min_to_ms<>(2) == 120000); + assert(min_to_ms(1.5) == 90000); + assert(ms_to_min<>(60000) == 1); + assert(min_to_sec<>(1.5) == 90); + assert(sec_to_min<>(180) == 3); + assert(ms_part(-1) == 999); + assert(ms_part(-1000) == 0); + assert(ms_part(-1001) == 999); + assert(us_part(static_cast(-1)) == 999999); + assert(ns_part(-1) == 999999999); + assert(ms_of_sec(-1.2) == 800); + assert(us_of_sec(-1.2) == 800000); + assert(ns_of_sec(-1.2) == 800000000); + assert(min_to_fsec(2) == static_cast(SEC_PER_MIN * 2)); + assert(sec_to_fmin(180) == 3.0); + assert(hour_to_ms<>(1) == MS_PER_HOUR); + assert(ms_to_hour<>(MS_PER_HOUR) == 1); + assert(hour_to_fsec(1) == static_cast(SEC_PER_HOUR)); + assert(sec_to_fhour(7200) == 2.0); + assert(sec_to_hour<>(7200) == 2); + assert(hour_to_sec<>(1) == 3600); + assert(sec_to_hour<>(5400) == 1); + + // unix_time_conversions and aliases + const ts_t unix_day_two_ts = unix_day_to_ts(2); + assert(unix_day_to_timestamp(2) == unix_day_two_ts); + assert(unixday_to_ts(2) == unix_day_two_ts); + assert(uday_to_ts(2) == unix_day_two_ts); + assert(start_of_day_from_unix_day(2) == unix_day_two_ts); + + const ts_ms_t unix_day_two_ms = unix_day_to_ts_ms(2); + assert(unix_day_to_timestamp_ms(2) == unix_day_two_ms); + assert(unixday_to_ts_ms(2) == unix_day_two_ms); + assert(uday_to_ts_ms(2) == unix_day_two_ms); + assert(start_of_day_from_unix_day_ms(2) == unix_day_two_ms); + + assert(end_of_day_from_unix_day(0) == SEC_PER_DAY - 1); + assert(end_of_day_from_unix_day_ms(0) == MS_PER_DAY - 1); + assert(start_of_next_day_from_unix_day(0) == SEC_PER_DAY); + assert(next_day_from_unix_day(0) == SEC_PER_DAY); + assert(next_day_unix_day(0) == SEC_PER_DAY); + assert(next_day_unixday(0) == SEC_PER_DAY); + + assert(start_of_next_day_from_unix_day_ms(0) == MS_PER_DAY); + assert(next_day_from_unix_day_ms(0) == MS_PER_DAY); + assert(next_day_unix_day_ms(0) == MS_PER_DAY); + assert(next_day_unixday_ms(0) == MS_PER_DAY); + + assert(days_since_epoch(SEC_PER_DAY) == 1); + assert(get_unixday(SEC_PER_DAY) == 1); + assert(unix_day(SEC_PER_DAY) == 1); + assert(unixday(SEC_PER_DAY) == 1); + assert(uday(SEC_PER_DAY) == 1); + assert(get_unix_day(SEC_PER_DAY) == 1); + + assert(days_since_epoch_ms(MS_PER_DAY) == 1); + assert(get_unixday_ms(MS_PER_DAY) == 1); + assert(unix_day_ms(MS_PER_DAY) == 1); + assert(unixday_ms(MS_PER_DAY) == 1); + assert(uday_ms(MS_PER_DAY) == 1); + assert(get_unix_day_ms(MS_PER_DAY) == 1); + + assert(days_between(0, SEC_PER_DAY * 3) == 3); + + assert(date_to_unix_day(1970, 1, 1) == 0); + assert(years_since_epoch(static_cast(SEC_PER_YEAR)) == 1); + + const ts_t minute_mark = SEC_PER_MIN * 5; + assert(min_since_epoch<>(minute_mark) == 5); + assert(minutes_since_epoch(minute_mark) == 5); + assert(unix_min(minute_mark) == 5); + assert(to_unix_min(minute_mark) == 5); + assert(umin(minute_mark) == 5); + assert(get_unix_min(minute_mark) == 5); + + assert(sec_of_day(SEC_PER_DAY + 10) == 10); + assert(sec_of_day_ms(MS_PER_DAY + 2000) == 2); + assert((sec_of_day(1, 1, 1) == SEC_PER_HOUR + SEC_PER_MIN + 1)); + assert(sec_of_min(SEC_PER_MIN + 7) == 7); + assert(sec_of_hour(SEC_PER_HOUR + 15) == 15); + + // date_conversions and aliases + { + const ts_ms_t points[] = { + static_cast(-2001), static_cast(-2000), static_cast(-1999), + static_cast(-1001), static_cast(-1000), static_cast(-999), + static_cast(-2), static_cast(-1), static_cast(0), + static_cast(1), static_cast(999), static_cast(1000), + static_cast(1001) + }; + + for (size_t i = 0; i < sizeof(points)/sizeof(points[0]); ++i) { + const ts_ms_t t = points[i]; + + // Day boundaries in ms: + const ts_ms_t d0 = start_of_day_ms(t); + const ts_ms_t d1 = end_of_day_ms(t); + assert(d0 <= t && t <= d1); + assert(ms_part(d0) == 0); + assert(ms_part(d1) == (MS_PER_SEC - 1)); + + // Year boundaries in ms: + const ts_ms_t y0 = start_of_year_ms(t); + const ts_ms_t y1 = end_of_year_ms(t); + assert(y0 <= t && t <= y1); + assert(ms_part(y0) == 0); + assert(ms_part(y1) == (MS_PER_SEC - 1)); + } + } + const ts_t sample_ts = to_timestamp(2024, 6, 30, 12, 0, 0); + DateStruct sample_date{2024, 6, 30}; + assert(to_date_time(sample_ts).year == 2024); + assert(to_date_time_ms(sec_to_ms(sample_ts)).mon == 6); + std::tm tm_info{}; + tm_info.tm_year = 124; + tm_info.tm_mon = 5; + tm_info.tm_mday = 30; + tm_info.tm_hour = 12; + tm_info.tm_min = 34; + tm_info.tm_sec = 56; + tm_info.tm_isdst = -1; + assert(tm_to_timestamp(&tm_info) == to_timestamp(2024, 6, 30, 12, 34, 56)); + assert(tm_to_ts(&tm_info) == tm_to_timestamp(&tm_info)); + assert(tm_to_timestamp_ms(&tm_info) == sec_to_ms(to_timestamp(2024, 6, 30, 12, 34, 56))); + assert(tm_to_ts_ms(&tm_info) == tm_to_timestamp_ms(&tm_info)); + assert(tm_to_ftimestamp(&tm_info) == static_cast(to_timestamp(2024, 6, 30, 12, 34, 56))); + assert(tm_to_fts(&tm_info) == tm_to_ftimestamp(&tm_info)); + assert(year_of<>(sample_ts) == 2024); + assert(year_of_ms<>(sec_to_ms(sample_ts)) == 2024); + assert(num_days_in_year<>(2024) == DAYS_PER_LEAP_YEAR); + assert(num_days_in_year<>(2023) == DAYS_PER_YEAR); + assert(num_days_in_year_ts(sample_ts) == DAYS_PER_LEAP_YEAR); + assert(start_of_year(sample_ts) == to_timestamp(2024, 1, 1)); + assert(start_of_year_ms(sec_to_ms(sample_ts)) == sec_to_ms(to_timestamp(2024, 1, 1))); + assert(start_of_year_date(2024) == to_timestamp(2024, 1, 1)); + assert(start_of_year_date_ms(2024) == sec_to_ms(to_timestamp(2024, 1, 1))); + assert(end_of_year(sample_ts) == to_timestamp(2024, 12, 31, 23, 59, 59)); + assert(end_of_year_ms(sec_to_ms(sample_ts)) == sec_to_ms(to_timestamp(2024, 12, 31, 23, 59, 59)) + (MS_PER_SEC - 1)); + { + const ts_ms_t t = -1; + const ts_ms_t y0 = start_of_year_ms(t); + const ts_ms_t y1 = end_of_year_ms(t); + + // Calendar invariants for ms boundaries: + assert(y0 <= t && t <= y1); + assert(ms_part(y0) == 0); + assert(ms_part(y1) == (MS_PER_SEC - 1)); + } + const ts_t before_epoch = to_timestamp(1969, 12, 31, 23, 59, 59); + assert(start_of_year(before_epoch) == to_timestamp(1969, 1, 1)); + assert(end_of_year(before_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59)); + const ts_t epoch_start = to_timestamp(1970, 1, 1, 0, 0, 0); + assert(start_of_year(epoch_start) == to_timestamp(1970, 1, 1)); + assert(end_of_year(epoch_start) == to_timestamp(1970, 12, 31, 23, 59, 59)); + const ts_t leap_day_2000 = to_timestamp(2000, 2, 29); + assert(start_of_year(leap_day_2000) == to_timestamp(2000, 1, 1)); + assert(end_of_year(leap_day_2000) == to_timestamp(2000, 12, 31, 23, 59, 59)); + const ts_t march_1900 = to_timestamp(1900, 3, 1); + assert(start_of_year(march_1900) == to_timestamp(1900, 1, 1)); + assert(end_of_year(march_1900) == to_timestamp(1900, 12, 31, 23, 59, 59)); + const ts_t march_2100 = to_timestamp(2100, 3, 1); + assert(start_of_year(march_2100) == to_timestamp(2100, 1, 1)); + assert(end_of_year(march_2100) == to_timestamp(2100, 12, 31, 23, 59, 59)); + const ts_t min_ts = MIN_TIMESTAMP; + const ts_t max_ts = MAX_TIMESTAMP; + const ts_t min_year_start = start_of_year(min_ts); + const ts_t min_year_end = end_of_year(min_ts); + assert(min_year_start <= min_ts); + assert(min_year_end >= min_ts); + assert(start_of_year(min_year_end) == min_year_start); + assert(end_of_year(min_year_start) == min_year_end); + const ts_t max_year_start = start_of_year(max_ts); + const ts_t max_year_end = end_of_year(max_ts); + assert(max_year_start <= max_ts); + assert(max_year_end >= max_ts); + assert(year_of(max_year_start) == year_of(max_ts)); + assert(is_valid_date(MAX_YEAR, 1, 1)); + assert(is_valid_date(MIN_YEAR, 1, 1)); + assert(year_of(max_ts) <= MAX_YEAR); + assert(year_of(min_ts) >= MIN_YEAR); + + assert(day_of_week_date<>(2024, 6, 30) == SUN); + assert(weekday_of_date<>(sample_date) == SUN); + assert(weekday_from_date<>(sample_date) == SUN); + assert(get_weekday_from_date<>(sample_date) == SUN); + assert(wd<>(sample_date) == SUN); + + // date_time_conversions and aliases + DateTimeStruct dt{2024, 6, 30, 12, 34, 56, 0}; + struct CustomTz { int hour; int min; bool is_positive; }; + const ts_t dt_ts = to_timestamp(dt); + assert(dt_ts == to_timestamp(2024, 6, 30, 12, 34, 56)); + assert(dt_ts == dt_to_timestamp(dt)); + assert(dt_to_ts(dt) == dt_to_timestamp(dt)); + assert(to_timestamp_ms(2024, 6, 30, 12, 34, 56, 5) == sec_to_ms(dt_ts) + 5); + assert(dt_to_timestamp_ms(dt) == sec_to_ms(dt_ts)); + assert(dt_to_ts_ms(dt) == dt_to_timestamp_ms(dt)); + assert(to_ts_ms(2024, 6, 30, 12, 34, 56, 5) == to_timestamp_ms(2024, 6, 30, 12, 34, 56, 5)); + + assert(to_ftimestamp(2024, 6, 30, 12, 34, 56) == static_cast(dt_ts)); + assert(dt_to_ftimestamp(dt) == static_cast(dt_ts)); + assert(to_fts(2024, 6, 30, 12, 34, 56, 5) == to_ftimestamp(2024, 6, 30, 12, 34, 56, 5)); + assert(dt_to_fts(dt) == dt_to_ftimestamp(dt)); + + assert(hour24_to_12(0) == 12); + assert(h24_to_h12(13) == 12); + + const auto alias_dt = to_dt(dt_ts); + assert(alias_dt.year == dt.year && alias_dt.mon == dt.mon && alias_dt.sec == dt.sec); + const auto alias_dt_ms = to_dt_ms(sec_to_ms(dt_ts)); + assert(alias_dt_ms.year == dt.year && alias_dt_ms.mon == dt.mon && alias_dt_ms.sec == dt.sec); + + const ts_t day_start = start_of_day(dt_ts); + assert(day_start == to_timestamp(2024, 6, 30)); + assert(start_of_prev_day(day_start) == to_timestamp(2024, 6, 29)); + assert(start_of_day_sec(sec_to_ms(day_start)) == day_start); + assert(start_of_day_ms(sec_to_ms(day_start)) == sec_to_ms(day_start)); + assert(start_of_next_day(day_start) == to_timestamp(2024, 7, 1)); + assert(start_of_next_day_ms(sec_to_ms(day_start)) == sec_to_ms(to_timestamp(2024, 7, 1))); + assert(next_day(day_start, 2) == to_timestamp(2024, 7, 2)); + assert(next_day_ms(sec_to_ms(day_start), 2) == sec_to_ms(to_timestamp(2024, 7, 2))); + assert(end_of_day(day_start) == to_timestamp(2024, 6, 30, 23, 59, 59)); + assert(end_of_day_sec(sec_to_ms(day_start)) == to_timestamp(2024, 6, 30, 23, 59, 59)); + assert(end_of_day_ms(sec_to_ms(day_start)) == sec_to_ms(to_timestamp(2024, 6, 30, 23, 59, 59)) + 999); + { + const ts_ms_t t = sec_to_ms(day_start); + const ts_ms_t d0 = start_of_day_ms(t); + const ts_ms_t d1 = end_of_day_ms(t); + assert(d0 == t); + assert(d1 == t + MS_PER_DAY - 1); + } + + assert(day_of_year(day_start) == 182); + assert(month_of_year(day_start) == 6); + assert(day_of_month(day_start) == 30); + assert(num_days_in_month(2024, 2) == 29); + assert(num_days_in_month(2023, 2) == 28); + assert(num_days_in_month_ts(sample_ts) == 30); + + assert(weekday_of_ts(day_start) == SUN); + assert(get_weekday_from_ts(day_start) == SUN); + assert(weekday_of_ts_ms(sec_to_ms(day_start)) == SUN); + assert(get_weekday_from_ts_ms(sec_to_ms(day_start)) == SUN); + assert(wd_ts(day_start) == SUN); + assert(wd_ms(sec_to_ms(day_start)) == SUN); + + assert(start_of_month(day_start) == to_timestamp(2024, 6, 1)); + assert(end_of_month(day_start) == to_timestamp(2024, 6, 30, 23, 59, 59)); + assert(last_sunday_of_month(day_start) == to_timestamp(2024, 6, 30, 23, 59, 59)); + assert(last_sunday_month_day<>(2024, 6) == 30); + + assert(start_of_week(day_start) == to_timestamp(2024, 6, 30)); + assert(end_of_week(day_start) == to_timestamp(2024, 7, 6, 23, 59, 59)); + assert(start_of_saturday(day_start) == to_timestamp(2024, 7, 6)); + + assert(start_of_hour(dt_ts) == to_timestamp(2024, 6, 30, 12, 0, 0)); + assert(start_of_hour_sec(sec_to_ms(dt_ts)) == to_timestamp(2024, 6, 30, 12, 0, 0)); + assert(start_of_hour_ms(sec_to_ms(dt_ts)) == sec_to_ms(to_timestamp(2024, 6, 30, 12, 0, 0))); + assert(end_of_hour(dt_ts) == to_timestamp(2024, 6, 30, 12, 59, 59)); + assert(end_of_hour_sec(sec_to_ms(dt_ts)) == to_timestamp(2024, 6, 30, 12, 59, 59)); + assert(end_of_hour_ms(sec_to_ms(dt_ts)) == sec_to_ms(to_timestamp(2024, 6, 30, 12, 59, 59)) + 999); + assert(start_of_min(dt_ts) == to_timestamp(2024, 6, 30, 12, 34, 0)); + assert(end_of_min(dt_ts) == to_timestamp(2024, 6, 30, 12, 34, 59)); + assert(min_of_day(dt_ts) == 754); + assert(hour_of_day(dt_ts) == 12); + assert(min_of_hour(dt_ts) == 34); + assert(start_of_period(300, dt_ts) == to_timestamp(2024, 6, 30, 12, 30, 0)); + assert(end_of_period(300, dt_ts) == to_timestamp(2024, 6, 30, 12, 34, 59)); + + const TimeZoneStruct tz_struct{3, 30, true}; + assert(time_zone_struct_to_offset(tz_struct) == SEC_PER_HOUR * 3 + SEC_PER_MIN * 30); + assert(tz_to_offset(tz_struct) == time_zone_struct_to_offset(tz_struct)); + assert(to_offset(tz_struct) == time_zone_struct_to_offset(tz_struct)); + assert(to_tz_offset(tz_struct) == time_zone_struct_to_offset(tz_struct)); + assert(tz_offset(tz_struct) == time_zone_struct_to_offset(tz_struct)); + assert(tz_offset_hm(3, 30) == time_zone_struct_to_offset(tz_struct)); + assert(tz_offset_hm(-5, -30) == -(SEC_PER_HOUR * 5 + SEC_PER_MIN * 30)); + assert(is_valid_tz_offset(tz_offset_hm(3, 30))); + assert(valid_tz_offset(tz_offset_hm(3, 30))); + const CustomTz custom_tz = to_time_zone(time_zone_struct_to_offset(tz_struct)); + assert(custom_tz.hour == 3 && custom_tz.min == 30 && custom_tz.is_positive); + + // workday_conversions + assert(first_workday_day(2024, 6) == 3); + assert(last_workday_day(2024, 6) == 28); + assert(count_workdays_in_month(2024, 6) == 20); + assert(workday_index_in_month(2024, 6, 3) == 1); + assert(is_first_workday_of_month(2024, 6, 3)); + assert(!is_first_workday_of_month(2024, 6, 4)); + assert(is_within_first_workdays_of_month(2024, 6, 5, 5)); + assert(is_last_workday_of_month(2024, 6, 28)); + assert(is_within_last_workdays_of_month(2024, 6, 24, 5)); + + const ts_t workday_ts = to_timestamp(2024, 6, 3); + const ts_ms_t workday_ts_ms = sec_to_ms(workday_ts); + assert(is_workday(workday_ts)); + assert(is_workday_ms(workday_ts_ms)); + assert(is_first_workday_of_month(workday_ts)); + assert(is_first_workday_of_month_ms(workday_ts_ms)); + assert(is_within_first_workdays_of_month(workday_ts, 3)); + assert(is_within_first_workdays_of_month_ms(workday_ts_ms, 3)); + assert(!is_last_workday_of_month(workday_ts)); + assert(!is_last_workday_of_month_ms(workday_ts_ms)); + assert(is_last_workday_of_month(to_timestamp(2024, 6, 28))); + assert(is_last_workday_of_month_ms(sec_to_ms(to_timestamp(2024, 6, 28)))); + assert(is_within_last_workdays_of_month(to_timestamp(2024, 6, 24), 5)); + assert(is_within_last_workdays_of_month_ms(sec_to_ms(to_timestamp(2024, 6, 24)), 5)); + + assert(start_of_first_workday_month(2024, 6) == workday_ts); + assert(start_of_first_workday_month_ms(2024, 6) == workday_ts_ms); + assert(start_of_first_workday_month(workday_ts) == workday_ts); + assert(start_of_first_workday_month_ms(workday_ts_ms) == workday_ts_ms); + assert(end_of_first_workday_month(2024, 6) == end_of_day(workday_ts)); + assert(end_of_first_workday_month_ms(2024, 6) == end_of_day_ms(workday_ts_ms)); + assert(end_of_first_workday_month(workday_ts) == end_of_day(workday_ts)); + assert(end_of_first_workday_month_ms(workday_ts_ms) == end_of_day_ms(workday_ts_ms)); + + const ts_t last_workday_ts = to_timestamp(2024, 6, 28); + const ts_ms_t last_workday_ts_ms = sec_to_ms(last_workday_ts); + assert(start_of_last_workday_month(2024, 6) == last_workday_ts); + assert(start_of_last_workday_month_ms(2024, 6) == last_workday_ts_ms); + assert(start_of_last_workday_month(last_workday_ts) == last_workday_ts); + assert(start_of_last_workday_month_ms(last_workday_ts_ms) == last_workday_ts_ms); + assert(end_of_last_workday_month(2024, 6) == end_of_day(last_workday_ts)); + assert(end_of_last_workday_month_ms(2024, 6) == end_of_day_ms(last_workday_ts_ms)); + assert(end_of_last_workday_month(last_workday_ts) == end_of_day(last_workday_ts)); + assert(end_of_last_workday_month_ms(last_workday_ts_ms) == end_of_day_ms(last_workday_ts_ms)); + + (void)unix_day_two_ts; + (void)unix_day_two_ms; + (void)minute_mark; + (void)sample_ts; + (void)day_start; + (void)workday_ts; + (void)workday_ts_ms; + (void)last_workday_ts; + (void)last_workday_ts_ms; + + return 0; +} diff --git a/tests/time_conversions_test.cpp b/tests/time_conversions_test.cpp index b79924a6..a0e585a4 100644 --- a/tests/time_conversions_test.cpp +++ b/tests/time_conversions_test.cpp @@ -14,5 +14,19 @@ int main() { assert(start_of_min(61) == 60); assert(end_of_min(60) == 119); + const ts_t first_workday_start = start_of_first_workday_month(2024, 6); + assert(first_workday_start == to_timestamp(2024, 6, 3)); + assert(end_of_first_workday_month(2024, 6) == end_of_day(first_workday_start)); + + const ts_t last_workday_start = start_of_last_workday_month(2024, 3); + assert(last_workday_start == to_timestamp(2024, 3, 29)); + assert(end_of_last_workday_month_ms(2024, 3) == end_of_day_ms(sec_to_ms(last_workday_start))); + + assert(start_of_first_workday_month(2024, 13) == ERROR_TIMESTAMP); + assert(end_of_last_workday_month_ms(2024, 0) == ERROR_TIMESTAMP); + + (void)first_workday_start; + (void)last_workday_start; + return 0; } diff --git a/tests/time_parser_test.cpp b/tests/time_parser_test.cpp index f8541cbf..31e4a372 100644 --- a/tests/time_parser_test.cpp +++ b/tests/time_parser_test.cpp @@ -18,5 +18,8 @@ int main() { assert(ts("1970-01-01T00:00:00Z") == 0); assert(ts_ms("1970-01-01T00:00:01.500Z") == 1500); + (void)m; + (void)sec; + return 0; } diff --git a/tests/time_utils_test.cpp b/tests/time_utils_test.cpp index 6286833b..25fd9f4b 100644 --- a/tests/time_utils_test.cpp +++ b/tests/time_utils_test.cpp @@ -1,5 +1,10 @@ +#include #include + #include +#include +#include +#include /// \brief Basic checks for time utility helpers. int main() { @@ -22,5 +27,75 @@ int main() { ts_us_t u2 = timestamp_us(); assert(u2 >= u1 && u2 - u1 < US_PER_SEC); + const int64_t rt1 = now_realtime_us(); + const int64_t rt2 = now_realtime_us(); + assert(rt2 >= rt1); + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + const int64_t rt3 = now_realtime_us(); + assert(rt3 >= rt2); + + CpuTickTimer timer{}; + double first_sample = timer.record_sample(); + assert(timer.sample_count() == 1); + assert(timer.total_ticks() >= 0.0); + assert(timer.last_sample_ticks() == first_sample); + assert(!std::isnan(timer.average_ticks())); + + timer.stop(); + double frozen_elapsed = timer.elapsed(); + timer.stop(); + assert(timer.elapsed() == frozen_elapsed); + + double resumed_sample = timer.record_sample(); + assert(resumed_sample == 0.0); + assert(timer.last_sample_ticks() == 0.0); + assert(timer.sample_count() == 1); + + double second_sample = timer.record_sample(); + assert(second_sample >= 0.0); + assert(timer.sample_count() == 2); + + timer.reset_samples(); + assert(timer.sample_count() == 0); + assert(std::isnan(timer.average_ticks())); + + CpuTickTimer manual_timer{false}; + assert(manual_timer.elapsed() == 0.0); + assert(std::isnan(manual_timer.average_ticks())); + assert(manual_timer.sample_count() == 0); + + double no_sample = manual_timer.record_sample(); + assert(no_sample == 0.0); + assert(manual_timer.sample_count() == 0); + double collected = manual_timer.record_sample(); + assert(collected >= 0.0); + assert(manual_timer.sample_count() == 1); + + manual_timer.stop(); + double manual_frozen = manual_timer.elapsed(); + assert(manual_timer.elapsed() == manual_frozen); + + manual_timer.restart(); + assert(manual_timer.sample_count() == 0); + + (void)ns; + (void)us; + (void)ms; + (void)t1; + (void)t2; + (void)u1; + (void)u2; + (void)rt1; + (void)rt2; + (void)rt3; + (void)first_sample; + (void)frozen_elapsed; + (void)resumed_sample; + (void)second_sample; + (void)no_sample; + (void)collected; + (void)manual_frozen; + return 0; } diff --git a/tests/time_zone_conversion_test.cpp b/tests/time_zone_conversion_test.cpp index 6a285d91..213bdd30 100644 --- a/tests/time_zone_conversion_test.cpp +++ b/tests/time_zone_conversion_test.cpp @@ -11,6 +11,17 @@ int main() { using namespace time_shield; + const int SWITCH_HOUR = 2; + + auto first_sunday_month_day = [](int year, int month) { + return static_cast( + 1 + (DAYS_PER_WEEK - day_of_week_date(year, month, 1)) % DAYS_PER_WEEK); + }; + + auto second_sunday_month_day = [&](int year, int month) { + return first_sunday_month_day(year, month) + 7; + }; + ts_t cet_winter = to_timestamp(2023, 1, 1, 12, 0, 0); ts_t gmt_winter = cet_to_gmt(cet_winter); assert(gmt_winter == to_timestamp(2023, 1, 1, 11, 0, 0)); @@ -35,6 +46,52 @@ int main() { ts_t gmt_eet_summer = eet_to_gmt(eet_summer); assert(gmt_eet_summer == to_timestamp(2023, 7, 1, 9, 0, 0)); + int spring_day_2024 = static_cast(second_sunday_month_day(2024, MAR)); + ts_t et_before_spring = to_timestamp(2024, int(MAR), spring_day_2024, 1, 59, 59); + ts_t gmt_before_spring = et_to_gmt(et_before_spring); + assert(gmt_before_spring == to_timestamp(2024, int(MAR), spring_day_2024, 6, 59, 59)); + + ts_t gmt_spring_switch = to_timestamp(2024, int(MAR), spring_day_2024, 7, 0, 0); + ts_t et_after_spring = gmt_to_et(gmt_spring_switch); + assert(et_after_spring == to_timestamp(2024, int(MAR), spring_day_2024, 3, 0, 0)); + + int fall_day_2024 = static_cast(first_sunday_month_day(2024, NOV)); + ts_t gmt_fall_before = to_timestamp(2024, int(NOV), fall_day_2024, 6, 59, 59); + ts_t et_fall_before = gmt_to_et(gmt_fall_before); + assert(gmt_fall_before - et_fall_before == SEC_PER_HOUR * 4); + + ts_t gmt_fall_after = to_timestamp(2024, int(NOV), fall_day_2024, 7, 0, 0); + ts_t et_fall_after = gmt_to_et(gmt_fall_after); + assert(gmt_fall_after - et_fall_after == SEC_PER_HOUR * 5); + + int spring_day_2006 = static_cast(first_sunday_month_day(2006, APR)); + ts_t et_before_2006 = to_timestamp(2006, int(APR), spring_day_2006, 1, 59, 59); + ts_t gmt_before_2006 = et_to_gmt(et_before_2006); + assert(gmt_before_2006 == to_timestamp(2006, int(APR), spring_day_2006, 6, 59, 59)); + + ts_t et_after_2006 = to_timestamp(2006, int(APR), spring_day_2006, SWITCH_HOUR, 0, 0); + ts_t gmt_after_2006 = et_to_gmt(et_after_2006); + assert(gmt_after_2006 == to_timestamp(2006, int(APR), spring_day_2006, 6, 0, 0)); + + int fall_day_2006 = last_sunday_month_day(2006, OCT); + ts_t et_fall_2006_before = to_timestamp(2006, int(OCT), fall_day_2006, 1, 59, 59); + ts_t gmt_fall_2006_before = et_to_gmt(et_fall_2006_before); + assert(gmt_fall_2006_before == to_timestamp(2006, int(OCT), fall_day_2006, 5, 59, 59)); + + ts_t et_fall_2006_after = to_timestamp(2006, int(OCT), fall_day_2006, SWITCH_HOUR, 0, 0); + ts_t gmt_fall_2006_after = et_to_gmt(et_fall_2006_after); + assert(gmt_fall_2006_after == to_timestamp(2006, int(OCT), fall_day_2006, 7, 0, 0)); + + ts_t et_round_trip_winter = to_timestamp(2024, 1, 15, 12, 0, 0); + ts_t et_round_trip_summer = to_timestamp(2024, 7, 15, 12, 0, 0); + assert(et_round_trip_winter == gmt_to_et(et_to_gmt(et_round_trip_winter))); + assert(et_round_trip_summer == gmt_to_et(et_to_gmt(et_round_trip_summer))); + + ts_t gmt_round_trip_winter = to_timestamp(2024, 1, 15, 17, 0, 0); + ts_t gmt_round_trip_summer = to_timestamp(2024, 7, 15, 16, 0, 0); + assert(gmt_round_trip_winter == et_to_gmt(gmt_to_et(gmt_round_trip_winter))); + assert(gmt_round_trip_summer == et_to_gmt(gmt_to_et(gmt_round_trip_summer))); + for(int year : {2021, 2022, 2023, 2024}) { int day = last_sunday_month_day(year, OCT); @@ -53,7 +110,54 @@ int main() { ts_t eet_end_after = to_timestamp(year, int(OCT), day, 4, 30, 0); ts_t gmt_eet_end_after = eet_to_gmt(eet_end_after); assert(gmt_eet_end_after == to_timestamp(year, int(OCT), day, 2, 30, 0)); + + (void)day; + (void)cet_end_before; + (void)gmt_end_before; + (void)cet_end_after; + (void)gmt_end_after; + (void)eet_end_before; + (void)gmt_eet_end_before; + (void)eet_end_after; + (void)gmt_eet_end_after; } + (void)cet_winter; + (void)gmt_winter; + (void)cet_summer; + (void)gmt_summer; + (void)cet_before; + (void)gmt_before; + (void)cet_after; + (void)gmt_after; + (void)eet_winter; + (void)gmt_eet_winter; + (void)eet_summer; + (void)gmt_eet_summer; + (void)spring_day_2024; + (void)et_before_spring; + (void)gmt_before_spring; + (void)gmt_spring_switch; + (void)et_after_spring; + (void)fall_day_2024; + (void)gmt_fall_before; + (void)et_fall_before; + (void)gmt_fall_after; + (void)et_fall_after; + (void)spring_day_2006; + (void)et_before_2006; + (void)gmt_before_2006; + (void)et_after_2006; + (void)gmt_after_2006; + (void)fall_day_2006; + (void)et_fall_2006_before; + (void)gmt_fall_2006_before; + (void)et_fall_2006_after; + (void)gmt_fall_2006_after; + (void)et_round_trip_winter; + (void)et_round_trip_summer; + (void)gmt_round_trip_winter; + (void)gmt_round_trip_summer; + return 0; } diff --git a/tests/time_zone_conversions_us_test.cpp b/tests/time_zone_conversions_us_test.cpp new file mode 100644 index 00000000..158dcf40 --- /dev/null +++ b/tests/time_zone_conversions_us_test.cpp @@ -0,0 +1,94 @@ +#include +#include +#include + +/// \brief Tests US ET/CT conversions to UTC including DST transitions. +int main() { + using namespace time_shield; + + auto first_sunday_month_day = [](int year, int month) { + return static_cast( + 1 + (DAYS_PER_WEEK - day_of_week_date(year, month, 1)) % DAYS_PER_WEEK); + }; + + auto second_sunday_month_day = [&](int year, int month) { + return first_sunday_month_day(year, month) + 7; + }; + + ts_t ct_standard = to_timestamp(2024, 1, 15, 12, 0, 0); + ts_t gmt_ct_standard = ct_to_gmt(ct_standard); + assert(gmt_ct_standard == to_timestamp(2024, 1, 15, 18, 0, 0)); + + ts_t et_standard = to_timestamp(2024, 1, 15, 12, 0, 0); + ts_t gmt_et_standard = et_to_gmt(et_standard); + assert(gmt_et_standard == to_timestamp(2024, 1, 15, 17, 0, 0)); + + ts_t ct_summer = to_timestamp(2024, 7, 15, 12, 0, 0); + ts_t gmt_ct_summer = ct_to_gmt(ct_summer); + assert(gmt_ct_summer == to_timestamp(2024, 7, 15, 17, 0, 0)); + + ts_t et_summer = to_timestamp(2024, 7, 15, 12, 0, 0); + ts_t gmt_et_summer = et_to_gmt(et_summer); + assert(gmt_et_summer == to_timestamp(2024, 7, 15, 16, 0, 0)); + + ts_t ct_wrap_check = to_timestamp(2024, 6, 10, 9, 0, 0); + assert(ct_to_gmt(ct_wrap_check) == et_to_gmt(ct_wrap_check + SEC_PER_HOUR)); + + ts_t gmt_wrap_check = to_timestamp(2024, 6, 10, 15, 0, 0); + assert(gmt_to_ct(gmt_wrap_check) == gmt_to_et(gmt_wrap_check) - SEC_PER_HOUR); + + int spring_day_2024 = static_cast(second_sunday_month_day(2024, MAR)); + ts_t et_before_spring = to_timestamp(2024, int(MAR), spring_day_2024, 1, 59, 59); + ts_t gmt_et_before_spring = et_to_gmt(et_before_spring); + assert(gmt_et_before_spring == to_timestamp(2024, int(MAR), spring_day_2024, 6, 59, 59)); + + ts_t et_after_spring = to_timestamp(2024, int(MAR), spring_day_2024, 3, 0, 0); + ts_t gmt_et_after_spring = et_to_gmt(et_after_spring); + assert(gmt_et_after_spring == to_timestamp(2024, int(MAR), spring_day_2024, 7, 0, 0)); + + ts_t ct_before_spring = to_timestamp(2024, int(MAR), spring_day_2024, 1, 59, 59); + ts_t gmt_before_spring = ct_to_gmt(ct_before_spring); + assert(gmt_before_spring == to_timestamp(2024, int(MAR), spring_day_2024, 6, 59, 59)); + + ts_t ct_after_spring = to_timestamp(2024, int(MAR), spring_day_2024, 3, 0, 0); + ts_t gmt_after_spring = ct_to_gmt(ct_after_spring); + assert(gmt_after_spring == to_timestamp(2024, int(MAR), spring_day_2024, 8, 0, 0)); + + int fall_day_2024 = static_cast(first_sunday_month_day(2024, NOV)); + ts_t et_repeat_hour = to_timestamp(2024, int(NOV), fall_day_2024, 1, 30, 0); + ts_t gmt_et_repeat_hour = et_to_gmt(et_repeat_hour); + assert(gmt_et_repeat_hour == to_timestamp(2024, int(NOV), fall_day_2024, 5, 30, 0)); + + ts_t ct_repeat_hour = to_timestamp(2024, int(NOV), fall_day_2024, 1, 30, 0); + ts_t gmt_repeat_hour = ct_to_gmt(ct_repeat_hour); + assert(gmt_repeat_hour == to_timestamp(2024, int(NOV), fall_day_2024, 7, 30, 0)); + + ts_t ct_after_fall = to_timestamp(2024, int(NOV), fall_day_2024, 2, 0, 0); + ts_t gmt_after_fall = ct_to_gmt(ct_after_fall); + assert(gmt_after_fall == to_timestamp(2024, int(NOV), fall_day_2024, 8, 0, 0)); + + (void)gmt_ct_standard; + (void)gmt_et_standard; + (void)gmt_ct_summer; + (void)gmt_et_summer; + (void)ct_wrap_check; + (void)gmt_wrap_check; + (void)spring_day_2024; + (void)et_before_spring; + (void)gmt_et_before_spring; + (void)et_after_spring; + (void)gmt_et_after_spring; + (void)ct_before_spring; + (void)gmt_before_spring; + (void)ct_after_spring; + (void)gmt_after_spring; + (void)fall_day_2024; + (void)et_repeat_hour; + (void)gmt_et_repeat_hour; + (void)ct_repeat_hour; + (void)gmt_repeat_hour; + (void)ct_after_fall; + (void)gmt_after_fall; + + return 0; +} diff --git a/tests/timer_scheduler_test.cpp b/tests/timer_scheduler_test.cpp new file mode 100644 index 00000000..2e0a2476 --- /dev/null +++ b/tests/timer_scheduler_test.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include +#include +#include + +/// \brief Basic behavioral checks for TimerScheduler and Timer classes. +int main() { + using namespace time_shield; + + TimerScheduler scheduler; + + // Single shot timer processed via process(). + Timer single_timer(scheduler); + std::atomic single_counter{0}; + single_timer.set_single_shot(true); + single_timer.set_callback([&single_counter]() { single_counter.fetch_add(1); }); + const auto start_time = std::chrono::steady_clock::now(); + single_timer.start(std::chrono::seconds(1)); + while (single_counter.load() == 0) { + scheduler.process(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + const auto elapsed = std::chrono::steady_clock::now() - start_time; + assert(elapsed >= std::chrono::milliseconds(900)); + assert(elapsed < std::chrono::seconds(2)); + assert(single_counter.load() == 1); + assert(!single_timer.is_active()); + + // Repeating timer stopped from callback. + Timer repeating_timer(scheduler); + std::atomic repeating_counter{0}; + repeating_timer.set_callback([&repeating_counter, &repeating_timer]() { + const int value = repeating_counter.fetch_add(1) + 1; + if (value >= 3) { + repeating_timer.stop(); + } + }); + repeating_timer.start(std::chrono::milliseconds(0)); + for (int i = 0; i < 5 && repeating_counter.load() < 3; ++i) { + scheduler.process(); + } + assert(repeating_counter.load() >= 3); + + // Worker thread driven timer with stop_and_wait(). + Timer worker_timer(scheduler); + std::atomic worker_counter{0}; + worker_timer.set_single_shot(true); + worker_timer.set_callback([&worker_counter]() { + worker_counter.fetch_add(1); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + }); + scheduler.run(); + worker_timer.start(std::chrono::milliseconds(10)); + for (int i = 0; i < 50 && !worker_timer.is_running(); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + worker_timer.stop_and_wait(); + scheduler.stop(); + assert(worker_counter.load() == 1); + + // Static single shot helper. + const auto baseline_states = scheduler.active_timer_count_for_testing(); + std::atomic helper_counter{0}; + Timer::single_shot(scheduler, std::chrono::milliseconds(50), [&helper_counter]() { + helper_counter.fetch_add(1); + }); + assert(scheduler.active_timer_count_for_testing() == baseline_states + 1); + while (helper_counter.load() == 0) { + scheduler.process(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + assert(helper_counter.load() == 1); + for (int i = 0; i < 3; ++i) { + scheduler.process(); + } + assert(scheduler.active_timer_count_for_testing() == baseline_states); + + // Static single shot cancelled before firing should release its state. + const auto cancellation_baseline = scheduler.active_timer_count_for_testing(); + std::atomic cancelled_counter{0}; + Timer::single_shot(scheduler, std::chrono::seconds(1), [&cancelled_counter]() { + cancelled_counter.fetch_add(1); + }); + assert(scheduler.active_timer_count_for_testing() == cancellation_baseline + 1); + scheduler.stop(); + assert(cancelled_counter.load() == 0); + assert(scheduler.active_timer_count_for_testing() == cancellation_baseline); + + (void)baseline_states; + (void)cancellation_baseline; + + return 0; +} diff --git a/tests/win_time_utils_test.cpp b/tests/win_time_utils_test.cpp index 3e779bb8..19275675 100644 --- a/tests/win_time_utils_test.cpp +++ b/tests/win_time_utils_test.cpp @@ -8,6 +8,8 @@ int main() { const int64_t start = now_realtime_us(); const int64_t end = now_realtime_us(); assert(end >= start); + (void)start; + (void)end; #endif return 0; } diff --git a/tests/workday_boundaries_test.cpp b/tests/workday_boundaries_test.cpp new file mode 100644 index 00000000..de58b693 --- /dev/null +++ b/tests/workday_boundaries_test.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include + +/// \brief Validates first/last workday helpers across overload sets. +int main() { + using namespace time_shield; + + const ts_t june_first = to_timestamp(2024, 6, 1); + const ts_t june_third = to_timestamp(2024, 6, 3); + const ts_t june_fourth = to_timestamp(2024, 6, 4); + const ts_t june_twenty_sixth = to_timestamp(2024, 6, 26); + const ts_t june_twenty_seventh = to_timestamp(2024, 6, 27); + const ts_t june_twenty_eighth = to_timestamp(2024, 6, 28); + + assert(first_workday_day(2024, 6) == 3); + assert(last_workday_day(2024, 6) == 28); + assert(count_workdays_in_month(2024, 6) == 20); + assert(workday_index_in_month(2024, 6, 27) == 19); + + assert(is_first_workday_of_month(june_third)); + assert(!is_first_workday_of_month(june_first)); + assert(!is_first_workday_of_month(june_fourth)); + + assert(is_first_workday_of_month(2024, 6, 3)); + assert(!is_first_workday_of_month(2024, 6, 1)); + + const ts_ms_t june_third_ms = june_third * MS_PER_SEC + 250; + const ts_ms_t june_fourth_ms = june_fourth * MS_PER_SEC; + assert(is_first_workday_of_month_ms(june_third_ms)); + assert(!is_first_workday_of_month_ms(june_fourth_ms)); + + assert(is_last_workday_of_month(june_twenty_eighth)); + assert(!is_last_workday_of_month(june_twenty_seventh)); + + assert(is_last_workday_of_month(2024, 6, 28)); + assert(!is_last_workday_of_month(2024, 6, 29)); + + assert(is_last_workday_of_month_ms(june_twenty_eighth * MS_PER_SEC)); + assert(!is_last_workday_of_month_ms((june_twenty_eighth + SEC_PER_DAY) * MS_PER_SEC)); + + assert(is_within_first_workdays_of_month(june_third, 1)); + assert(!is_within_first_workdays_of_month(june_fourth, 1)); + assert(is_within_first_workdays_of_month(june_fourth, 2)); + assert(!is_within_first_workdays_of_month(june_third, 0)); + assert(!is_within_first_workdays_of_month(june_third, 25)); + + assert(is_within_first_workdays_of_month(2024, 6, 3, 1)); + assert(!is_within_first_workdays_of_month(2024, 6, 4, 1)); + assert(is_within_first_workdays_of_month(2024, 6, 4, 2)); + assert(!is_within_first_workdays_of_month(2024, 6, 3, 30)); + + assert(is_within_first_workdays_of_month_ms(june_third_ms, 1)); + assert(!is_within_first_workdays_of_month_ms(june_fourth_ms, 1)); + + assert(is_within_last_workdays_of_month(june_twenty_eighth, 1)); + assert(is_within_last_workdays_of_month(june_twenty_seventh, 2)); + assert(!is_within_last_workdays_of_month(june_twenty_sixth, 2)); + assert(is_within_last_workdays_of_month(june_twenty_sixth, 3)); + assert(!is_within_last_workdays_of_month(june_twenty_eighth, 0)); + assert(!is_within_last_workdays_of_month(june_twenty_eighth, 40)); + + assert(is_within_last_workdays_of_month(2024, 6, 28, 1)); + assert(is_within_last_workdays_of_month(2024, 6, 27, 2)); + assert(!is_within_last_workdays_of_month(2024, 6, 27, 1)); + assert(is_within_last_workdays_of_month(2024, 6, 26, 3)); + + assert(is_within_last_workdays_of_month_ms(june_twenty_eighth * MS_PER_SEC, 1)); + assert(!is_within_last_workdays_of_month_ms(june_twenty_sixth * MS_PER_SEC, 2)); + + const std::string sept_first = "2024-09-02T09:00:00Z"; // Monday after a Sunday start + const std::string sept_second = "2024-09-03T09:00:00Z"; + const std::string sept_first_ms = "2024-09-02T09:00:00.250Z"; + + assert(is_first_workday_of_month(sept_first)); + assert(!is_first_workday_of_month(sept_second)); + assert(is_first_workday_of_month_ms(sept_first_ms)); + assert(!is_first_workday_of_month_ms("2024-09-03T09:00:00.250Z")); + + assert(is_within_first_workdays_of_month(sept_second, 2)); + assert(!is_within_first_workdays_of_month(sept_second, 1)); + assert(is_within_first_workdays_of_month_ms("2024-09-03T09:00:00.000Z", 2)); + + assert(is_last_workday_of_month("2024-06-28T12:00:00Z")); + assert(is_last_workday_of_month_ms("2024-06-28T12:00:00.000Z")); + assert(!is_last_workday_of_month("2024-06-27T12:00:00Z")); + + assert(is_within_last_workdays_of_month("2024-06-26T10:00:00Z", 3)); + assert(!is_within_last_workdays_of_month("2024-06-26T10:00:00Z", 2)); + assert(is_within_last_workdays_of_month_ms("2024-06-27T10:00:00.500Z", 2)); + + assert(!is_first_workday_of_month("not-a-date")); + assert(!is_within_first_workdays_of_month_ms("invalid", 2)); + + (void)june_first; + (void)june_third; + (void)june_fourth; + (void)june_twenty_sixth; + (void)june_twenty_seventh; + (void)june_twenty_eighth; + (void)june_third_ms; + (void)june_fourth_ms; + (void)sept_first; + (void)sept_second; + (void)sept_first_ms; + + return 0; +} diff --git a/tests/workday_validation_test.cpp b/tests/workday_validation_test.cpp new file mode 100644 index 00000000..11939b34 --- /dev/null +++ b/tests/workday_validation_test.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +/// \brief Validates the is_workday overload set. +int main() { + using namespace time_shield; + + const ts_t weekday_ts = 1710720000; // 2024-03-18 (Monday) + const ts_t weekend_ts = 1710547200; // 2024-03-16 (Saturday) + + assert(is_workday(weekday_ts)); + assert(!is_workday(weekend_ts)); + + const ts_ms_t weekday_ms = weekday_ts * MS_PER_SEC; + const ts_ms_t weekend_ms = weekend_ts * MS_PER_SEC + 500; + + assert(is_workday_ms(weekday_ms)); + assert(!is_workday_ms(weekend_ms)); + + assert(is_workday(2024, 3, 18)); + assert(!is_workday(2024, 3, 16)); + + const std::string weekday_iso = "2024-03-18T00:00:00Z"; + const std::string weekend_iso = "2024-03-16T00:00:00Z"; + const std::string weekday_iso_ms = "2024-03-18T00:00:00.500Z"; + const std::string weekend_iso_ms = "2024-03-16T00:00:00.500Z"; + + assert(is_workday(weekday_iso)); + assert(!is_workday(weekend_iso)); + assert(is_workday_ms(weekday_iso_ms)); + assert(!is_workday_ms(weekend_iso_ms)); + + assert(!is_workday("not-a-date")); + assert(!is_workday_ms("2024-13-40T00:00:00.000Z")); + + (void)weekday_ts; + (void)weekend_ts; + (void)weekday_ms; + (void)weekend_ms; + (void)weekday_iso; + (void)weekend_iso; + (void)weekday_iso_ms; + (void)weekend_iso_ms; + + return 0; +} diff --git a/vcpkg-overlay/ports/time-shield-cpp/portfile.cmake b/vcpkg-overlay/ports/time-shield-cpp/portfile.cmake index 28b71cdf..c116268b 100644 --- a/vcpkg-overlay/ports/time-shield-cpp/portfile.cmake +++ b/vcpkg-overlay/ports/time-shield-cpp/portfile.cmake @@ -1,8 +1,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO NewYaroslav/time-shield-cpp - REF ff3852144b64d9aea9ba3c4b0116016940af1642 - SHA512 560d4229cfd7a3a2f293f9c94dde63b6b8746720ebf361fdedd0a12a55b519eb4c531a4ebcc42c915eb883c897943bfa4daa65588f6e230cbfaaa30834abcf46 + REF v1.0.4 + SHA512 10888145b60dcfc6b7029a633495fcf95f92dacb2936f4886d198ad8e6627cf31c0add5ae39dd078f4d96fa79610155551ecc5407136d94e9df1160ca6ba4b24 HEAD_REF stable )