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