quill
StringFromTime.h
1 
7 #pragma once
8 
9 #include "quill/bundled/fmt/format.h"
10 #include "quill/core/Attributes.h"
11 #include "quill/core/Common.h"
12 #include "quill/core/QuillError.h"
13 #include "quill/core/TimeUtilities.h"
14 
15 #include <array>
16 #include <chrono>
17 #include <cstdint>
18 #include <cstdlib>
19 #include <ctime>
20 #include <map>
21 #include <string>
22 #include <utility>
23 #include <vector>
24 
25 QUILL_BEGIN_NAMESPACE
26 
27 namespace detail
28 {
50 {
51 public:
52  /***/
53  QUILL_ATTRIBUTE_COLD void init(std::string timestamp_format, Timezone timezone)
54  {
55  _timestamp_format = std::move(timestamp_format);
56  _time_zone = timezone;
57 
58  if (_timestamp_format.find("%X") != std::string::npos)
59  {
60  QUILL_THROW(QuillError("`%X` as format modifier is not currently supported in format: " + _timestamp_format));
61  }
62 
63  // We first look for some special format modifiers and replace them
64  _replace_all(_timestamp_format, "%r", "%I:%M:%S %p");
65  _replace_all(_timestamp_format, "%R", "%H:%M");
66  _replace_all(_timestamp_format, "%T", "%H:%M:%S");
67 
68  // Populate the initial parts that we will use to generate a pre-formatted string
69  _populate_initial_parts(_timestamp_format);
70  }
71 
72  /***/
73  QUILL_NODISCARD QUILL_ATTRIBUTE_HOT std::string const& format_timestamp(time_t timestamp)
74  {
75  // First we check for the edge case where the given timestamp is back in time. This is when
76  // the timestamp provided is less than our cached_timestamp. We only expect to format timestamps
77  // that are incrementing not those back in time. In this case we just fall back to calling strfime
78  if (timestamp < _cached_timestamp)
79  {
80  _fallback_formatted = _safe_strftime(_timestamp_format.data(), timestamp, _time_zone).data();
81  return _fallback_formatted;
82  }
83 
84  // After this point we know that given timestamp is >= to the cache timestamp.
85 
86  // We check if the given timestamp greater than the _next_recalculation_timestamp to recalculate
87  if (timestamp >= _next_recalculation_timestamp)
88  {
89  // in this case we have to populate our cached string again using strftime
90  _pre_formatted_ts.clear();
91  _cached_indexes.clear();
92 
93  // Now populate a pre-formatted string for the next rec
94  _populate_pre_formatted_string_and_cached_indexes(timestamp);
95 
96  if (_time_zone == Timezone::LocalTime)
97  {
98  // If localtime is used, we will recalculate every 15 minutes. This approach accounts for
99  // DST changes and simplifies handling transitions around midnight. Recalculating every 15
100  // minutes ensures coverage for all possible timezones without additional computations.
101  _next_recalculation_timestamp = _next_quarter_hour_timestamp(timestamp);
102  }
103  else if (_time_zone == Timezone::GmtTime)
104  {
105  // otherwise we will only recalculate every noon and midnight. the reason for this is in
106  // case user is using PM, AM format etc
107  _next_recalculation_timestamp = _next_noon_or_midnight_timestamp(timestamp);
108  }
109  }
110 
111  if (_cached_indexes.empty())
112  {
113  // if we don't have to format any hours minutes or seconds we can just return here
114  return _pre_formatted_ts;
115  }
116 
117  if (_cached_timestamp == timestamp)
118  {
119  // This has 2 usages:
120  // 1. Any timestamps in seconds precision that are the same, we don't need to do anything.
121  // 2. This checks happens after the _next_recalculation_timestamp calculation. The _next_recalculation_timestamp
122  // will mutate _cached_timestamp and if they are similar we will just return here anyway
123  return _pre_formatted_ts;
124  }
125 
126  // Get the difference from our cached timestamp
127  time_t const timestamp_diff = timestamp - _cached_timestamp;
128 
129  // cache this timestamp
130  _cached_timestamp = timestamp;
131 
132  // Add the timestamp_diff to the cached seconds and calculate the new hours minutes seconds.
133  // Note: cached_seconds could be in gmtime or localtime but we don't care as we are just
134  // adding the difference.
135  _cached_seconds += static_cast<uint32_t>(timestamp_diff);
136 
137  uint32_t total_seconds = _cached_seconds;
138  uint32_t const hours = total_seconds / 3600;
139  total_seconds -= hours * 3600;
140  uint32_t const minutes = total_seconds / 60;
141  total_seconds -= minutes * 60;
142  uint32_t const seconds = total_seconds;
143 
144  for (auto const& index : _cached_indexes)
145  {
146  // for each cached index we have, replace it when the new value
147  switch (index.second)
148  {
149  case format_type::H:
150  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", hours);
151  break;
152  case format_type::M:
153  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", minutes);
154  break;
155  case format_type::S:
156  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", seconds);
157  break;
158  case format_type::I:
159  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}",
160  (hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours)));
161  break;
162  case format_type::l:
163  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:2}",
164  (hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours)));
165  break;
166  case format_type::k:
167  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:2}", hours);
168  break;
169  case format_type::s:
170  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:10}", _cached_timestamp);
171  break;
172  default:
173  abort();
174  }
175  }
176 
177  return _pre_formatted_ts;
178  }
179 
180 protected:
181  enum class format_type : uint8_t
182  {
183  H,
184  M,
185  S,
186  I,
187  k,
188  l,
189  s
190  };
191 
192  /***/
193  QUILL_ATTRIBUTE_COLD void _populate_initial_parts(std::string timestamp_format)
194  {
195  do
196  {
197  // we get part1 and part2 and keep looping on the new modified string without the part1 and
198  // part2 until we find not %H, %M or %S at all
199  auto const [part1, part2] = _split_timestamp_format_once(timestamp_format);
200 
201  if (!part1.empty())
202  {
203  _initial_parts.push_back(part1);
204  }
205 
206  if (!part2.empty())
207  {
208  _initial_parts.push_back(part2);
209  }
210 
211  if (part1.empty() && part2.empty())
212  {
213  // if both part_1 and part_2 are empty it means we have no more
214  // format modifiers to add, we push back the remaining timestamp_format string
215  // and break
216  if (!timestamp_format.empty())
217  {
218  _initial_parts.push_back(timestamp_format);
219  }
220  break;
221  }
222  } while (true);
223  }
224 
225  /***/
226  void _populate_pre_formatted_string_and_cached_indexes(time_t timestamp)
227  {
228  _cached_timestamp = timestamp;
229 
230  tm time_info{};
231 
232  if (_time_zone == Timezone::LocalTime)
233  {
234  localtime_rs(reinterpret_cast<time_t const*>(std::addressof(_cached_timestamp)), std::addressof(time_info));
235  }
236  else if (_time_zone == Timezone::GmtTime)
237  {
238  gmtime_rs(reinterpret_cast<time_t const*>(std::addressof(_cached_timestamp)), std::addressof(time_info));
239  }
240 
241  // also cache the seconds
242  _cached_seconds =
243  static_cast<uint32_t>((time_info.tm_hour * 3600) + (time_info.tm_min * 60) + time_info.tm_sec);
244 
245  // Now run through all parts and call strftime
246  for (auto const& format_part : _initial_parts)
247  {
248  // We call strftime on each part of the timestamp to format it.
249  _pre_formatted_ts += _safe_strftime(format_part.data(), _cached_timestamp, _time_zone).data();
250 
251  // If we formatted and appended to the string a time modifier also store the
252  // current index in the string
253  if (format_part == "%H")
254  {
255  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::H);
256  }
257  else if (format_part == "%M")
258  {
259  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::M);
260  }
261  else if (format_part == "%S")
262  {
263  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::S);
264  }
265  else if (format_part == "%I")
266  {
267  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::I);
268  }
269  else if (format_part == "%k")
270  {
271  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::k);
272  }
273  else if (format_part == "%l")
274  {
275  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::l);
276  }
277  else if (format_part == "%s")
278  {
279  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 10, format_type::s);
280  }
281  }
282  }
283 
284  /***/
285  std::pair<std::string, std::string> static _split_timestamp_format_once(std::string& timestamp_format) noexcept
286  {
287  // don't make this static as it breaks on windows with atexit when backend worker stops
288  std::array<std::string, 7> const modifiers{"%H", "%M", "%S", "%I", "%k", "%l", "%s"};
289 
290  // if we find any modifier in the timestamp format we store the index where we
291  // found it.
292  // We use a map to find the first modifier (with the lowest index) in the given string
293  // Maps found_index -> modifier value
294  std::map<size_t, std::string> found_format_modifiers;
295 
296  for (auto const& modifier : modifiers)
297  {
298  if (auto const search = timestamp_format.find(modifier); search != std::string::npos)
299  {
300  // Add the index and the modifier string to our map
301  found_format_modifiers.emplace(search, modifier);
302  }
303  }
304 
305  if (found_format_modifiers.empty())
306  {
307  // We didn't find any modifiers in the given string, therefore we return
308  // both parts as empty
309  return std::make_pair(std::string{}, std::string{});
310  }
311 
312  // we will split the formatted timestamp on the first modifier we found
313 
314  // Part 1 is the part before the modifier
315  // Here we check that there is actually a part of before and the format doesn't start with the
316  // modifier, otherwise we use an empty string
317  std::string const part_1 = found_format_modifiers.begin()->first > 0
318  ? std::string{timestamp_format.data(), found_format_modifiers.begin()->first}
319  : "";
320 
321  // The actual value of the modifier string
322  std::string const part_2 = found_format_modifiers.begin()->second;
323 
324  // We modify the given timestamp string to exclude part_1 and part_2.
325  // part_2 length as the modifier value will always be 2
326  timestamp_format = std::string{timestamp_format.data() + found_format_modifiers.begin()->first + 2};
327 
328  return std::make_pair(part_1, part_2);
329  }
330 
331  /***/
332  QUILL_NODISCARD static std::vector<char> _safe_strftime(char const* format_string, time_t timestamp, Timezone timezone)
333  {
334  if (format_string[0] == '\0')
335  {
336  std::vector<char> res;
337  res.push_back('\0');
338  return res;
339  }
340 
341  // Convert timestamp to time_info
342  tm time_info;
343  if (timezone == Timezone::LocalTime)
344  {
345  localtime_rs(reinterpret_cast<time_t const*>(std::addressof(timestamp)), std::addressof(time_info));
346  }
347  else if (timezone == Timezone::GmtTime)
348  {
349  gmtime_rs(reinterpret_cast<time_t const*>(std::addressof(timestamp)), std::addressof(time_info));
350  }
351 
352  // Create a buffer to call strftimex
353  std::vector<char> buffer;
354  buffer.resize(32);
355  size_t res = strftime(&buffer[0], buffer.size(), format_string, std::addressof(time_info));
356 
357  while (res == 0)
358  {
359  // if strftime fails we will reserve more space
360  buffer.resize(buffer.size() * 2);
361  res = strftime(&buffer[0], buffer.size(), format_string, std::addressof(time_info));
362  }
363 
364  return buffer;
365  }
366 
367  /***/
368  static void _replace_all(std::string& str, std::string const& old_value, std::string const& new_value) noexcept
369  {
370  std::string::size_type pos = 0u;
371  while ((pos = str.find(old_value, pos)) != std::string::npos)
372  {
373  str.replace(pos, old_value.length(), new_value);
374  pos += new_value.length();
375  }
376  }
377 
378  /***/
379  QUILL_NODISCARD static time_t _nearest_quarter_hour_timestamp(time_t timestamp) noexcept
380  {
381  time_t const nearest_quarter_hour_ts = (timestamp / 900) * 900;
382  return nearest_quarter_hour_ts;
383  }
384 
385  /***/
386  QUILL_NODISCARD static time_t _next_quarter_hour_timestamp(time_t timestamp) noexcept
387  {
388  time_t const next_quarter_hour_ts = _nearest_quarter_hour_timestamp(timestamp) + 900;
389  return next_quarter_hour_ts;
390  }
391 
392  /***/
393  QUILL_NODISCARD static time_t _next_noon_or_midnight_timestamp(time_t timestamp) noexcept
394  {
395  // Get the current date and time now as time_info
396  tm time_info;
397  gmtime_rs(&timestamp, &time_info);
398 
399  if (time_info.tm_hour < 12)
400  {
401  // we are before noon, so calculate noon
402  time_info.tm_hour = 11;
403  time_info.tm_min = 59;
404  time_info.tm_sec = 59; // we add 1 second later
405  }
406  else
407  {
408  // we are after noon so we calculate midnight
409  time_info.tm_hour = 23;
410  time_info.tm_min = 59;
411  time_info.tm_sec = 59; // we add 1 second later
412  }
413 
414  // convert back to time since epoch
415  std::chrono::system_clock::time_point const next_midnight =
416  std::chrono::system_clock::from_time_t(detail::timegm(&time_info));
417 
418  // returns seconds since epoch of the next midnight.
419  return std::chrono::duration_cast<std::chrono::seconds>(next_midnight.time_since_epoch()).count() + 1;
420  }
421 
422 private:
425  std::vector<std::string> _initial_parts;
426 
428  std::vector<std::pair<size_t, format_type>> _cached_indexes;
429 
431  std::string _timestamp_format;
432 
434  std::string _pre_formatted_ts;
435 
437  std::string _fallback_formatted;
438 
440  time_t _next_recalculation_timestamp{0};
441 
443  time_t _cached_timestamp{0};
444 
446  uint32_t _cached_seconds{0};
447 
449  Timezone _time_zone{Timezone::GmtTime};
450 };
451 } // namespace detail
452 
453 QUILL_END_NAMESPACE
tm * localtime_rs(time_t const *timer, tm *buf)
Portable localtime_r or _s per operating system.
Definition: TimeUtilities.h:59
A class that converts a timestamp to a string based on the given format.
Definition: StringFromTime.h:49
Setups a signal handler to handle fatal signals.
Definition: BackendManager.h:24
tm * gmtime_rs(time_t const *timer, tm *buf)
Portable gmtime_r or _s per operating system.
Definition: TimeUtilities.h:31
time_t timegm(tm *tm)
inverses of gmtime
Definition: TimeUtilities.h:85
custom exception
Definition: QuillError.h:45
A contiguous memory buffer with an optional growing ability.
Definition: base.h:1751