3030#include " mgutility/std/charconv.hpp"
3131#include " mgutility/std/string_view.hpp"
3232
33- #include < array>
3433#include < cctype>
3534#include < chrono>
35+ #include < cstddef>
36+ #include < cstdint>
3637#include < stdexcept>
3738#include < type_traits>
3839
39- // NOLINTBEGIN(modernize-concat-nested-namespaces)
40+ // NOLINTBEGIN(modernize-concat-nested-namespaces)
4041namespace mgutility {
4142namespace chrono {
4243namespace detail {
43- // NOLINTEND(modernize-concat-nested-namespaces)
44+ // NOLINTEND(modernize-concat-nested-namespaces)
4445
4546/* *
4647 * @brief Extended tm structure with milliseconds.
@@ -71,6 +72,16 @@ MGUTILITY_CNSTXPR auto parse_integer(T &result, mgutility::string_view str,
7172 return error.ec ;
7273}
7374
75+ /* *
76+ * @brief Returns the absolute value of an integer.
77+ *
78+ * @param value
79+ * @return constexpr int32_t
80+ */
81+ constexpr int32_t abs (int32_t value) noexcept {
82+ return value >= 0 ? value : -value;
83+ }
84+
7485/* *
7586 * @brief Checks if a value is within a given range.
7687 *
@@ -79,8 +90,9 @@ MGUTILITY_CNSTXPR auto parse_integer(T &result, mgutility::string_view str,
7990 * @param max The maximum acceptable value.
8091 * @throws std::out_of_range if the value is out of range.
8192 */
82- template <typename T>
83- MGUTILITY_CNSTXPR auto check_range (const T& value, const T& min, const T& max) -> std::errc {
93+ template <typename T>
94+ MGUTILITY_CNSTXPR auto check_range (const T &value, const T &min, const T &max)
95+ -> std::errc {
8496 if (value < min || value > max) {
8597 return std::errc::result_out_of_range;
8698 }
@@ -97,39 +109,62 @@ auto MGUTILITY_CNSTXPR is_leap_year(int32_t year) -> bool {
97109 return (year % 4 ) == 0 && ((year % 100 ) != 0 || (year % 400 ) == 0 );
98110}
99111
112+ /* *
113+ * @brief Returns the number of days in a given month of a given year.
114+ *
115+ * @param year
116+ * @param month
117+ * @return int32_t
118+ */
119+ // NOLINTNEXTLINE
120+ auto MGUTILITY_CNSTXPR days_in_month (int32_t year, int32_t month) -> int32_t {
121+ // NOLINTNEXTLINE
122+ constexpr int days_per_month[] = {31 , 28 , 31 , 30 , 31 , 30 ,
123+ 31 , 31 , 30 , 31 , 30 , 31 };
124+ if (month < 0 || month > 11 ) {
125+ return 0 ; // Invalid month
126+ }
127+ if (month == 1 ) { // February
128+ return is_leap_year (year) ? 29 : 28 ;
129+ }
130+ // NOLINTNEXTLINE
131+ return days_per_month[month];
132+ }
133+
100134/* *
101135 * @brief Converts a tm structure to a time_t value.
102136 *
103137 * @param tm The tm structure to convert.
104138 * @return std::time_t The corresponding time_t value.
105139 * @throws std::out_of_range if any tm value is out of valid range.
106140 */
107- MGUTILITY_CNSTXPR auto mktime (std::time_t &result, std::tm &time_struct) -> std::errc {
108- MGUTILITY_CNSTXPR std::array<std::array<uint32_t , 12 >, 2 > num_of_days{
109- {{31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 ,
110- 31 }, // 365 days in a common year
111- {31 , 29 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 ,
112- 31 }}}; // 366 days in a leap year
113-
141+ MGUTILITY_CNSTXPR auto mktime (std::time_t &result, std::tm &time_struct)
142+ -> std::errc {
114143 result = 0 ;
115144
116145 // Check for out of range values in tm structure
117- if (time_struct.tm_mon > 12 || time_struct.tm_mon < 0 || time_struct.tm_mday > 31 || time_struct.tm_min > 60 ||
146+ if (time_struct.tm_mon > 12 || time_struct.tm_mon < 0 ||
147+ time_struct.tm_mday > 31 || time_struct.tm_min > 60 ||
118148 time_struct.tm_sec > 60 || time_struct.tm_hour > 24 ) {
119149 return std::errc::result_out_of_range;
120150 }
121151
122152 time_struct.tm_year += 1900 ;
123153
154+ if (days_in_month (time_struct.tm_year , time_struct.tm_mon ) <
155+ time_struct.tm_mday ) {
156+ return std::errc::result_out_of_range;
157+ }
158+
124159 // Calculate the number of days since 1970
125160 for (auto i{1970 }; i < time_struct.tm_year ; ++i) {
126161 result += is_leap_year (i) ? 366 : 365 ;
127162 }
128163
129164 // Add the days for the current year
130165 for (auto i{0 }; i < time_struct.tm_mon ; ++i) {
131- // NOLINTNEXTLINE
132- result += num_of_days[ is_leap_year (time_struct.tm_year )][ static_cast <std:: size_t >(i)] ;
166+ // NOLINTNEXTLINE
167+ result += days_in_month (time_struct.tm_year , i) ;
133168 }
134169
135170 result += time_struct.tm_mday - 1 ; // nth day since 1970
@@ -149,54 +184,82 @@ MGUTILITY_CNSTXPR auto mktime(std::time_t &result, std::tm &time_struct) -> std:
149184 * @param tm The tm structure to adjust.
150185 * @param offset The timezone offset in hours and minutes.
151186 */
152- MGUTILITY_CNSTXPR auto handle_timezone (tm &time_struct, int32_t offset) -> void {
153- const auto minute = offset % 100 ;
154- const auto hour = offset / 100 ;
155-
156- if (offset < 0 ) {
157- if (time_struct.tm_min + minute < 0 ) {
158- time_struct.tm_min += 60 - minute;
159- time_struct.tm_hour -= 1 ;
160- if (time_struct.tm_hour < 0 ) {
161- time_struct.tm_hour += 24 ;
162- time_struct.tm_mday -= 1 ;
163- }
164- } else {
165- time_struct.tm_min += minute;
166- }
187+ MGUTILITY_CNSTXPR auto handle_timezone (tm &time_struct, int32_t offset)
188+ -> std::errc {
189+ // Validate offset: HHMM format, minutes 0-59, hours 0-23
190+ const int32_t abs_offset = abs (offset);
191+ const int32_t minutes = abs_offset % 100 ;
192+ const int32_t hours = abs_offset / 100 ;
193+ if (minutes > 59 || hours > 23 ) {
194+ return std::errc::invalid_argument;
195+ }
167196
168- if (time_struct.tm_hour + hour < 0 ) {
169- time_struct.tm_hour += 24 + hour;
170- time_struct.tm_mday -= 1 ;
171- } else {
172- time_struct.tm_hour += hour;
173- }
174- } else {
175- if (time_struct.tm_min + minute >= 60 ) {
176- time_struct.tm_min -= 60 - minute;
177- time_struct.tm_hour += 1 ;
178- if (time_struct.tm_hour >= 24 ) {
179- time_struct.tm_hour -= 24 ;
180- time_struct.tm_mday += 1 ;
197+ // Validate input tm structure (basic checks)
198+ if (time_struct.tm_mon < 0 || time_struct.tm_mon > 11 ||
199+ time_struct.tm_mday < 1 || time_struct.tm_year < 0 ) {
200+ return std::errc::invalid_argument;
201+ }
202+
203+ // Apply offset (positive or negative)
204+ const int32_t total_minutes =
205+ time_struct.tm_min + (offset >= 0 ? minutes : -minutes);
206+ const int32_t total_hours =
207+ time_struct.tm_hour + (offset >= 0 ? hours : -hours);
208+
209+ // Normalize minutes (-59 to 119 -> 0-59 with hour carry)
210+ time_struct.tm_min = total_minutes % 60 ;
211+ int minute_carry = total_minutes / 60 ;
212+ if (total_minutes < 0 && total_minutes % 60 != 0 ) {
213+ minute_carry -= 1 ;
214+ time_struct.tm_min += 60 ;
215+ }
216+
217+ // Normalize hours (-23 to 47 -> 0-23 with day carry)
218+ time_struct.tm_hour = (total_hours + minute_carry) % 24 ;
219+ int day_carry = (total_hours + minute_carry) / 24 ;
220+ if (total_hours + minute_carry < 0 &&
221+ (total_hours + minute_carry) % 24 != 0 ) {
222+ day_carry -= 1 ;
223+ time_struct.tm_hour += 24 ;
224+ }
225+
226+ // Normalize days, months, and years
227+ int days = time_struct.tm_mday + day_carry;
228+ int months = time_struct.tm_mon ;
229+ int years = time_struct.tm_year ;
230+
231+ // Handle negative days
232+ while (days <= 0 ) {
233+ months -= 1 ;
234+ if (months < 0 ) {
235+ months += 12 ;
236+ years -= 1 ;
237+ if (years < 0 ) {
238+ return std::errc::result_out_of_range; // Year underflow
181239 }
182- } else {
183- time_struct.tm_min += minute;
184240 }
241+ days += days_in_month (years, months);
242+ }
185243
186- if (time_struct. tm_hour + hour >= 24 ) {
187- time_struct. tm_hour += hour - 24 ;
188- time_struct. tm_mday += 1 ;
189- if (time_struct. tm_mon == 11 && time_struct. tm_mday > 31 ) {
190- time_struct. tm_mday = 1 ;
191- time_struct. tm_mon = 0 ;
192- } else if (time_struct. tm_mday > 30 ) {
193- time_struct. tm_mday = 1 ;
194- time_struct. tm_mon += 1 ;
244+ // Handle day overflow
245+ while (days > days_in_month (years, months)) {
246+ days -= days_in_month (years, months) ;
247+ months += 1 ;
248+ if (months > 11 ) {
249+ months -= 12 ;
250+ years += 1 ;
251+ if (years > 9999 - 1900 ) { // Avoid overflow (arbitrary limit)
252+ return std::errc::result_out_of_range ;
195253 }
196- } else {
197- time_struct.tm_hour += hour;
198254 }
199255 }
256+
257+ // Update tm structure
258+ time_struct.tm_mday = days;
259+ time_struct.tm_mon = months;
260+ time_struct.tm_year = years;
261+
262+ return std::errc{};
200263}
201264
202265// Free parsing functions
@@ -220,7 +283,8 @@ MGUTILITY_CNSTXPR auto parse_day(detail::tm &result, string_view date_str,
220283 if (error != std::errc{}) {
221284 return error;
222285 }
223- error = check_range (result.tm_mday , 1 , 31 );
286+ error = check_range (result.tm_mday , 1 ,
287+ days_in_month (result.tm_year , result.tm_mon ));
224288 return error;
225289}
226290
@@ -264,15 +328,18 @@ MGUTILITY_CNSTXPR auto parse_fraction(detail::tm &result, string_view date_str,
264328 return error;
265329}
266330
267- MGUTILITY_CNSTXPR auto parse_timezone_offset (detail::tm &result, string_view date_str,
268- uint32_t &next) -> std::errc {
331+ MGUTILITY_CNSTXPR auto parse_timezone_offset (detail::tm &result,
332+ string_view date_str,
333+ uint32_t &next) -> std::errc {
269334 std::errc error{};
335+ // NOLINTNEXTLINE [bugprone-inc-dec-in-conditions]
270336 if (--next < date_str.size () && date_str[next] == ' Z' ) {
271- handle_timezone (result, 0 );
337+ error = handle_timezone (result, 0 );
272338 return error;
273339 }
274340
275- if (next >= date_str.size () || (date_str[next] != ' +' && date_str[next] != ' -' )) {
341+ if (next >= date_str.size () ||
342+ (date_str[next] != ' +' && date_str[next] != ' -' )) {
276343 return std::errc::invalid_argument;
277344 }
278345
@@ -294,13 +361,13 @@ MGUTILITY_CNSTXPR auto parse_timezone_offset(detail::tm &result, string_view dat
294361 return error;
295362 }
296363
297- const int32_t offset = hour * 100 + minute;
364+ const int32_t offset = ( hour * 100 ) + minute;
298365 error = check_range (offset, 0 , 1200 );
299366 if (error != std::errc{}) {
300367 return error;
301368 }
302369
303- handle_timezone (result, sign == ' +' ? -offset : offset);
370+ error = handle_timezone (result, sign == ' +' ? -offset : offset);
304371 return error;
305372}
306373
@@ -345,11 +412,11 @@ MGUTILITY_CNSTXPR auto get_time(detail::tm &result, string_view format,
345412 error = parse_day (result, date_str, next);
346413 break ;
347414 case ' F' : {
348- // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
415+ // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
349416 error = parse_year (result, date_str, next);
350- // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
417+ // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
351418 error = parse_month (result, date_str, next);
352- // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
419+ // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
353420 error = parse_day (result, date_str, next);
354421 } break ;
355422 case ' H' :
@@ -362,11 +429,11 @@ MGUTILITY_CNSTXPR auto get_time(detail::tm &result, string_view format,
362429 error = parse_second (result, date_str, next);
363430 break ;
364431 case ' T' : {
365- // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
432+ // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
366433 error = parse_hour (result, date_str, next);
367- // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
434+ // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
368435 error = parse_minute (result, date_str, next);
369- // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
436+ // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
370437 error = parse_second (result, date_str, next);
371438 } break ;
372439 case ' f' :
0 commit comments