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