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