xref: /openbmc/bmcweb/features/redfish/include/utils/time_utils.hpp (revision 543f44000a992870ff76e76888dd589a3a31ed4e)
1 #pragma once
2 
3 #include "logging.hpp"
4 
5 #include <charconv>
6 #include <chrono>
7 #include <cmath>
8 #include <optional>
9 #include <string>
10 #include <system_error>
11 
12 namespace redfish
13 {
14 
15 namespace time_utils
16 {
17 
18 namespace details
19 {
20 
21 constexpr intmax_t dayDuration = static_cast<intmax_t>(24 * 60 * 60);
22 using Days = std::chrono::duration<long long, std::ratio<dayDuration>>;
23 
24 inline void leftZeroPadding(std::string& str, const std::size_t padding)
25 {
26     if (str.size() < padding)
27     {
28         str.insert(0, padding - str.size(), '0');
29     }
30 }
31 
32 template <typename FromTime>
33 bool fromDurationItem(std::string_view& fmt, const char postfix,
34                       std::chrono::milliseconds& out)
35 {
36     const size_t pos = fmt.find(postfix);
37     if (pos == std::string::npos)
38     {
39         return true;
40     }
41     if ((pos + 1U) > fmt.size())
42     {
43         return false;
44     }
45 
46     const char* end = nullptr;
47     std::chrono::milliseconds::rep ticks = 0;
48     if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
49     {
50         end = fmt.data() + std::min<size_t>(pos, 3U);
51     }
52     else
53     {
54         end = fmt.data() + pos;
55     }
56 
57     auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks);
58     if (ptr != end || ec != std::errc())
59     {
60         BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: "
61                          << static_cast<int>(ec) << "("
62                          << std::make_error_code(ec).message() << "), ptr{"
63                          << static_cast<const void*>(ptr) << "} != end{"
64                          << static_cast<const void*>(end) << "})";
65         return false;
66     }
67 
68     if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
69     {
70         ticks *= static_cast<std::chrono::milliseconds::rep>(
71             std::pow(10, 3 - std::min<size_t>(pos, 3U)));
72     }
73     if (ticks < 0)
74     {
75         return false;
76     }
77 
78     out += FromTime(ticks);
79     const auto maxConversionRange =
80         std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max())
81             .count();
82     if (out < FromTime(ticks) || maxConversionRange < ticks)
83     {
84         return false;
85     }
86 
87     fmt.remove_prefix(pos + 1U);
88     return true;
89 }
90 } // namespace details
91 
92 /**
93  * @brief Convert string that represents value in Duration Format to its numeric
94  *        equivalent.
95  */
96 inline std::optional<std::chrono::milliseconds>
97     fromDurationString(const std::string& str)
98 {
99     std::chrono::milliseconds out = std::chrono::milliseconds::zero();
100     std::string_view v = str;
101 
102     if (v.empty())
103     {
104         return out;
105     }
106     if (v.front() != 'P')
107     {
108         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
109         return std::nullopt;
110     }
111 
112     v.remove_prefix(1);
113     if (!details::fromDurationItem<details::Days>(v, 'D', out))
114     {
115         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
116         return std::nullopt;
117     }
118 
119     if (v.empty())
120     {
121         return out;
122     }
123     if (v.front() != 'T')
124     {
125         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
126         return std::nullopt;
127     }
128 
129     v.remove_prefix(1);
130     if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) ||
131         !details::fromDurationItem<std::chrono::minutes>(v, 'M', out))
132     {
133         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
134         return std::nullopt;
135     }
136 
137     if (v.find('.') != std::string::npos && v.find('S') != std::string::npos)
138     {
139         if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) ||
140             !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out))
141         {
142             BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
143             return std::nullopt;
144         }
145     }
146     else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out))
147     {
148         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
149         return std::nullopt;
150     }
151 
152     if (!v.empty())
153     {
154         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
155         return std::nullopt;
156     }
157     return out;
158 }
159 
160 /**
161  * @brief Convert time value into duration format that is based on ISO 8601.
162  *        Example output: "P12DT1M5.5S"
163  *        Ref: Redfish Specification, Section 9.4.4. Duration values
164  */
165 inline std::string toDurationString(std::chrono::milliseconds ms)
166 {
167     if (ms < std::chrono::milliseconds::zero())
168     {
169         return "";
170     }
171 
172     std::string fmt;
173     fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS"));
174 
175     details::Days days = std::chrono::floor<details::Days>(ms);
176     ms -= days;
177 
178     std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms);
179     ms -= hours;
180 
181     std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms);
182     ms -= minutes;
183 
184     std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms);
185     ms -= seconds;
186 
187     fmt = "P";
188     if (days.count() > 0)
189     {
190         fmt += std::to_string(days.count()) + "D";
191     }
192     fmt += "T";
193     if (hours.count() > 0)
194     {
195         fmt += std::to_string(hours.count()) + "H";
196     }
197     if (minutes.count() > 0)
198     {
199         fmt += std::to_string(minutes.count()) + "M";
200     }
201     if (seconds.count() != 0 || ms.count() != 0)
202     {
203         fmt += std::to_string(seconds.count()) + ".";
204         std::string msStr = std::to_string(ms.count());
205         details::leftZeroPadding(msStr, 3);
206         fmt += msStr + "S";
207     }
208 
209     return fmt;
210 }
211 
212 inline std::optional<std::string>
213     toDurationStringFromUint(const uint64_t timeMs)
214 {
215     static const uint64_t maxTimeMs =
216         static_cast<uint64_t>(std::chrono::milliseconds::max().count());
217 
218     if (maxTimeMs < timeMs)
219     {
220         return std::nullopt;
221     }
222 
223     std::string duration = toDurationString(std::chrono::milliseconds(timeMs));
224     if (duration.empty())
225     {
226         return std::nullopt;
227     }
228 
229     return std::make_optional(duration);
230 }
231 
232 } // namespace time_utils
233 } // namespace redfish
234