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 (_find_unescaped_modifier(_timestamp_format, "%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_unescaped_modifier(_timestamp_format, "%r", "%I:%M:%S %p");
65  _replace_unescaped_modifier(_timestamp_format, "%R", "%H:%M");
66  _replace_unescaped_modifier(_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  if (QUILL_UNLIKELY(_cached_epoch_seconds_width != 0))
133  {
134  size_t const epoch_seconds_width = _decimal_width(_cached_timestamp);
135  if (epoch_seconds_width != _cached_epoch_seconds_width)
136  {
137  _next_recalculation_timestamp = 0;
138  return format_timestamp(timestamp);
139  }
140  }
141 
142  // Add the timestamp_diff to the cached seconds and calculate the new hours minutes seconds.
143  // Note: cached_seconds could be in gmtime or localtime but we don't care as we are just
144  // adding the difference.
145  _cached_seconds += static_cast<uint32_t>(timestamp_diff);
146 
147  if (QUILL_UNLIKELY(_cached_seconds >= 86400))
148  {
149  // This is purely defensive
150  // - GMT mode: recalc boundaries align exactly with midnight/noon, so _cached_seconds can never exceed 86399 in the incremental path
151  // - LocalTime mode: triggerable on machines in non-UTC-aligned timezones, or during DST transitions
152 
153  // Force a full recalculation
154  _next_recalculation_timestamp = 0;
155  return format_timestamp(timestamp);
156  }
157 
158  uint32_t total_seconds = _cached_seconds;
159  uint32_t const hours = total_seconds / 3600;
160  total_seconds -= hours * 3600;
161  uint32_t const minutes = total_seconds / 60;
162  total_seconds -= minutes * 60;
163  uint32_t const seconds = total_seconds;
164 
165  // only update components that have changed
166  bool const hours_changed = (hours != _prev_hours);
167  bool const minutes_changed = (minutes != _prev_minutes);
168  bool const seconds_changed = (seconds != _prev_seconds);
169 
170  // Update cached values
171  _prev_hours = hours;
172  _prev_minutes = minutes;
173  _prev_seconds = seconds;
174 
175  for (auto const& index : _cached_indexes)
176  {
177  // for each cached index we have, replace it when the new value
178  switch (index.second)
179  {
180  case format_type::H:
181  if (hours_changed)
182  {
183  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", hours);
184  }
185  break;
186  case format_type::M:
187  if (minutes_changed)
188  {
189  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", minutes);
190  }
191  break;
192  case format_type::S:
193  if (seconds_changed)
194  {
195  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", seconds);
196  }
197  break;
198  case format_type::I:
199  if (hours_changed)
200  {
201  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}",
202  (hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours)));
203  }
204  break;
205  case format_type::l:
206  if (hours_changed)
207  {
208  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:2}",
209  (hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours)));
210  }
211  break;
212  case format_type::k:
213  if (hours_changed)
214  {
215  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:2}", hours);
216  }
217  break;
218  case format_type::s:
219  fmtquill::format_to(&_pre_formatted_ts[index.first], "{}", _cached_timestamp);
220  break;
221  default:
222  abort();
223  }
224  }
225 
226  return _pre_formatted_ts;
227  }
228 
229  /***/
230  QUILL_NODISCARD static size_t _find_unescaped_modifier(std::string const& timestamp_format,
231  std::string const& modifier) noexcept
232  {
233  size_t search_pos = 0;
234  while ((search_pos = timestamp_format.find(modifier, search_pos)) != std::string::npos)
235  {
236  if (!_is_escaped_percent(timestamp_format, search_pos))
237  {
238  return search_pos;
239  }
240 
241  ++search_pos;
242  }
243 
244  return std::string::npos;
245  }
246 
247  /***/
248  QUILL_NODISCARD static bool _is_escaped_percent(std::string const& timestamp_format, size_t percent_pos) noexcept
249  {
250  size_t preceding_percent_count = 0;
251  while ((percent_pos > preceding_percent_count) &&
252  (timestamp_format[percent_pos - preceding_percent_count - 1] == '%'))
253  {
254  ++preceding_percent_count;
255  }
256 
257  return (preceding_percent_count % 2u) != 0u;
258  }
259 
260 protected:
261  enum class format_type : uint8_t
262  {
263  H,
264  M,
265  S,
266  I,
267  k,
268  l,
269  s
270  };
271 
272  /***/
273  QUILL_ATTRIBUTE_COLD void _populate_initial_parts(std::string timestamp_format)
274  {
275  do
276  {
277  // we get part1 and part2 and keep looping on the new modified string without the part1 and
278  // part2 until we find not %H, %M or %S at all
279  auto const [part1, part2] = _split_timestamp_format_once(timestamp_format);
280 
281  if (!part1.empty())
282  {
283  _initial_parts.push_back(part1);
284  }
285 
286  if (!part2.empty())
287  {
288  _initial_parts.push_back(part2);
289  }
290 
291  if (part1.empty() && part2.empty())
292  {
293  // if both part_1 and part_2 are empty it means we have no more
294  // format modifiers to add, we push back the remaining timestamp_format string
295  // and break
296  if (!timestamp_format.empty())
297  {
298  _initial_parts.push_back(timestamp_format);
299  }
300  break;
301  }
302  } while (true);
303  }
304 
305  /***/
306  void _populate_pre_formatted_string_and_cached_indexes(time_t timestamp)
307  {
308  _cached_timestamp = timestamp;
309 
310  tm time_info{};
311 
312  if (_time_zone == Timezone::LocalTime)
313  {
314  localtime_rs(reinterpret_cast<time_t const*>(std::addressof(_cached_timestamp)), std::addressof(time_info));
315  }
316  else if (_time_zone == Timezone::GmtTime)
317  {
318  gmtime_rs(reinterpret_cast<time_t const*>(std::addressof(_cached_timestamp)), std::addressof(time_info));
319  }
320 
321  // also cache the seconds
322  _cached_seconds =
323  static_cast<uint32_t>((time_info.tm_hour * 3600) + (time_info.tm_min * 60) + time_info.tm_sec);
324 
325  // Initialize previous time component tracking
326  uint32_t total_seconds = _cached_seconds;
327  _prev_hours = total_seconds / 3600;
328  total_seconds -= _prev_hours * 3600;
329  _prev_minutes = total_seconds / 60;
330  _prev_seconds = total_seconds - _prev_minutes * 60;
331 
332  // Now run through all parts and call strftime
333  for (auto const& format_part : _initial_parts)
334  {
335  // We call strftime on each part of the timestamp to format it.
336  std::vector<char> const formatted_part = _safe_strftime(format_part.data(), _cached_timestamp, _time_zone);
337  std::string_view const formatted_part_sv{formatted_part.data()};
338  size_t const formatted_part_size = formatted_part_sv.size();
339  _pre_formatted_ts.append(formatted_part_sv);
340 
341  // If we formatted and appended to the string a time modifier also store the
342  // current index in the string
343  if (format_part == "%H")
344  {
345  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::H);
346  }
347  else if (format_part == "%M")
348  {
349  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::M);
350  }
351  else if (format_part == "%S")
352  {
353  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::S);
354  }
355  else if (format_part == "%I")
356  {
357  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::I);
358  }
359  else if (format_part == "%k")
360  {
361  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::k);
362  }
363  else if (format_part == "%l")
364  {
365  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::l);
366  }
367  else if (format_part == "%s")
368  {
369  _cached_indexes.emplace_back(_pre_formatted_ts.size() - formatted_part_size, format_type::s);
370  _cached_epoch_seconds_width = formatted_part_size;
371  }
372  }
373  }
374 
375  /***/
376  std::pair<std::string, std::string> static _split_timestamp_format_once(std::string& timestamp_format)
377  {
378  // don't make this static as it breaks on windows with atexit when backend worker stops
379  std::array<std::string, 7> const modifiers{"%H", "%M", "%S", "%I", "%k", "%l", "%s"};
380 
381  // if we find any modifier in the timestamp format we store the index where we
382  // found it.
383  // We use a map to find the first modifier (with the lowest index) in the given string
384  // Maps found_index -> modifier value
385  std::map<size_t, std::string> found_format_modifiers;
386 
387  for (auto const& modifier : modifiers)
388  {
389  if (auto const search = _find_unescaped_modifier(timestamp_format, modifier); search != std::string::npos)
390  {
391  // Add the index and the modifier string to our map
392  found_format_modifiers.emplace(search, modifier);
393  }
394  }
395 
396  if (found_format_modifiers.empty())
397  {
398  // We didn't find any modifiers in the given string, therefore we return
399  // both parts as empty
400  return std::make_pair(std::string{}, std::string{});
401  }
402 
403  // we will split the formatted timestamp on the first modifier we found
404 
405  // Part 1 is the part before the modifier
406  // Here we check that there is actually a part of before and the format doesn't start with the
407  // modifier, otherwise we use an empty string
408  std::string const part_1 = found_format_modifiers.begin()->first > 0
409  ? std::string{timestamp_format.data(), found_format_modifiers.begin()->first}
410  : "";
411 
412  // The actual value of the modifier string
413  std::string const part_2 = found_format_modifiers.begin()->second;
414 
415  // We modify the given timestamp string to exclude part_1 and part_2.
416  // part_2 length as the modifier value will always be 2
417  timestamp_format = std::string{timestamp_format.data() + found_format_modifiers.begin()->first + 2};
418 
419  return std::make_pair(part_1, part_2);
420  }
421 
422  /***/
423  QUILL_NODISCARD static std::vector<char> _safe_strftime(char const* format_string, time_t timestamp, Timezone timezone)
424  {
425  if (format_string[0] == '\0')
426  {
427  std::vector<char> res;
428  res.push_back('\0');
429  return res;
430  }
431 
432  // Convert timestamp to time_info
433  tm time_info;
434  if (timezone == Timezone::LocalTime)
435  {
436  localtime_rs(reinterpret_cast<time_t const*>(std::addressof(timestamp)), std::addressof(time_info));
437  }
438  else if (timezone == Timezone::GmtTime)
439  {
440  gmtime_rs(reinterpret_cast<time_t const*>(std::addressof(timestamp)), std::addressof(time_info));
441  }
442 
443  // Create a buffer to call strftime
444  static constexpr size_t max_buffer_size{64 * 1024};
445  std::vector<char> buffer;
446  buffer.resize(32);
447  size_t res = strftime(&buffer[0], buffer.size(), format_string, std::addressof(time_info));
448 
449  while (res == 0)
450  {
451  if (QUILL_UNLIKELY(buffer.size() >= max_buffer_size))
452  {
453  QUILL_THROW(
454  QuillError{"strftime failed to format timestamp. The timestamp pattern may "
455  "contain an unsupported format specifier."});
456  }
457 
458  // if strftime fails we will reserve more space
459  buffer.resize(buffer.size() * 2);
460  res = strftime(&buffer[0], buffer.size(), format_string, std::addressof(time_info));
461  }
462 
463  return buffer;
464  }
465 
466  /***/
467  static void _replace_unescaped_modifier(std::string& str, std::string const& old_value, std::string const& new_value)
468  {
469  std::string::size_type pos = 0u;
470  while ((pos = str.find(old_value, pos)) != std::string::npos)
471  {
472  if (_is_escaped_percent(str, pos))
473  {
474  ++pos;
475  continue;
476  }
477 
478  str.replace(pos, old_value.length(), new_value);
479  pos += new_value.length();
480  }
481  }
482 
483  /***/
484  QUILL_NODISCARD static time_t _nearest_quarter_hour_timestamp(time_t timestamp) noexcept
485  {
486  time_t const nearest_quarter_hour_ts = (timestamp / 900) * 900;
487  return nearest_quarter_hour_ts;
488  }
489 
490  /***/
491  QUILL_NODISCARD static time_t _next_quarter_hour_timestamp(time_t timestamp) noexcept
492  {
493  time_t const next_quarter_hour_ts = _nearest_quarter_hour_timestamp(timestamp) + 900;
494  return next_quarter_hour_ts;
495  }
496 
497  /***/
498  QUILL_NODISCARD static time_t _next_noon_or_midnight_timestamp(time_t timestamp) noexcept
499  {
500  // Get the current date and time now as time_info
501  tm time_info;
502  gmtime_rs(&timestamp, &time_info);
503 
504  if (time_info.tm_hour < 12)
505  {
506  // we are before noon, so calculate noon
507  time_info.tm_hour = 11;
508  time_info.tm_min = 59;
509  time_info.tm_sec = 59; // we add 1 second later
510  }
511  else
512  {
513  // we are after noon so we calculate midnight
514  time_info.tm_hour = 23;
515  time_info.tm_min = 59;
516  time_info.tm_sec = 59; // we add 1 second later
517  }
518 
519  // convert back to time since epoch
520  std::chrono::system_clock::time_point const next_midnight =
521  std::chrono::system_clock::from_time_t(detail::timegm(&time_info));
522 
523  // returns seconds since epoch of the next midnight.
524  return std::chrono::duration_cast<std::chrono::seconds>(next_midnight.time_since_epoch()).count() + 1;
525  }
526 
527  /***/
528  QUILL_NODISCARD static size_t _decimal_width(time_t timestamp) noexcept
529  {
530  size_t width = 0;
531 
532  using unsigned_time_t = std::make_unsigned_t<time_t>;
533  unsigned_time_t magnitude = 0;
534 
535  if constexpr (std::is_signed_v<time_t>)
536  {
537  if (timestamp < 0)
538  {
539  width = 1; // '-'
540  magnitude = static_cast<unsigned_time_t>(-(timestamp + 1)) + 1;
541  }
542  else
543  {
544  magnitude = static_cast<unsigned_time_t>(timestamp);
545  }
546  }
547  else
548  {
549  magnitude = timestamp;
550  }
551 
552  do
553  {
554  ++width;
555  magnitude /= 10;
556  } while (magnitude != 0);
557 
558  return width;
559  }
560 
561 private:
564  std::vector<std::string> _initial_parts;
565 
567  std::vector<std::pair<size_t, format_type>> _cached_indexes;
568 
570  std::string _timestamp_format;
571 
573  std::string _pre_formatted_ts;
574 
576  std::string _fallback_formatted;
577 
579  time_t _next_recalculation_timestamp{0};
580 
582  time_t _cached_timestamp{0};
583 
585  uint32_t _cached_seconds{0};
586 
588  uint32_t _prev_hours{0};
589  uint32_t _prev_minutes{0};
590  uint32_t _prev_seconds{0};
591 
593  Timezone _time_zone{Timezone::GmtTime};
594  size_t _cached_epoch_seconds_width{0};
595 };
596 } // namespace detail
597 
598 QUILL_END_NAMESPACE
tm * localtime_rs(time_t const *timer, tm *buf)
Portable localtime_r or _s per operating system.
Definition: TimeUtilities.h:56
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:28
tm * gmtime_rs(time_t const *timer, tm *buf)
Portable gmtime_r or _s per operating system.
Definition: TimeUtilities.h:28
time_t timegm(tm *tm)
inverses of gmtime
Definition: TimeUtilities.h:82
custom exception
Definition: QuillError.h:47
A contiguous memory buffer with an optional growing ability.
Definition: base.h:1779