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