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