diff --git a/include/time_shield/constants.hpp b/include/time_shield/constants.hpp index 449dc49..5142790 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 = 9223371891821347199LL; ///< 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_time_conversions.hpp b/include/time_shield/date_time_conversions.hpp index 8a14550..57b8aba 100644 --- a/include/time_shield/date_time_conversions.hpp +++ b/include/time_shield/date_time_conversions.hpp @@ -12,6 +12,7 @@ #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" @@ -21,6 +22,7 @@ #include #include +#include #include #include @@ -122,7 +124,7 @@ namespace time_shield { date_time.mon = TABLE_MONTH_OF_YEAR[days]; } - ts_t day_secs = static_cast(secs % SEC_PER_DAY); + 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); @@ -518,7 +520,17 @@ namespace time_shield { T2 min = 0, T2 sec = 0, T2 ms = 0) { - return sec_to_ms(to_timestamp(year, month, day, hour, min, sec)) + ms; + 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 @@ -532,9 +544,19 @@ namespace time_shield { /// \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( + TIME_SHIELD_CONSTEXPR inline ts_ms_t dt_to_timestamp_ms( const T& date_time) { - return sec_to_ms(dt_to_timestamp(date_time)) + date_time.ms; + 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 @@ -577,8 +599,12 @@ namespace time_shield { 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); + 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 @@ -595,8 +621,12 @@ namespace time_shield { 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); + 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. @@ -621,7 +651,7 @@ namespace time_shield { /// \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); + return ts - detail::floor_mod(ts, SEC_PER_DAY); } /// \brief Get timestamp of the start of the previous day. @@ -633,7 +663,7 @@ namespace time_shield { /// \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; + return ts - detail::floor_mod(ts, SEC_PER_DAY) - SEC_PER_DAY * days; } /// \brief Get the start of the day timestamp in seconds. @@ -655,7 +685,7 @@ namespace time_shield { /// \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); + 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. @@ -715,7 +745,7 @@ namespace time_shield { /// \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; + 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. @@ -735,7 +765,7 @@ namespace time_shield { /// \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; + 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. @@ -769,16 +799,14 @@ namespace time_shield { /// \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) { + if (ts >= 0 && ts < BIAS_2100) { constexpr ts_t SEC_PER_YEAR_X2 = SEC_PER_YEAR * 2; - ts_t year_start_ts = ts % SEC_PER_4_YEARS; + 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) { + } 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)) { + } 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; @@ -787,7 +815,7 @@ namespace time_shield { constexpr ts_t BIAS_2000 = 946684800; ts_t secs = ts - BIAS_2000; - ts_t offset_y400 = secs % SEC_PER_400_YEARS; + ts_t offset_y400 = detail::floor_mod(secs, SEC_PER_400_YEARS); ts_t start_ts = secs - offset_y400 + BIAS_2000; secs = offset_y400; @@ -804,36 +832,24 @@ namespace time_shield { secs -= SEC_PER_4_YEARS_V2; start_ts += SEC_PER_4_YEARS_V2; } else { - start_ts += secs - secs % SEC_PER_YEAR; + start_ts += secs - detail::floor_mod(secs, SEC_PER_YEAR); return start_ts; } } - ts_t offset_4y = secs % SEC_PER_4_YEARS; + 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 - secs % SEC_PER_YEAR; - } - else { - start_ts += secs - secs % SEC_PER_YEAR; + start_ts += secs - detail::floor_mod(secs, SEC_PER_YEAR); return start_ts; } - ts_t offset_y = secs % SEC_PER_YEAR; - ts_t year_start_ts = start_ts + secs - offset_y; - if (offset_y < SEC_PER_YEAR * 2) { - return year_start_ts; - } - else - if (offset_y < (SEC_PER_YEAR * 3)) { - return year_start_ts + SEC_PER_YEAR; - } - - return start_ts + SEC_PER_LEAP_YEAR; + 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. @@ -850,73 +866,20 @@ namespace time_shield { /// \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; + 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 timestamp of the current year in milliseconds. + /// 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))); + return sec_to_ms(end_of_year(ms_to_sec(ts_ms))) + (MS_PER_SEC - 1); } /// \brief Get the day of the year. @@ -1027,7 +990,8 @@ namespace time_shield { /// \return Weekday (SUN = 0, MON = 1, ... SAT = 6). template constexpr T weekday_of_ts(ts_t ts) noexcept { - return static_cast((ts / SEC_PER_DAY + THU) % DAYS_PER_WEEK); + 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. @@ -1139,7 +1103,7 @@ namespace time_shield { /// \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); + return ts - detail::floor_mod(ts, SEC_PER_HOUR); } /// \brief Get the timestamp at the start of the hour. @@ -1157,14 +1121,14 @@ namespace time_shield { /// \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); + 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 - (ts % SEC_PER_HOUR) + SEC_PER_HOUR - 1; + 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. @@ -1178,21 +1142,21 @@ namespace time_shield { /// \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 - (ts_ms % MS_PER_HOUR) + MS_PER_HOUR - 1; + 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 - (ts % SEC_PER_MIN); + 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 - (ts % SEC_PER_MIN) + SEC_PER_MIN - 1; + return ts - detail::floor_mod(ts, SEC_PER_MIN) + SEC_PER_MIN - 1; } /// \brief Get minute of day. @@ -1201,7 +1165,8 @@ namespace time_shield { /// \return Minute of day. template constexpr T min_of_day(ts_t ts = time_shield::ts()) noexcept { - return static_cast((ts / SEC_PER_MIN) % MIN_PER_DAY); + 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. @@ -1210,7 +1175,8 @@ namespace time_shield { /// \return Hour of day. template constexpr T hour_of_day(ts_t ts = time_shield::ts()) noexcept { - return static_cast((ts / SEC_PER_HOUR) % HOURS_PER_DAY); + 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. @@ -1219,7 +1185,8 @@ namespace time_shield { /// \return Minute of hour. template constexpr T min_of_hour(ts_t ts = time_shield::ts()) noexcept { - return static_cast((ts / SEC_PER_MIN) % MIN_PER_HOUR); + 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. @@ -1228,7 +1195,7 @@ namespace time_shield { /// \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); + return ts - detail::floor_mod(ts, static_cast(p)); } /// \brief Get the timestamp of the end of the period. @@ -1237,7 +1204,7 @@ namespace time_shield { /// \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; + return ts - detail::floor_mod(ts, static_cast(p)) + p - 1; } /// \} diff --git a/include/time_shield/detail/floor_math.hpp b/include/time_shield/detail/floor_math.hpp new file mode 100644 index 0000000..baf6f37 --- /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/time_unit_conversions.hpp b/include/time_shield/time_unit_conversions.hpp index 14aebf2..a6a52ce 100644 --- a/include/time_shield/time_unit_conversions.hpp +++ b/include/time_shield/time_unit_conversions.hpp @@ -8,6 +8,7 @@ #include "config.hpp" #include "constants.hpp" +#include "detail/floor_math.hpp" #include "types.hpp" #include @@ -24,8 +25,8 @@ namespace time_shield { /// \return T Nanosecond part of the second. template TIME_SHIELD_CONSTEXPR T ns_of_sec(fts_t ts) noexcept { - fts_t temp = 0; - return static_cast(std::round(std::modf(ts, &temp) * static_cast(NS_PER_SEC))); + 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. @@ -34,8 +35,8 @@ namespace time_shield { /// \return T Microsecond part of the second. template TIME_SHIELD_CONSTEXPR T us_of_sec(fts_t ts) noexcept { - fts_t temp = 0; - return static_cast(std::round(std::modf(ts, &temp) * static_cast(US_PER_SEC))); + 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. @@ -44,6 +45,36 @@ namespace time_shield { /// \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))); } @@ -53,8 +84,44 @@ namespace time_shield { /// \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 static_cast(ts % MS_PER_SEC); + 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 @@ -115,7 +182,9 @@ namespace time_shield { /// \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); + 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. @@ -181,7 +250,9 @@ namespace time_shield { /// \return T1 Timestamp in minutes. template constexpr T1 ms_to_min(T2 ts) noexcept { - return static_cast(ts) / static_cast(MS_PER_MIN); + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(MS_PER_MIN))); } //----------------------------------------------------------------------------// @@ -238,7 +309,9 @@ namespace time_shield { /// \return T1 Timestamp in minutes. template constexpr T1 sec_to_min(T2 ts) noexcept { - return static_cast(ts) / static_cast(SEC_PER_MIN); + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(SEC_PER_MIN))); } /// \brief Converts a timestamp from minutes to floating-point seconds. @@ -314,7 +387,9 @@ namespace time_shield { /// \return T1 Timestamp in hours. template constexpr T1 ms_to_hour(T2 ts) noexcept { - return static_cast(ts) / static_cast(MS_PER_HOUR); + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(MS_PER_HOUR))); } //----------------------------------------------------------------------------// @@ -372,7 +447,9 @@ namespace time_shield { /// \return T1 Timestamp in hours. template constexpr T1 sec_to_hour(T2 ts) noexcept { - return static_cast(ts) / static_cast(SEC_PER_HOUR); + return static_cast(detail::floor_div( + static_cast(ts), + static_cast(SEC_PER_HOUR))); } /// \brief Converts a timestamp from hours to floating-point seconds. diff --git a/tests/negative_time_boundaries_test.cpp b/tests/negative_time_boundaries_test.cpp new file mode 100644 index 0000000..2d55de1 --- /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/test_timestamp_ms_pre_epoch.cpp b/tests/test_timestamp_ms_pre_epoch.cpp new file mode 100644 index 0000000..557015d --- /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 0000000..b77dbb6 --- /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_conversions_coverage_test.cpp b/tests/time_conversions_coverage_test.cpp index bb02136..c39f4ed 100644 --- a/tests/time_conversions_coverage_test.cpp +++ b/tests/time_conversions_coverage_test.cpp @@ -12,6 +12,9 @@ int main() { 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); @@ -22,6 +25,14 @@ int main() { 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); @@ -119,7 +130,41 @@ int main() { 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))); + 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)); + assert(start_of_year_ms(-1) == sec_to_ms(start_of_year(-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);