xref: /openbmc/bmcweb/features/redfish/include/utils/time_utils.hpp (revision 2b82937ecef572954b49569177b16831cbc09cfe)
1 #pragma once
2 
3 #include "logging.hpp"
4 
5 #include <algorithm>
6 #include <charconv>
7 #include <chrono>
8 #include <cmath>
9 #include <compare>
10 #include <cstddef>
11 #include <cstdint>
12 #include <optional>
13 #include <ratio>
14 #include <string>
15 #include <string_view>
16 #include <system_error>
17 
18 // IWYU pragma: no_include <stddef.h>
19 // IWYU pragma: no_include <stdint.h>
20 
21 namespace redfish
22 {
23 
24 namespace time_utils
25 {
26 
27 namespace details
28 {
29 
30 constexpr intmax_t dayDuration = static_cast<intmax_t>(24 * 60 * 60);
31 using Days = std::chrono::duration<long long, std::ratio<dayDuration>>;
32 
33 inline void leftZeroPadding(std::string& str, const std::size_t padding)
34 {
35     if (str.size() < padding)
36     {
37         str.insert(0, padding - str.size(), '0');
38     }
39 }
40 
41 template <typename FromTime>
42 bool fromDurationItem(std::string_view& fmt, const char postfix,
43                       std::chrono::milliseconds& out)
44 {
45     const size_t pos = fmt.find(postfix);
46     if (pos == std::string::npos)
47     {
48         return true;
49     }
50     if ((pos + 1U) > fmt.size())
51     {
52         return false;
53     }
54 
55     const char* end = nullptr;
56     std::chrono::milliseconds::rep ticks = 0;
57     if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
58     {
59         end = fmt.data() + std::min<size_t>(pos, 3U);
60     }
61     else
62     {
63         end = fmt.data() + pos;
64     }
65 
66     auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks);
67     if (ptr != end || ec != std::errc())
68     {
69         BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: "
70                          << static_cast<int>(ec) << "("
71                          << std::make_error_code(ec).message() << "), ptr{"
72                          << static_cast<const void*>(ptr) << "} != end{"
73                          << static_cast<const void*>(end) << "})";
74         return false;
75     }
76 
77     if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
78     {
79         ticks *= static_cast<std::chrono::milliseconds::rep>(
80             std::pow(10, 3 - std::min<size_t>(pos, 3U)));
81     }
82     if (ticks < 0)
83     {
84         return false;
85     }
86 
87     out += FromTime(ticks);
88     const auto maxConversionRange =
89         std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max())
90             .count();
91     if (out < FromTime(ticks) || maxConversionRange < ticks)
92     {
93         return false;
94     }
95 
96     fmt.remove_prefix(pos + 1U);
97     return true;
98 }
99 } // namespace details
100 
101 /**
102  * @brief Convert string that represents value in Duration Format to its numeric
103  *        equivalent.
104  */
105 inline std::optional<std::chrono::milliseconds>
106     fromDurationString(const std::string& str)
107 {
108     std::chrono::milliseconds out = std::chrono::milliseconds::zero();
109     std::string_view v = str;
110 
111     if (v.empty())
112     {
113         return out;
114     }
115     if (v.front() != 'P')
116     {
117         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
118         return std::nullopt;
119     }
120 
121     v.remove_prefix(1);
122     if (!details::fromDurationItem<details::Days>(v, 'D', out))
123     {
124         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
125         return std::nullopt;
126     }
127 
128     if (v.empty())
129     {
130         return out;
131     }
132     if (v.front() != 'T')
133     {
134         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
135         return std::nullopt;
136     }
137 
138     v.remove_prefix(1);
139     if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) ||
140         !details::fromDurationItem<std::chrono::minutes>(v, 'M', out))
141     {
142         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
143         return std::nullopt;
144     }
145 
146     if (v.find('.') != std::string::npos && v.find('S') != std::string::npos)
147     {
148         if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) ||
149             !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out))
150         {
151             BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
152             return std::nullopt;
153         }
154     }
155     else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out))
156     {
157         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
158         return std::nullopt;
159     }
160 
161     if (!v.empty())
162     {
163         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
164         return std::nullopt;
165     }
166     return out;
167 }
168 
169 /**
170  * @brief Convert time value into duration format that is based on ISO 8601.
171  *        Example output: "P12DT1M5.5S"
172  *        Ref: Redfish Specification, Section 9.4.4. Duration values
173  */
174 inline std::string toDurationString(std::chrono::milliseconds ms)
175 {
176     if (ms < std::chrono::milliseconds::zero())
177     {
178         return "";
179     }
180 
181     std::string fmt;
182     fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS"));
183 
184     details::Days days = std::chrono::floor<details::Days>(ms);
185     ms -= days;
186 
187     std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms);
188     ms -= hours;
189 
190     std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms);
191     ms -= minutes;
192 
193     std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms);
194     ms -= seconds;
195 
196     fmt = "P";
197     if (days.count() > 0)
198     {
199         fmt += std::to_string(days.count()) + "D";
200     }
201     fmt += "T";
202     if (hours.count() > 0)
203     {
204         fmt += std::to_string(hours.count()) + "H";
205     }
206     if (minutes.count() > 0)
207     {
208         fmt += std::to_string(minutes.count()) + "M";
209     }
210     if (seconds.count() != 0 || ms.count() != 0)
211     {
212         fmt += std::to_string(seconds.count()) + ".";
213         std::string msStr = std::to_string(ms.count());
214         details::leftZeroPadding(msStr, 3);
215         fmt += msStr + "S";
216     }
217 
218     return fmt;
219 }
220 
221 inline std::optional<std::string>
222     toDurationStringFromUint(const uint64_t timeMs)
223 {
224     static const uint64_t maxTimeMs =
225         static_cast<uint64_t>(std::chrono::milliseconds::max().count());
226 
227     if (maxTimeMs < timeMs)
228     {
229         return std::nullopt;
230     }
231 
232     std::string duration = toDurationString(std::chrono::milliseconds(timeMs));
233     if (duration.empty())
234     {
235         return std::nullopt;
236     }
237 
238     return std::make_optional(duration);
239 }
240 
241 namespace details
242 {
243 // Returns year/month/day triple in civil calendar
244 // Preconditions:  z is number of days since 1970-01-01 and is in the range:
245 //                   [numeric_limits<Int>::min(),
246 //                   numeric_limits<Int>::max()-719468].
247 // Algorithm sourced from
248 // https://howardhinnant.github.io/date_algorithms.html#civil_from_days
249 // All constants are explained in the above
250 template <class IntType>
251 constexpr std::tuple<IntType, unsigned, unsigned>
252     civilFromDays(IntType z) noexcept
253 {
254     z += 719468;
255     IntType era = (z >= 0 ? z : z - 146096) / 146097;
256     unsigned doe = static_cast<unsigned>(z - era * 146097); // [0, 146096]
257     unsigned yoe =
258         (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
259     IntType y = static_cast<IntType>(yoe) + era * 400;
260     unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
261     unsigned mp = (5 * doy + 2) / 153;                      // [0, 11]
262     unsigned d = doy - (153 * mp + 2) / 5 + 1;              // [1, 31]
263     unsigned m = mp < 10 ? mp + 3 : mp - 9;                 // [1, 12]
264 
265     return std::tuple<IntType, unsigned, unsigned>(y + (m <= 2), m, d);
266 }
267 
268 // Creates a string from an integer in the most efficient way possible without
269 // using std::locale.  Adds an exact zero pad based on the pad input parameter.
270 // Does nt handle negative numbers.
271 inline std::string padZeros(int value, size_t pad)
272 {
273     std::string result(pad, '0');
274     for (int val = value; pad > 0; pad--)
275     {
276         result[pad - 1] = static_cast<char>('0' + val % 10);
277         val /= 10;
278     }
279     return result;
280 }
281 
282 template <typename IntType, typename Period>
283 std::string toISO8061ExtendedStr(std::chrono::duration<IntType, Period> t)
284 {
285     using seconds = std::chrono::duration<int>;
286     using minutes = std::chrono::duration<int, std::ratio<60>>;
287     using hours = std::chrono::duration<int, std::ratio<3600>>;
288     using days = std::chrono::duration<
289         IntType, std::ratio_multiply<hours::period, std::ratio<24>>>;
290 
291     // d is days since 1970-01-01
292     days d = std::chrono::duration_cast<days>(t);
293 
294     // t is now time duration since midnight of day d
295     t -= d;
296 
297     // break d down into year/month/day
298     int year = 0;
299     int month = 0;
300     int day = 0;
301     std::tie(year, month, day) = details::civilFromDays(d.count());
302     // Check against limits.  Can't go above year 9999, and can't go below epoch
303     // (1970)
304     if (year >= 10000)
305     {
306         year = 9999;
307         month = 12;
308         day = 31;
309         t = days(1) - std::chrono::duration<IntType, Period>(1);
310     }
311     else if (year < 1970)
312     {
313         year = 1970;
314         month = 1;
315         day = 1;
316         t = std::chrono::duration<IntType, Period>::zero();
317     }
318 
319     std::string out;
320     out += details::padZeros(year, 4);
321     out += '-';
322     out += details::padZeros(month, 2);
323     out += '-';
324     out += details::padZeros(day, 2);
325     out += 'T';
326     hours hr = duration_cast<hours>(t);
327     out += details::padZeros(hr.count(), 2);
328     t -= hr;
329     out += ':';
330 
331     minutes mt = duration_cast<minutes>(t);
332     out += details::padZeros(mt.count(), 2);
333     t -= mt;
334     out += ':';
335 
336     seconds se = duration_cast<seconds>(t);
337     out += details::padZeros(se.count(), 2);
338     t -= se;
339 
340     if constexpr (std::is_same_v<typename decltype(t)::period, std::milli>)
341     {
342         out += '.';
343         using MilliDuration = std::chrono::duration<int, std::milli>;
344         MilliDuration subsec = duration_cast<MilliDuration>(t);
345         out += details::padZeros(subsec.count(), 3);
346     }
347     else if constexpr (std::is_same_v<typename decltype(t)::period, std::micro>)
348     {
349         out += '.';
350 
351         using MicroDuration = std::chrono::duration<int, std::micro>;
352         MicroDuration subsec = duration_cast<MicroDuration>(t);
353         out += details::padZeros(subsec.count(), 6);
354     }
355 
356     out += "+00:00";
357     return out;
358 }
359 } // namespace details
360 
361 // Returns the formatted date time string.
362 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if
363 // the given |secondsSinceEpoch| is too large, we return the maximum supported
364 // date.
365 inline std::string getDateTimeUint(uint64_t secondsSinceEpoch)
366 {
367     using DurationType = std::chrono::duration<uint64_t>;
368     DurationType sinceEpoch(secondsSinceEpoch);
369     return details::toISO8061ExtendedStr(sinceEpoch);
370 }
371 
372 // Returns the formatted date time string with millisecond precision
373 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if
374 // the given |secondsSinceEpoch| is too large, we return the maximum supported
375 // date.
376 inline std::string getDateTimeUintMs(uint64_t milliSecondsSinceEpoch)
377 {
378     using DurationType = std::chrono::duration<uint64_t, std::milli>;
379     DurationType sinceEpoch(milliSecondsSinceEpoch);
380     return details::toISO8061ExtendedStr(sinceEpoch);
381 }
382 
383 // Returns the formatted date time string with microsecond precision
384 inline std::string getDateTimeUintUs(uint64_t microSecondsSinceEpoch)
385 {
386     using DurationType = std::chrono::duration<uint64_t, std::micro>;
387     DurationType sinceEpoch(microSecondsSinceEpoch);
388     return details::toISO8061ExtendedStr(sinceEpoch);
389 }
390 
391 inline std::string getDateTimeStdtime(std::time_t secondsSinceEpoch)
392 {
393     using DurationType = std::chrono::duration<std::time_t>;
394     DurationType sinceEpoch(secondsSinceEpoch);
395     return details::toISO8061ExtendedStr(sinceEpoch);
396 }
397 
398 /**
399  * Returns the current Date, Time & the local Time Offset
400  * infromation in a pair
401  *
402  * @param[in] None
403  *
404  * @return std::pair<std::string, std::string>, which consist
405  * of current DateTime & the TimeOffset strings respectively.
406  */
407 inline std::pair<std::string, std::string> getDateTimeOffsetNow()
408 {
409     std::time_t time = std::time(nullptr);
410     std::string dateTime = getDateTimeStdtime(time);
411 
412     /* extract the local Time Offset value from the
413      * recevied dateTime string.
414      */
415     std::string timeOffset("Z00:00");
416     std::size_t lastPos = dateTime.size();
417     std::size_t len = timeOffset.size();
418     if (lastPos > len)
419     {
420         timeOffset = dateTime.substr(lastPos - len);
421     }
422 
423     return std::make_pair(dateTime, timeOffset);
424 }
425 
426 } // namespace time_utils
427 } // namespace redfish
428