Skip to content

Commit e68787d

Browse files
authored
fix(time): make ms conversions overflow-safe (bounds + year-end) and update tests
* fix(time): adjust timestamp ms bounds Update MAX_TIMESTAMP to avoid ms overflow and extend boundary tests for negative and ms timestamps. * fix(tests): reuse detail::floor_div/floor_mod and ensure C++11 compatibility * fix(constants): cap max timestamp year range Reduce the maximum timestamp to keep year-end conversions in range for millisecond calculations. * fix(conversions): align end_of_year_ms with ms bounds Return the last millisecond of the year to match end-of-year expectations and update coverage assertions. * fix(conversions): avoid windows min max macros Wrap numeric_limits min/max in parentheses to prevent Windows macro expansion in timestamp checks.
1 parent 4f567b6 commit e68787d

File tree

8 files changed

+423
-119
lines changed

8 files changed

+423
-119
lines changed

include/time_shield/constants.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ namespace time_shield {
154154
constexpr int64_t MAX_YEAR = 292277022000LL; ///< Maximum representable year
155155
constexpr int64_t MIN_YEAR = -2967369602200LL; ///< Minimum representable year
156156
constexpr int64_t ERROR_YEAR = 9223372036854770000LL; ///< Error year value
157-
constexpr int64_t MAX_TIMESTAMP = 9223371891821347199LL; ///< Maximum timestamp value
157+
constexpr int64_t MAX_TIMESTAMP = (((std::numeric_limits<int64_t>::max)() - (MS_PER_SEC - 1)) / MS_PER_SEC) - (SEC_PER_YEAR - 1); ///< Maximum timestamp value
158+
constexpr int64_t MIN_TIMESTAMP = -MAX_TIMESTAMP; ///< Minimum timestamp value
159+
constexpr int64_t MAX_TIMESTAMP_MS = MAX_TIMESTAMP * MS_PER_SEC + (MS_PER_SEC - 1); ///< Maximum timestamp value in milliseconds
160+
constexpr int64_t MIN_TIMESTAMP_MS = MIN_TIMESTAMP * MS_PER_SEC; ///< Minimum timestamp value in milliseconds
158161
constexpr int64_t ERROR_TIMESTAMP = 9223372036854770000LL; ///< Error timestamp value
159162
constexpr double MAX_OADATE = (std::numeric_limits<double>::max)(); ///< Maximum OLE automation date
160163
constexpr double AVG_DAYS_PER_YEAR = 365.25; ///< Average days per year

include/time_shield/date_time_conversions.hpp

Lines changed: 74 additions & 107 deletions
Large diffs are not rendered by default.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MIT
2+
#pragma once
3+
#ifndef _TIME_SHIELD_DETAIL_FLOOR_MATH_HPP_INCLUDED
4+
#define _TIME_SHIELD_DETAIL_FLOOR_MATH_HPP_INCLUDED
5+
6+
/// \file floor_math.hpp
7+
/// \brief Floor division and modulus helpers.
8+
9+
namespace time_shield {
10+
namespace detail {
11+
12+
/// \brief Floor division for positive divisor.
13+
template<class T>
14+
TIME_SHIELD_CONSTEXPR inline T floor_div(T a, T b) noexcept {
15+
T q = a / b;
16+
T r = a % b;
17+
if (r != 0 && a < 0) --q;
18+
return q;
19+
}
20+
21+
/// \brief Floor-mod for positive modulus (returns r in [0..b)).
22+
template<class T>
23+
TIME_SHIELD_CONSTEXPR inline T floor_mod(T a, T b) noexcept {
24+
T r = a % b;
25+
if (r < 0) r += b;
26+
return r;
27+
}
28+
29+
} // namespace detail
30+
} // namespace time_shield
31+
32+
#endif // _TIME_SHIELD_DETAIL_FLOOR_MATH_HPP_INCLUDED

include/time_shield/time_unit_conversions.hpp

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "config.hpp"
1010
#include "constants.hpp"
11+
#include "detail/floor_math.hpp"
1112
#include "types.hpp"
1213

1314
#include <cmath>
@@ -24,8 +25,8 @@ namespace time_shield {
2425
/// \return T Nanosecond part of the second.
2526
template<class T = int>
2627
TIME_SHIELD_CONSTEXPR T ns_of_sec(fts_t ts) noexcept {
27-
fts_t temp = 0;
28-
return static_cast<T>(std::round(std::modf(ts, &temp) * static_cast<fts_t>(NS_PER_SEC)));
28+
const int64_t ns = static_cast<int64_t>(std::floor(ts * static_cast<fts_t>(NS_PER_SEC)));
29+
return static_cast<T>(detail::floor_mod<int64_t>(ns, NS_PER_SEC));
2930
}
3031

3132
/// \brief Get the microsecond part of the second from a floating-point timestamp.
@@ -34,8 +35,8 @@ namespace time_shield {
3435
/// \return T Microsecond part of the second.
3536
template<class T = int>
3637
TIME_SHIELD_CONSTEXPR T us_of_sec(fts_t ts) noexcept {
37-
fts_t temp = 0;
38-
return static_cast<T>(std::round(std::modf(ts, &temp) * static_cast<fts_t>(US_PER_SEC)));
38+
const int64_t us = static_cast<int64_t>(std::floor(ts * static_cast<fts_t>(US_PER_SEC)));
39+
return static_cast<T>(detail::floor_mod<int64_t>(us, US_PER_SEC));
3940
}
4041

4142
/// \brief Get the millisecond part of the second from a floating-point timestamp.
@@ -44,6 +45,36 @@ namespace time_shield {
4445
/// \return T Millisecond part of the second.
4546
template<class T = int>
4647
TIME_SHIELD_CONSTEXPR T ms_of_sec(fts_t ts) noexcept {
48+
const int64_t ms = static_cast<int64_t>(std::floor(ts * static_cast<fts_t>(MS_PER_SEC)));
49+
return static_cast<T>(detail::floor_mod<int64_t>(ms, MS_PER_SEC));
50+
}
51+
52+
/// \brief Get the nanosecond part of the second from a floating-point timestamp (truncating).
53+
/// \tparam T Type of the returned value (default is int).
54+
/// \param ts Timestamp in floating-point seconds.
55+
/// \return T Nanosecond part of the second, truncating toward zero.
56+
template<class T = int>
57+
TIME_SHIELD_CONSTEXPR T ns_of_sec_signed(fts_t ts) noexcept {
58+
fts_t temp = 0;
59+
return static_cast<T>(std::round(std::modf(ts, &temp) * static_cast<fts_t>(NS_PER_SEC)));
60+
}
61+
62+
/// \brief Get the microsecond part of the second from a floating-point timestamp (truncating).
63+
/// \tparam T Type of the returned value (default is int).
64+
/// \param ts Timestamp in floating-point seconds.
65+
/// \return T Microsecond part of the second, truncating toward zero.
66+
template<class T = int>
67+
TIME_SHIELD_CONSTEXPR T us_of_sec_signed(fts_t ts) noexcept {
68+
fts_t temp = 0;
69+
return static_cast<T>(std::round(std::modf(ts, &temp) * static_cast<fts_t>(US_PER_SEC)));
70+
}
71+
72+
/// \brief Get the millisecond part of the second from a floating-point timestamp (truncating).
73+
/// \tparam T Type of the returned value (default is int).
74+
/// \param ts Timestamp in floating-point seconds.
75+
/// \return T Millisecond part of the second, truncating toward zero.
76+
template<class T = int>
77+
TIME_SHIELD_CONSTEXPR T ms_of_sec_signed(fts_t ts) noexcept {
4778
fts_t temp = 0;
4879
return static_cast<T>(std::round(std::modf(ts, &temp) * static_cast<fts_t>(MS_PER_SEC)));
4980
}
@@ -53,8 +84,44 @@ namespace time_shield {
5384
/// \param ts Timestamp in milliseconds.
5485
/// \return T Millisecond part of the timestamp.
5586
template<class T = int>
87+
constexpr T ms_part(ts_ms_t ts) noexcept {
88+
return static_cast<T>(detail::floor_mod<int64_t>(static_cast<int64_t>(ts), MS_PER_SEC));
89+
}
90+
91+
/// \brief Alias for ms_part.
92+
/// \tparam T Type of the returned value (default is int).
93+
/// \param ts Timestamp in milliseconds.
94+
/// \return T Millisecond part of the timestamp.
95+
template<class T = int>
5696
constexpr T ms_of_ts(ts_ms_t ts) noexcept {
57-
return static_cast<T>(ts % MS_PER_SEC);
97+
return ms_part<T>(ts);
98+
}
99+
100+
/// \brief Get the microsecond part of the timestamp.
101+
/// \tparam T Type of the returned value (default is int).
102+
/// \param ts Timestamp in microseconds.
103+
/// \return T Microsecond part of the timestamp.
104+
template<class T = int>
105+
constexpr T us_part(ts_us_t ts) noexcept {
106+
return static_cast<T>(detail::floor_mod<int64_t>(static_cast<int64_t>(ts), US_PER_SEC));
107+
}
108+
109+
/// \brief Alias for us_part.
110+
/// \tparam T Type of the returned value (default is int).
111+
/// \param ts Timestamp in microseconds.
112+
/// \return T Microsecond part of the timestamp.
113+
template<class T = int>
114+
constexpr T us_of_ts(ts_us_t ts) noexcept {
115+
return us_part<T>(ts);
116+
}
117+
118+
/// \brief Get the nanosecond part of the timestamp.
119+
/// \tparam T Type of the returned value (default is int).
120+
/// \param ts Timestamp in nanoseconds.
121+
/// \return T Nanosecond part of the timestamp.
122+
template<class T = int, class T2 = int64_t>
123+
constexpr T ns_part(T2 ts) noexcept {
124+
return static_cast<T>(detail::floor_mod<int64_t>(static_cast<int64_t>(ts), NS_PER_SEC));
58125
}
59126

60127
# ifndef TIME_SHIELD_CPP17
@@ -115,7 +182,9 @@ namespace time_shield {
115182
/// \return T1 Timestamp in seconds.
116183
template<class T1 = ts_t, class T2 = ts_ms_t>
117184
constexpr T1 ms_to_sec(T2 ts_ms) noexcept {
118-
return static_cast<T1>(ts_ms) / static_cast<T1>(MS_PER_SEC);
185+
return static_cast<T1>(detail::floor_div(
186+
static_cast<int64_t>(ts_ms),
187+
static_cast<int64_t>(MS_PER_SEC)));
119188
}
120189

121190
/// \brief Converts a timestamp from milliseconds to floating-point seconds.
@@ -181,7 +250,9 @@ namespace time_shield {
181250
/// \return T1 Timestamp in minutes.
182251
template<class T1 = int, class T2 = ts_ms_t>
183252
constexpr T1 ms_to_min(T2 ts) noexcept {
184-
return static_cast<T1>(ts) / static_cast<T1>(MS_PER_MIN);
253+
return static_cast<T1>(detail::floor_div(
254+
static_cast<int64_t>(ts),
255+
static_cast<int64_t>(MS_PER_MIN)));
185256
}
186257

187258
//----------------------------------------------------------------------------//
@@ -238,7 +309,9 @@ namespace time_shield {
238309
/// \return T1 Timestamp in minutes.
239310
template<class T1 = int, class T2 = ts_t>
240311
constexpr T1 sec_to_min(T2 ts) noexcept {
241-
return static_cast<T1>(ts) / static_cast<T1>(SEC_PER_MIN);
312+
return static_cast<T1>(detail::floor_div(
313+
static_cast<int64_t>(ts),
314+
static_cast<int64_t>(SEC_PER_MIN)));
242315
}
243316

244317
/// \brief Converts a timestamp from minutes to floating-point seconds.
@@ -314,7 +387,9 @@ namespace time_shield {
314387
/// \return T1 Timestamp in hours.
315388
template<class T1 = int, class T2 = ts_ms_t>
316389
constexpr T1 ms_to_hour(T2 ts) noexcept {
317-
return static_cast<T1>(ts) / static_cast<T1>(MS_PER_HOUR);
390+
return static_cast<T1>(detail::floor_div(
391+
static_cast<int64_t>(ts),
392+
static_cast<int64_t>(MS_PER_HOUR)));
318393
}
319394

320395
//----------------------------------------------------------------------------//
@@ -372,7 +447,9 @@ namespace time_shield {
372447
/// \return T1 Timestamp in hours.
373448
template<class T1 = int, class T2 = ts_t>
374449
constexpr T1 sec_to_hour(T2 ts) noexcept {
375-
return static_cast<T1>(ts) / static_cast<T1>(SEC_PER_HOUR);
450+
return static_cast<T1>(detail::floor_div(
451+
static_cast<int64_t>(ts),
452+
static_cast<int64_t>(SEC_PER_HOUR)));
376453
}
377454

378455
/// \brief Converts a timestamp from hours to floating-point seconds.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#include <time_shield/time_conversions.hpp>
2+
3+
#include <cassert>
4+
5+
int main() {
6+
using namespace time_shield;
7+
8+
const ts_t pre_epoch = to_timestamp(1969, 12, 31, 23, 59, 59);
9+
const ts_t pre_epoch_start = to_timestamp(1969, 12, 31, 0, 0, 0);
10+
const ts_t pre_epoch_prev_day = to_timestamp(1969, 12, 30, 0, 0, 0);
11+
12+
assert(pre_epoch == -1);
13+
assert(start_of_day(pre_epoch) == pre_epoch_start);
14+
assert(start_of_day(pre_epoch_start) == pre_epoch_start);
15+
assert(start_of_day(-86400) == pre_epoch_start);
16+
assert(start_of_prev_day(pre_epoch_start) == pre_epoch_prev_day);
17+
assert(end_of_day(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59));
18+
assert(end_of_day(pre_epoch_start) == to_timestamp(1969, 12, 31, 23, 59, 59));
19+
20+
assert(start_of_hour(pre_epoch) == to_timestamp(1969, 12, 31, 23, 0, 0));
21+
assert(end_of_hour(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59));
22+
assert(start_of_min(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 0));
23+
assert(end_of_min(pre_epoch) == to_timestamp(1969, 12, 31, 23, 59, 59));
24+
25+
assert(min_of_day(pre_epoch) == 1439);
26+
assert(hour_of_day(pre_epoch) == 23);
27+
assert(min_of_hour(pre_epoch) == 59);
28+
assert(weekday_of_ts(pre_epoch) == WED);
29+
30+
assert(start_of_period(300, pre_epoch) == -300);
31+
assert(end_of_period(300, pre_epoch) == -1);
32+
33+
const ts_ms_t pre_epoch_ms = to_timestamp_ms(1969, 12, 31, 23, 59, 59, 999);
34+
const ts_ms_t pre_epoch_start_ms = sec_to_ms(pre_epoch_start);
35+
36+
assert(pre_epoch_ms == -1);
37+
assert(start_of_day_ms(pre_epoch_ms) == pre_epoch_start_ms);
38+
assert(start_of_day_ms(pre_epoch_start_ms - 1) == sec_to_ms(pre_epoch_prev_day));
39+
assert(end_of_day_ms(pre_epoch_ms) == pre_epoch_start_ms + MS_PER_DAY - 1);
40+
assert(start_of_hour_ms(pre_epoch_ms) == sec_to_ms(start_of_hour(pre_epoch)));
41+
assert(start_of_hour_ms(-1000) == sec_to_ms(to_timestamp(1969, 12, 31, 23, 0, 0)));
42+
assert(start_of_hour_ms(-1001) == sec_to_ms(to_timestamp(1969, 12, 31, 23, 0, 0)));
43+
assert(end_of_hour_ms(pre_epoch_ms) == sec_to_ms(end_of_hour(pre_epoch)) + 999);
44+
45+
return 0;
46+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#include <time_shield/time_conversions.hpp>
2+
#include <time_shield/detail/floor_math.hpp>
3+
4+
#include <cassert>
5+
#include <cstdint>
6+
7+
namespace {
8+
int64_t expected_ts_ms(int64_t year, int month, int day, int hour, int min, int sec, int ms) {
9+
const int64_t unix_day = static_cast<int64_t>(time_shield::date_to_unix_day(year, month, day));
10+
int64_t sec_value = unix_day * static_cast<int64_t>(time_shield::SEC_PER_DAY) +
11+
static_cast<int64_t>(hour) * static_cast<int64_t>(time_shield::SEC_PER_HOUR) +
12+
static_cast<int64_t>(min) * static_cast<int64_t>(time_shield::SEC_PER_MIN) +
13+
static_cast<int64_t>(sec);
14+
int64_t ms_value = static_cast<int64_t>(ms);
15+
sec_value += time_shield::detail::floor_div(ms_value, static_cast<int64_t>(time_shield::MS_PER_SEC));
16+
ms_value = time_shield::detail::floor_mod(ms_value, static_cast<int64_t>(time_shield::MS_PER_SEC));
17+
return sec_value * static_cast<int64_t>(time_shield::MS_PER_SEC) + ms_value;
18+
}
19+
}
20+
21+
int main() {
22+
using namespace time_shield;
23+
24+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 0) == -1000);
25+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 1) == -999);
26+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 500) == -500);
27+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 999) == -1);
28+
assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, 0) == 0);
29+
assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, 1) == 1);
30+
31+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 1000) ==
32+
expected_ts_ms(1969, 12, 31, 23, 59, 59, 1000));
33+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, -1) ==
34+
expected_ts_ms(1969, 12, 31, 23, 59, 59, -1));
35+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, -1000) ==
36+
expected_ts_ms(1969, 12, 31, 23, 59, 59, -1000));
37+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, -1001) ==
38+
expected_ts_ms(1969, 12, 31, 23, 59, 59, -1001));
39+
assert(to_timestamp_ms(1969, 12, 31, 23, 59, 59, 1500) ==
40+
expected_ts_ms(1969, 12, 31, 23, 59, 59, 1500));
41+
assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, -1) ==
42+
expected_ts_ms(1970, 1, 1, 0, 0, 0, -1));
43+
assert(to_timestamp_ms(1970, 1, 1, 0, 0, 0, 1234567) ==
44+
expected_ts_ms(1970, 1, 1, 0, 0, 0, 1234567));
45+
46+
assert(to_timestamp_ms(1900, 3, 1, 0, 0, 0, 123) ==
47+
expected_ts_ms(1900, 3, 1, 0, 0, 0, 123));
48+
assert(to_timestamp_ms(2000, 2, 29, 12, 34, 56, 789) ==
49+
expected_ts_ms(2000, 2, 29, 12, 34, 56, 789));
50+
assert(to_timestamp_ms(2100, 3, 1, 0, 0, 0, 0) ==
51+
expected_ts_ms(2100, 3, 1, 0, 0, 0, 0));
52+
53+
DateTimeStruct dt{1969, 12, 31, 23, 59, 59, 500};
54+
assert(dt_to_timestamp_ms(dt) == to_timestamp_ms(1969, 12, 31, 23, 59, 59, 500));
55+
56+
return 0;
57+
}

tests/test_year_boundaries_ms.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#include <time_shield/time_conversions.hpp>
2+
#include <time_shield/detail/floor_math.hpp>
3+
4+
#include <array>
5+
#include <cassert>
6+
#include <cstdint>
7+
8+
int main() {
9+
using namespace time_shield;
10+
11+
const auto check_ms = [](ts_ms_t ts_ms) {
12+
const int64_t sec_floor = detail::floor_div<int64_t>(
13+
static_cast<int64_t>(ts_ms), MS_PER_SEC);
14+
const ts_t start_sec = start_of_year(static_cast<ts_t>(sec_floor));
15+
const ts_t end_sec = end_of_year(static_cast<ts_t>(sec_floor));
16+
const ts_ms_t ref_start_ms = static_cast<ts_ms_t>(start_sec * MS_PER_SEC);
17+
const ts_ms_t ref_end_ms = static_cast<ts_ms_t>(end_sec * MS_PER_SEC + MS_PER_SEC - 1);
18+
assert(start_of_year_ms(ts_ms) == ref_start_ms);
19+
assert(end_of_year_ms(ts_ms) == ref_end_ms);
20+
assert(ref_start_ms <= ts_ms);
21+
assert(ts_ms <= ref_end_ms);
22+
};
23+
24+
const std::array<ts_ms_t, 10> near_epoch_ms = {
25+
-2001, -2000, -1001, -1000, -999, -2, -1, 0, 1, 999
26+
};
27+
for (ts_ms_t ts_ms : near_epoch_ms) {
28+
check_ms(ts_ms);
29+
}
30+
check_ms(1000);
31+
check_ms(1001);
32+
33+
const std::array<int64_t, 10> years = {
34+
1969, 1970, 1971, 1972, 1900, 2000, 2100, 2400, 1600, -2400
35+
};
36+
const std::array<int64_t, 7> start_offsets = {
37+
-1000, -999, -1, 0, 1, 999, 1000
38+
};
39+
const std::array<int64_t, 5> end_offsets = {
40+
-1000, -1, 0, 1, 1000
41+
};
42+
43+
for (int64_t year : years) {
44+
const ts_t year_start_sec = to_timestamp(year, 1, 1, 0, 0, 0);
45+
const ts_ms_t year_start_ms = static_cast<ts_ms_t>(year_start_sec * MS_PER_SEC);
46+
for (int64_t offset : start_offsets) {
47+
check_ms(year_start_ms + offset);
48+
}
49+
50+
const ts_t year_mid_sec = to_timestamp(year, 6, 1, 0, 0, 0);
51+
const ts_t year_end_sec = end_of_year(year_mid_sec);
52+
const ts_ms_t year_end_ms = static_cast<ts_ms_t>(year_end_sec * MS_PER_SEC + MS_PER_SEC - 1);
53+
for (int64_t offset : end_offsets) {
54+
check_ms(year_end_ms + offset);
55+
}
56+
}
57+
58+
const std::array<ts_ms_t, 11> extremes = {
59+
MIN_TIMESTAMP_MS + 1,
60+
MIN_TIMESTAMP_MS + 999,
61+
MIN_TIMESTAMP_MS + 1000,
62+
MIN_TIMESTAMP_MS,
63+
MIN_TIMESTAMP_MS - 1,
64+
MIN_TIMESTAMP_MS - 999,
65+
MIN_TIMESTAMP_MS - 1000,
66+
MAX_TIMESTAMP_MS,
67+
MAX_TIMESTAMP_MS - 1,
68+
MAX_TIMESTAMP_MS - 999,
69+
MAX_TIMESTAMP_MS - 1000
70+
};
71+
for (ts_ms_t ts_ms : extremes) {
72+
check_ms(ts_ms);
73+
}
74+
75+
(void)detail::floor_mod<int64_t>(0, 1);
76+
return 0;
77+
}

0 commit comments

Comments
 (0)