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  // only update components that have changed
145  bool const hours_changed = (hours != _prev_hours);
146  bool const minutes_changed = (minutes != _prev_minutes);
147  bool const seconds_changed = (seconds != _prev_seconds);
148 
149  // Update cached values
150  _prev_hours = hours;
151  _prev_minutes = minutes;
152  _prev_seconds = seconds;
153 
154  for (auto const& index : _cached_indexes)
155  {
156  // for each cached index we have, replace it when the new value
157  switch (index.second)
158  {
159  case format_type::H:
160  if (hours_changed)
161  {
162  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", hours);
163  }
164  break;
165  case format_type::M:
166  if (minutes_changed)
167  {
168  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", minutes);
169  }
170  break;
171  case format_type::S:
172  if (seconds_changed)
173  {
174  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}", seconds);
175  }
176  break;
177  case format_type::I:
178  if (hours_changed)
179  {
180  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:02}",
181  (hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours)));
182  }
183  break;
184  case format_type::l:
185  if (hours_changed)
186  {
187  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:2}",
188  (hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours)));
189  }
190  break;
191  case format_type::k:
192  if (hours_changed)
193  {
194  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:2}", hours);
195  }
196  break;
197  case format_type::s:
198  // Timestamp in seconds always changes
199  fmtquill::format_to(&_pre_formatted_ts[index.first], "{:10}", _cached_timestamp);
200  break;
201  default:
202  abort();
203  }
204  }
205 
206  return _pre_formatted_ts;
207  }
208 
209 protected:
210  enum class format_type : uint8_t
211  {
212  H,
213  M,
214  S,
215  I,
216  k,
217  l,
218  s
219  };
220 
221  /***/
222  QUILL_ATTRIBUTE_COLD void _populate_initial_parts(std::string timestamp_format)
223  {
224  do
225  {
226  // we get part1 and part2 and keep looping on the new modified string without the part1 and
227  // part2 until we find not %H, %M or %S at all
228  auto const [part1, part2] = _split_timestamp_format_once(timestamp_format);
229 
230  if (!part1.empty())
231  {
232  _initial_parts.push_back(part1);
233  }
234 
235  if (!part2.empty())
236  {
237  _initial_parts.push_back(part2);
238  }
239 
240  if (part1.empty() && part2.empty())
241  {
242  // if both part_1 and part_2 are empty it means we have no more
243  // format modifiers to add, we push back the remaining timestamp_format string
244  // and break
245  if (!timestamp_format.empty())
246  {
247  _initial_parts.push_back(timestamp_format);
248  }
249  break;
250  }
251  } while (true);
252  }
253 
254  /***/
255  void _populate_pre_formatted_string_and_cached_indexes(time_t timestamp)
256  {
257  _cached_timestamp = timestamp;
258 
259  tm time_info{};
260 
261  if (_time_zone == Timezone::LocalTime)
262  {
263  localtime_rs(reinterpret_cast<time_t const*>(std::addressof(_cached_timestamp)), std::addressof(time_info));
264  }
265  else if (_time_zone == Timezone::GmtTime)
266  {
267  gmtime_rs(reinterpret_cast<time_t const*>(std::addressof(_cached_timestamp)), std::addressof(time_info));
268  }
269 
270  // also cache the seconds
271  _cached_seconds =
272  static_cast<uint32_t>((time_info.tm_hour * 3600) + (time_info.tm_min * 60) + time_info.tm_sec);
273 
274  // Initialize previous time component tracking
275  uint32_t total_seconds = _cached_seconds;
276  _prev_hours = total_seconds / 3600;
277  total_seconds -= _prev_hours * 3600;
278  _prev_minutes = total_seconds / 60;
279  _prev_seconds = total_seconds - _prev_minutes * 60;
280 
281  // Now run through all parts and call strftime
282  for (auto const& format_part : _initial_parts)
283  {
284  // We call strftime on each part of the timestamp to format it.
285  _pre_formatted_ts += _safe_strftime(format_part.data(), _cached_timestamp, _time_zone).data();
286 
287  // If we formatted and appended to the string a time modifier also store the
288  // current index in the string
289  if (format_part == "%H")
290  {
291  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::H);
292  }
293  else if (format_part == "%M")
294  {
295  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::M);
296  }
297  else if (format_part == "%S")
298  {
299  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::S);
300  }
301  else if (format_part == "%I")
302  {
303  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::I);
304  }
305  else if (format_part == "%k")
306  {
307  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::k);
308  }
309  else if (format_part == "%l")
310  {
311  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 2, format_type::l);
312  }
313  else if (format_part == "%s")
314  {
315  _cached_indexes.emplace_back(_pre_formatted_ts.size() - 10, format_type::s);
316  }
317  }
318  }
319 
320  /***/
321  std::pair<std::string, std::string> static _split_timestamp_format_once(std::string& timestamp_format) noexcept
322  {
323  // don't make this static as it breaks on windows with atexit when backend worker stops
324  std::array<std::string, 7> const modifiers{"%H", "%M", "%S", "%I", "%k", "%l", "%s"};
325 
326  // if we find any modifier in the timestamp format we store the index where we
327  // found it.
328  // We use a map to find the first modifier (with the lowest index) in the given string
329  // Maps found_index -> modifier value
330  std::map<size_t, std::string> found_format_modifiers;
331 
332  for (auto const& modifier : modifiers)
333  {
334  if (auto const search = timestamp_format.find(modifier); search != std::string::npos)
335  {
336  // Add the index and the modifier string to our map
337  found_format_modifiers.emplace(search, modifier);
338  }
339  }
340 
341  if (found_format_modifiers.empty())
342  {
343  // We didn't find any modifiers in the given string, therefore we return
344  // both parts as empty
345  return std::make_pair(std::string{}, std::string{});
346  }
347 
348  // we will split the formatted timestamp on the first modifier we found
349 
350  // Part 1 is the part before the modifier
351  // Here we check that there is actually a part of before and the format doesn't start with the
352  // modifier, otherwise we use an empty string
353  std::string const part_1 = found_format_modifiers.begin()->first > 0
354  ? std::string{timestamp_format.data(), found_format_modifiers.begin()->first}
355  : "";
356 
357  // The actual value of the modifier string
358  std::string const part_2 = found_format_modifiers.begin()->second;
359 
360  // We modify the given timestamp string to exclude part_1 and part_2.
361  // part_2 length as the modifier value will always be 2
362  timestamp_format = std::string{timestamp_format.data() + found_format_modifiers.begin()->first + 2};
363 
364  return std::make_pair(part_1, part_2);
365  }
366 
367  /***/
368  QUILL_NODISCARD static std::vector<char> _safe_strftime(char const* format_string, time_t timestamp, Timezone timezone)
369  {
370  if (format_string[0] == '\0')
371  {
372  std::vector<char> res;
373  res.push_back('\0');
374  return res;
375  }
376 
377  // Convert timestamp to time_info
378  tm time_info;
379  if (timezone == Timezone::LocalTime)
380  {
381  localtime_rs(reinterpret_cast<time_t const*>(std::addressof(timestamp)), std::addressof(time_info));
382  }
383  else if (timezone == Timezone::GmtTime)
384  {
385  gmtime_rs(reinterpret_cast<time_t const*>(std::addressof(timestamp)), std::addressof(time_info));
386  }
387 
388  // Create a buffer to call strftimex
389  std::vector<char> buffer;
390  buffer.resize(32);
391  size_t res = strftime(&buffer[0], buffer.size(), format_string, std::addressof(time_info));
392 
393  while (res == 0)
394  {
395  // if strftime fails we will reserve more space
396  buffer.resize(buffer.size() * 2);
397  res = strftime(&buffer[0], buffer.size(), format_string, std::addressof(time_info));
398  }
399 
400  return buffer;
401  }
402 
403  /***/
404  static void _replace_all(std::string& str, std::string const& old_value, std::string const& new_value) noexcept
405  {
406  std::string::size_type pos = 0u;
407  while ((pos = str.find(old_value, pos)) != std::string::npos)
408  {
409  str.replace(pos, old_value.length(), new_value);
410  pos += new_value.length();
411  }
412  }
413 
414  /***/
415  QUILL_NODISCARD static time_t _nearest_quarter_hour_timestamp(time_t timestamp) noexcept
416  {
417  time_t const nearest_quarter_hour_ts = (timestamp / 900) * 900;
418  return nearest_quarter_hour_ts;
419  }
420 
421  /***/
422  QUILL_NODISCARD static time_t _next_quarter_hour_timestamp(time_t timestamp) noexcept
423  {
424  time_t const next_quarter_hour_ts = _nearest_quarter_hour_timestamp(timestamp) + 900;
425  return next_quarter_hour_ts;
426  }
427 
428  /***/
429  QUILL_NODISCARD static time_t _next_noon_or_midnight_timestamp(time_t timestamp) noexcept
430  {
431  // Get the current date and time now as time_info
432  tm time_info;
433  gmtime_rs(&timestamp, &time_info);
434 
435  if (time_info.tm_hour < 12)
436  {
437  // we are before noon, so calculate noon
438  time_info.tm_hour = 11;
439  time_info.tm_min = 59;
440  time_info.tm_sec = 59; // we add 1 second later
441  }
442  else
443  {
444  // we are after noon so we calculate midnight
445  time_info.tm_hour = 23;
446  time_info.tm_min = 59;
447  time_info.tm_sec = 59; // we add 1 second later
448  }
449 
450  // convert back to time since epoch
451  std::chrono::system_clock::time_point const next_midnight =
452  std::chrono::system_clock::from_time_t(detail::timegm(&time_info));
453 
454  // returns seconds since epoch of the next midnight.
455  return std::chrono::duration_cast<std::chrono::seconds>(next_midnight.time_since_epoch()).count() + 1;
456  }
457 
458 private:
461  std::vector<std::string> _initial_parts;
462 
464  std::vector<std::pair<size_t, format_type>> _cached_indexes;
465 
467  std::string _timestamp_format;
468 
470  std::string _pre_formatted_ts;
471 
473  std::string _fallback_formatted;
474 
476  time_t _next_recalculation_timestamp{0};
477 
479  time_t _cached_timestamp{0};
480 
482  uint32_t _cached_seconds{0};
483 
485  uint32_t _prev_hours{0};
486  uint32_t _prev_minutes{0};
487  uint32_t _prev_seconds{0};
488 
490  Timezone _time_zone{Timezone::GmtTime};
491 };
492 } // namespace detail
493 
494 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:1777