userver: userver/utils/time_of_day.hpp Source File
Loading...
Searching...
No Matches
time_of_day.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/time_of_day.hpp
4/// @brief @copybrief utils::datetime::TimeOfDay
5
6#include <algorithm>
7#include <array>
8#include <chrono>
9#include <compare>
10#include <string_view>
11#include <type_traits>
12#include <vector>
13
14#include <fmt/format.h>
15
16#include <userver/utils/fmt_compat.hpp>
17
18USERVER_NAMESPACE_BEGIN
19
20namespace logging {
21class LogHelper;
22}
23
24namespace utils::datetime {
25
26/// @ingroup userver_universal
27///
28/// @brief A simple implementation of a "time since midnight" datatype.
29///
30/// This type is time-zone ignorant.
31///
32/// Valid time range is from [00:00:00.0, 24:00:00.0)
33///
34/// Available construction:
35///
36/// from duration (since midnight, the value will be normalized, e.g. 25:00 will
37/// become 01:00, 24:00 will become 00:00)
38///
39/// from string representation HH:MM[:SS[.s]], accepted range is from 00:00 to
40/// 24:00
41///
42/// construction from int in form 1300 as a static member function
43///
44/// Accessors:
45///
46/// int Hours(); // hours since midnight
47/// int Minutes(); // minutes since midnight + Hours()
48/// int Seconds(); // seconds since midnight + Hours() + Minutes()
49/// int Subseconds(); // seconds fraction since
50/// midnight + Hours() + Minutes() + Seconds()
51///
52///
53/// Output:
54///
55/// LOG(xxx) << val
56///
57/// Formatting:
58///
59/// fmt::format("{}", val)
60///
61/// Default format for hours and minutes is HH:MM, for seconds HH:MM:SS, for
62/// subseconds HH:MM:SS.s with fractional part, but truncating trailing zeros.
63///
64/// Custom formatting:
65///
66/// fmt:format("{:%H%M%S}", val); // will output HHMMSS without separators
67///
68/// Format keys:
69/// %H 24-hour two-digit zero-padded hours
70/// %M two-digit zero-padded minutes
71/// %S two-digit zero-padded seconds
72/// %% literal %
73template <typename Duration>
74class TimeOfDay;
75
76template <typename Rep, typename Period>
77class TimeOfDay<std::chrono::duration<Rep, Period>> {
78public:
79 using DurationType = std::chrono::duration<Rep, Period>;
80
81 constexpr TimeOfDay() noexcept = default;
82 constexpr explicit TimeOfDay(DurationType) noexcept;
83 template <typename ORep, typename OPeriod>
84 constexpr explicit TimeOfDay(std::chrono::duration<ORep, OPeriod>) noexcept;
85 constexpr explicit TimeOfDay(std::string_view);
86
87 //@{
88 /** @name Comparison operators */
89 constexpr auto operator<=>(const TimeOfDay&) const = default;
90 //@}
91
92 //@{
93 /** @name Accessors */
94 /// @return Hours since midnight
95 constexpr std::chrono::hours Hours() const noexcept {
96 return std::chrono::duration_cast<std::chrono::hours>(since_midnight_);
97 }
98
99 /// @return Minutes since midnight + Hours
100 constexpr std::chrono::minutes Minutes() const noexcept;
101 /// @return Seconds since midnight + Hours + Minutes
102 constexpr std::chrono::seconds Seconds() const noexcept;
103 /// @return Fractional part of seconds since midnight + Hours + Minutes +
104 /// Seconds up to resolution
105 constexpr DurationType Subseconds() const noexcept;
106
107 /// @return Underlying duration representation
108 constexpr DurationType SinceMidnight() const noexcept;
109 //@}
110
111 /// Create time of day from integer representation, e.g.1330 => 13:30
112 constexpr static TimeOfDay FromHHMMInt(int);
113
114private:
115 DurationType since_midnight_{};
116};
117
118//@{
119/** @name Duration arithmetic */
120
121template <typename LDuration, typename RDuration>
122auto operator-(TimeOfDay<LDuration> lhs, TimeOfDay<RDuration> rhs) {
123 return lhs.SinceMidnight() - rhs.SinceMidnight();
124}
125
126template <typename Duration, typename Rep, typename Period>
127TimeOfDay<Duration> operator+(TimeOfDay<Duration> lhs, std::chrono::duration<Rep, Period> rhs) {
128 return TimeOfDay<Duration>{lhs.SinceMidnight() + rhs};
129}
130
131template <typename Duration, typename Rep, typename Period>
132TimeOfDay<Duration> operator-(TimeOfDay<Duration> lhs, std::chrono::duration<Rep, Period> rhs) {
133 return TimeOfDay<Duration>{lhs.SinceMidnight() - rhs};
134}
135//@}
136
137template <typename Duration>
138logging::LogHelper& operator<<(logging::LogHelper& lh, TimeOfDay<Duration> value) {
139 lh << fmt::to_string(value);
140 return lh;
141}
142
143namespace detail {
144template <typename Rep, typename Period>
145inline constexpr std::chrono::duration<Rep, Period>
146 kTwentyFourHours = std::chrono::duration_cast<std::chrono::duration<Rep, Period>>(std::chrono::hours{24});
147
148template <typename Rep, typename Period, typename ORep = Rep, typename OPeriod = Period>
149constexpr std::chrono::duration<Rep, Period> NormalizeTimeOfDay(std::chrono::duration<ORep, OPeriod> d) {
150 auto res = std::chrono::duration_cast<std::chrono::duration<Rep, Period>>(d) % kTwentyFourHours<Rep, Period>;
151 return res.count() < 0 ? res + kTwentyFourHours<Rep, Period> : res;
152}
153
154template <typename Ratio>
155inline constexpr std::size_t kDecimalPositions = 0;
156template <>
157inline constexpr std::size_t kDecimalPositions<std::milli> = 3;
158template <>
159inline constexpr std::size_t kDecimalPositions<std::micro> = 6;
160template <>
161inline constexpr const std::size_t kDecimalPositions<std::nano> = 9;
162
163constexpr std::intmax_t MissingDigits(std::size_t n) {
164 // As we support resolutions up to nano, all we need is up to 10^9
165 // clang-format off
166 constexpr std::intmax_t powers[]{
167 1,
168 10,
169 100,
170 1'000,
171 10'000,
172 100'000,
173 1'000'000,
174 10'000'000,
175 100'000'000,
176 1'000'000'000
177 };
178 // clang-format on
179
180 return powers[n];
181}
182
183template <typename Ratio>
184struct HasMinutes : std::false_type {};
185
186template <intmax_t Num, intmax_t Den>
187struct HasMinutes<std::ratio<Num, Den>> : std::integral_constant<bool, (Num <= 60LL)> {};
188
189template <typename Rep, typename Period>
190struct HasMinutes<std::chrono::duration<Rep, Period>> : HasMinutes<Period> {};
191
192template <typename Duration>
193struct HasMinutes<TimeOfDay<Duration>> : HasMinutes<Duration> {};
194
195template <typename T>
196constexpr inline bool kHasMinutes = HasMinutes<T>{};
197
198template <typename Ratio>
199struct HasSeconds : std::false_type {};
200
201template <intmax_t Num, intmax_t Den>
202struct HasSeconds<std::ratio<Num, Den>> : std::integral_constant<bool, (Num == 1)> {};
203
204template <typename Rep, typename Period>
205struct HasSeconds<std::chrono::duration<Rep, Period>> : HasSeconds<Period> {};
206
207template <typename Duration>
208struct HasSeconds<TimeOfDay<Duration>> : HasSeconds<Duration> {};
209
210template <typename T>
211constexpr inline bool kHasSeconds = HasSeconds<T>{};
212
213template <typename Ratio>
214struct HasSubseconds : std::false_type {};
215
216template <intmax_t Num, intmax_t Den>
217struct HasSubseconds<std::ratio<Num, Den>> : std::integral_constant<bool, (Den > 1)> {};
218
219template <typename Rep, typename Period>
220struct HasSubseconds<std::chrono::duration<Rep, Period>> : HasSubseconds<Period> {};
221
222template <typename Duration>
223struct HasSubseconds<TimeOfDay<Duration>> : HasSubseconds<Duration> {};
224
225template <typename T>
226constexpr inline bool kHasSubseconds = HasSubseconds<T>{};
227
228template <typename Rep, typename Period>
229class TimeOfDayParser {
230public:
231 using DurationType = std::chrono::duration<Rep, Period>;
232
233 constexpr DurationType operator()(std::string_view str) {
234 for (auto c : str) {
235 switch (c) {
236 case ':':
237 if (position_ >= kSeconds) {
238 throw std::runtime_error{fmt::format("Extra colon in TimeOfDay string `{}`", str)};
239 }
240 AssignCurrentPosition(str);
241 position_ = static_cast<TimePart>(position_ + 1);
242 break;
243 case '.':
244 if (position_ != kSeconds) {
245 throw std::runtime_error{fmt::format("Unexpected decimal point in TimeOfDay string `{}`", str)};
246 }
247 AssignCurrentPosition(str);
248 position_ = static_cast<TimePart>(position_ + 1);
249 break;
250 default:
251 if (!std::isdigit(c)) {
252 throw std::runtime_error{fmt::format("Unexpected character {} in TimeOfDay string `{}`", c, str)
253 };
254 }
255 if (position_ == kOverflow) {
256 continue;
257 } else if (position_ == kSubseconds) {
258 if (digit_count_ >= kDecimalPositions<Period>) {
259 AssignCurrentPosition(str);
260 position_ = kOverflow;
261 continue;
262 }
263 } else if (digit_count_ >= 2) {
264 throw std::runtime_error{fmt::format("Too much digits in TimeOfDay string `{}`", str)};
265 }
266 ++digit_count_;
267 current_number_ = current_number_ * 10 + (c - '0');
268 break;
269 }
270 }
271 if (position_ == kHour) {
272 throw std::runtime_error{fmt::format(
273 "Expected to have at least minutes after hours in "
274 "TimeOfDay string `{}`",
275 str
276 )};
277 }
278 AssignCurrentPosition(str);
279
280 auto sum = hours_ + minutes_ + seconds_ + subseconds_;
281 if (sum > kTwentyFourHours<Rep, Period>) {
282 throw std::runtime_error(fmt::format("TimeOfDay value {} is out of range [00:00, 24:00)", str));
283 }
284 return NormalizeTimeOfDay<Rep, Period>(sum);
285 }
286
287private:
288 enum TimePart {
289 kHour,
290 kMinutes,
291 kSeconds,
292 kSubseconds,
293 kOverflow
294 };
295
296 void AssignCurrentPosition(std::string_view str) {
297 switch (position_) {
298 case kHour: {
299 if (digit_count_ < 1) {
300 throw std::runtime_error{fmt::format("Not enough digits for hours in TimeOfDay string `{}`", str)};
301 }
302 if (current_number_ > 24) {
303 throw std::runtime_error{fmt::format("Invalid value for hours in TimeOfDay string `{}`", str)};
304 }
305 hours_ = std::chrono::hours{current_number_};
306 break;
307 }
308 case kMinutes: {
309 if (digit_count_ != 2) {
310 throw std::runtime_error{fmt::format("Not enough digits for minutes in TimeOfDay string `{}`", str)
311 };
312 }
313 if (current_number_ > 59) {
314 throw std::runtime_error{fmt::format("Invalid value for minutes in TimeOfDay string `{}`", str)};
315 }
316 minutes_ = std::chrono::minutes{current_number_};
317 break;
318 }
319 case kSeconds: {
320 if (digit_count_ != 2) {
321 throw std::runtime_error{fmt::format("Not enough digits for seconds in TimeOfDay string `{}`", str)
322 };
323 }
324 if (current_number_ > 59) {
325 throw std::runtime_error{fmt::format("Invalid value for seconds in TimeOfDay string `{}`", str)};
326 }
327 seconds_ = std::chrono::seconds{current_number_};
328 break;
329 }
330 case kSubseconds: {
331 if (digit_count_ < 1) {
332 throw std::runtime_error{
333 fmt::format("Not enough digits for subseconds in TimeOfDay string `{}`", str)
334 };
335 }
336 if constexpr (kHasSubseconds<Period>) {
337 // TODO check digit count and adjust if needed
338 if (digit_count_ < kDecimalPositions<Period>) {
339 current_number_ *= MissingDigits(kDecimalPositions<Period> - digit_count_);
340 }
341 subseconds_ = DurationType{current_number_};
342 }
343 break;
344 }
345 case kOverflow:
346 // Just ignore this
347 break;
348 }
349 current_number_ = 0;
350 digit_count_ = 0;
351 }
352
353 TimePart position_ = kHour;
354 std::chrono::hours hours_{0};
355 std::chrono::minutes minutes_{0};
356 std::chrono::seconds seconds_{0};
357 DurationType subseconds_{0};
358
359 std::size_t digit_count_{0};
360 std::size_t current_number_{0};
361};
362
363/// Format string used for format key `%H`, two-digit 24-hour left-padded by 0
364inline constexpr std::string_view kLongHourFormat = "{0:0>#2d}";
365/// Format string used for minutes, key `%M`, no variations
366inline constexpr std::string_view kMinutesFormat = "{1:0>#2d}";
367/// Format string used for seconds, key `%S`, no variations
368inline constexpr std::string_view kSecondsFormat = "{2:0>#2d}";
369/// Format string for subseconds, keys not yet assigned
370inline constexpr std::string_view kSubsecondsFormat = "{3}";
371
372template <typename Ratio>
373constexpr inline std::string_view kSubsecondsPreformat = ".0";
374template <>
375inline constexpr std::string_view kSubsecondsPreformat<std::milli> = ".{:0>#3d}";
376template <>
377inline constexpr std::string_view kSubsecondsPreformat<std::micro> = ".{:0>#6d}";
378template <>
379inline constexpr std::string_view kSubsecondsPreformat<std::nano> = ".{:0>#9d}";
380
381// Default format for formatting is HH:MM:SS
382template <typename Ratio>
383inline constexpr std::array<std::string_view, 5> kDefaultFormat{
384 {kLongHourFormat, ":", kMinutesFormat, ":", kSecondsFormat}
385};
386
387// Default format for formatting with minutes resolution is HH:MM
388template <>
389inline constexpr std::array<std::string_view, 3> kDefaultFormat<std::ratio<60, 1>>{
390 {kLongHourFormat, ":", kMinutesFormat}
391};
392
393// Default format for formatting with hours resolution is HH:MM
394template <>
395inline constexpr std::array<std::string_view, 3> kDefaultFormat<std::ratio<3600, 1>>{
396 {kLongHourFormat, ":", kMinutesFormat}
397};
398
399} // namespace detail
400
401template <typename Rep, typename Period>
402constexpr TimeOfDay<std::chrono::duration<Rep, Period>>::TimeOfDay(DurationType d) noexcept
403 : since_midnight_{detail::NormalizeTimeOfDay<Rep, Period>(d)} {}
404
405template <typename Rep, typename Period>
406template <typename ORep, typename OPeriod>
407constexpr TimeOfDay<std::chrono::duration<Rep, Period>>::TimeOfDay(std::chrono::duration<ORep, OPeriod> d) noexcept
408 : since_midnight_{detail::NormalizeTimeOfDay<Rep, Period>(d)} {}
409
410template <typename Rep, typename Period>
411constexpr TimeOfDay<std::chrono::duration<Rep, Period>>::TimeOfDay(std::string_view str)
412 : since_midnight_{detail::TimeOfDayParser<Rep, Period>{}(str)}
413{}
414
415template <typename Rep, typename Period>
416constexpr std::chrono::minutes TimeOfDay<std::chrono::duration<Rep, Period>>::Minutes() const noexcept {
417 if constexpr (detail::kHasMinutes<Period>) {
418 return std::chrono::duration_cast<std::chrono::minutes>(since_midnight_) -
419 std::chrono::duration_cast<std::chrono::minutes>(Hours());
420 } else {
421 return std::chrono::minutes{0};
422 }
423}
424
425template <typename Rep, typename Period>
426constexpr std::chrono::seconds TimeOfDay<std::chrono::duration<Rep, Period>>::Seconds() const noexcept {
427 if constexpr (detail::kHasSeconds<Period>) {
428 return std::chrono::duration_cast<std::chrono::seconds>(since_midnight_) -
429 std::chrono::duration_cast<
430 std::chrono::seconds>(std::chrono::duration_cast<std::chrono::minutes>(since_midnight_));
431 } else {
432 return std::chrono::seconds{0};
433 }
434}
435
436template <typename Rep, typename Period>
437constexpr typename TimeOfDay<std::chrono::duration<Rep, Period>>::DurationType TimeOfDay<
438 std::chrono::duration<Rep, Period>>::Subseconds() const noexcept {
439 if constexpr (detail::kHasSubseconds<Period>) {
440 return since_midnight_ - std::chrono::duration_cast<std::chrono::seconds>(since_midnight_);
441 } else {
442 return DurationType{0};
443 }
444}
445
446template <typename Rep, typename Period>
447constexpr typename TimeOfDay<std::chrono::duration<Rep, Period>>::DurationType TimeOfDay<
448 std::chrono::duration<Rep, Period>>::SinceMidnight() const noexcept {
449 return since_midnight_;
450}
451
452template <typename Rep, typename Period>
453constexpr TimeOfDay<std::chrono::duration<Rep, Period>> TimeOfDay<
454 std::chrono::duration<Rep, Period>>::FromHHMMInt(int hh_mm) {
455 auto mm = hh_mm % 100;
456 if (mm >= 60) {
457 throw std::runtime_error{fmt::format("Invalid value for minutes {} in int representation {}", mm, hh_mm)};
458 }
459 return TimeOfDay{std::chrono::minutes{hh_mm / 100 * 60 + mm}};
460}
461
462} // namespace utils::datetime
463
464USERVER_NAMESPACE_END
465
466namespace fmt {
467
468template <typename Duration>
469class formatter<USERVER_NAMESPACE::utils::datetime::TimeOfDay<Duration>> {
470 /// Format string used for format key `%H`, two-digit 24-hour left-padded by 0
471 static constexpr auto kLongHourFormat = USERVER_NAMESPACE::utils::datetime::detail::kLongHourFormat;
472 /// Format string used for minutes, key `%M`, no variations
473 static constexpr auto kMinutesFormat = USERVER_NAMESPACE::utils::datetime::detail::kMinutesFormat;
474 /// Format string used for seconds, key `%S`, no variations
475 static constexpr auto kSecondsFormat = USERVER_NAMESPACE::utils::datetime::detail::kSecondsFormat;
476 /// Format string for subseconds, keys not yet assigned
477 /// for use in representation
478 static constexpr auto kSubsecondsFormat = USERVER_NAMESPACE::utils::datetime::detail::kSubsecondsFormat;
479
480 static constexpr auto kSubsecondsPreformat = USERVER_NAMESPACE::utils::datetime::detail::kSubsecondsPreformat<
481 typename Duration::period>;
482
483 static constexpr std::string_view kLiteralPercent = "%";
484
485 static constexpr auto
486 kDefaultFormat = USERVER_NAMESPACE::utils::datetime::detail::kDefaultFormat<typename Duration::period>;
487
488 constexpr std::string_view GetFormatForKey(char key) {
489 // TODO Check if time part already seen
490 switch (key) {
491 case 'H':
492 return kLongHourFormat;
493 case 'M':
494 return kMinutesFormat;
495 case 'S':
496 return kSecondsFormat;
497 default:
498 throw format_error{fmt::format("Unsupported format key {}", key)};
499 }
500 }
501
502public:
503 constexpr auto parse(format_parse_context& ctx) {
504 enum { kChar, kPercent, kKey } state = kChar;
505 const auto* it = ctx.begin();
506 const auto* end = ctx.end();
507 const auto* begin = it;
508
509 bool custom_format = false;
510 std::size_t size = 0;
511 for (; it != end && *it != '}'; ++it) {
512 if (!custom_format) {
513 representation_size_ = 0;
514 custom_format = true;
515 }
516 if (*it == '%') {
517 if (state == kPercent) {
518 PushBackFmt(kLiteralPercent);
519 state = kKey;
520 } else {
521 if (state == kChar && size > 0) {
522 PushBackFmt({begin, size});
523 }
524 state = kPercent;
525 }
526 } else {
527 if (state == kPercent) {
528 PushBackFmt(GetFormatForKey(*it));
529 state = kKey;
530 } else if (state == kKey) {
531 // start new literal
532 begin = it;
533 size = 1;
534 state = kChar;
535 } else {
536 ++size;
537 }
538 }
539 }
540 if (!custom_format) {
541 for (const auto fmt : kDefaultFormat) {
542 PushBackFmt(fmt);
543 }
544 }
545 if (state == kChar) {
546 if (size > 0) {
547 PushBackFmt({begin, size});
548 }
549 } else if (state == kPercent) {
550 throw format_error{"No format key after percent character"};
551 }
552 return it;
553 }
554
555 template <typename FormatContext>
556 constexpr auto format(const USERVER_NAMESPACE::utils::datetime::TimeOfDay<Duration>& value, FormatContext& ctx)
557 const {
558 auto hours = value.Hours().count();
559 auto mins = value.Minutes().count();
560 auto secs = value.Seconds().count();
561
562 auto ss = value.Subseconds().count();
563
564 // Number of decimal positions (min 1) + point + null terminator
565 constexpr std::size_t buffer_size =
566 std::max(
567 USERVER_NAMESPACE::utils::datetime::detail::kDecimalPositions<typename Duration::period>,
568 std::size_t{1}
569 ) +
570 2;
571 char subseconds[buffer_size];
572 subseconds[0] = 0;
573 if (ss > 0 || !truncate_trailing_subseconds_) {
574 fmt::format_to(subseconds, kSubsecondsPreformat, ss);
575 subseconds[buffer_size - 1] = 0;
576 if (truncate_trailing_subseconds_) {
577 // Truncate trailing zeros
578 for (auto last = buffer_size - 2; last > 0 && subseconds[last] == '0'; --last) {
579 subseconds[last] = 0;
580 }
581 }
582 }
583
584 auto res = ctx.out();
585 for (std::size_t i = 0; i < representation_size_; ++i) {
586 res = format_to(ctx.out(), fmt::runtime(representation_[i]), hours, mins, secs, subseconds);
587 }
588 return res;
589 }
590
591private:
592 constexpr void PushBackFmt(std::string_view fmt) {
593 if (representation_size_ >= kRepresentationCapacity) {
594 throw format_error("Format string complexity exceeds TimeOfDay limits");
595 }
596 representation_[representation_size_++] = fmt;
597 }
598
599 // Enough for hours, minutes, seconds, text and some % literals.
600 static constexpr std::size_t kRepresentationCapacity = 10;
601
602 std::string_view representation_[kRepresentationCapacity]{};
603 std::size_t representation_size_{0};
604 bool truncate_trailing_subseconds_{true};
605};
606
607} // namespace fmt