A library for working with phylogenetic and population genetic data.
v0.32.0
date_time.cpp
Go to the documentation of this file.
1 /*
2  Genesis - A toolkit for working with phylogenetic data.
3  Copyright (C) 2014-2023 Lucas Czech
4 
5  This program is free software: you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation, either version 3 of the License, or
8  (at your option) any later version.
9 
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  GNU General Public License for more details.
14 
15  You should have received a copy of the GNU General Public License
16  along with this program. If not, see <http://www.gnu.org/licenses/>.
17 
18  Contact:
19  Lucas Czech <lczech@carnegiescience.edu>
20  Department of Plant Biology, Carnegie Institution For Science
21  260 Panama Street, Stanford, CA 94305, USA
22 */
23 
32 
35 
36 #include <array>
37 #include <cerrno>
38 #include <cstdio>
39 #include <cstdlib>
40 #include <ctime>
41 #include <iomanip>
42 #include <iostream>
43 #include <locale>
44 #include <mutex>
45 #include <sstream>
46 #include <stdexcept>
47 
48 namespace genesis {
49 namespace utils {
50 
51 // =================================================================================================
52 // Thread Safety
53 // =================================================================================================
54 
62 static std::mutex tm_mutex_;
63 
64 // =================================================================================================
65 // Convenience Functions
66 // =================================================================================================
67 
68 std::string current_date()
69 {
70  // std::localtime is not threadsafe.
71  std::lock_guard<std::mutex> const tm_lock( tm_mutex_ );
72 
73  std::time_t now = std::time( nullptr );
74  std::tm* ltm = std::localtime( &now );
75 
76  if( !ltm ) {
77  throw std::runtime_error( "Cannot get current date." );
78  }
79 
80  char out[12];
81  std::snprintf(
82  out, 12, "%u-%02u-%02u",
83  ltm->tm_year + 1900, ltm->tm_mon + 1, ltm->tm_mday
84  );
85  return out;
86 }
87 
88 std::string current_time()
89 {
90  // std::localtime is not threadsafe.
91  std::lock_guard<std::mutex> const tm_lock( tm_mutex_ );
92 
93  std::time_t now = std::time( nullptr );
94  std::tm* ltm = std::localtime( &now );
95 
96  if( !ltm ) {
97  throw std::runtime_error( "Cannot get current time." );
98  }
99 
100  char out[10];
101  std::snprintf(
102  out, 10, "%02u:%02u:%02u",
103  ltm->tm_hour, ltm->tm_min, ltm->tm_sec
104  );
105  return out;
106 }
107 
108 std::time_t tm_to_time( std::tm time, bool use_local_time )
109 {
110  // We take the @p time object by value, as we need a non-const copy anyway to call the function:
111  // std::mktime changes and fixes values in the time object.
112 
113  std::time_t ret;
114  if( use_local_time ) {
115  ret = std::mktime( &time );
116  } else {
117 
118  // Set the time zone to UTC if needed, and store the previous value.
119  // Unfortunately, some of the functions are not in the std namespace... Ugly C++ standard!
120  char* tz;
121  tz = ::getenv("TZ");
122  ::setenv("TZ", "", 1);
123  ::tzset();
124 
125  // Convert.
126  ret = std::mktime( &time );
127 
128  // Reverse the time zone if needed.
129  if( tz ) {
130  ::setenv("TZ", tz, 1);
131  } else {
132  ::unsetenv("TZ");
133  }
134  ::tzset();
135  }
136  if( ret == -1 ) {
137  throw std::invalid_argument( "Cannot convert std::tm object to std::time." );
138  }
139  return ret;
140 }
141 
142 std::vector<std::time_t> tm_to_time( std::vector<std::tm> const& times, bool use_local_time )
143 {
144  return tm_to_time( times.begin(), times.end(), use_local_time, times.size() );
145 }
146 
147 std::tm time_to_tm( std::time_t const& time, bool use_local_time )
148 {
149  // std::localtime and std::gmtime are not threadsafe.
150  std::lock_guard<std::mutex> const tm_lock( tm_mutex_ );
151 
152  std::tm* ret;
153  if( use_local_time ) {
154  ret = std::localtime( &time );
155  } else {
156  ret = std::gmtime( &time );
157  }
158  if( ret == nullptr ) {
159  if( errno == EOVERFLOW ) {
160  throw std::invalid_argument(
161  "Cannot convert std::time object to std::tm, because the argument is too large."
162  );
163  } else {
164  throw std::invalid_argument( "Cannot convert std::time object to std::tm." );
165  }
166  }
167  return *ret;
168 }
169 
170 std::vector<std::tm> time_to_tm( std::vector<std::time_t> const& times, bool use_local_time )
171 {
172  return time_to_tm( times.begin(), times.end(), use_local_time, times.size() );
173 }
174 
175 // =================================================================================================
176 // Date/Time Conversion from std::tm
177 // =================================================================================================
178 
179 std::string tm_to_string( std::tm const& time, std::string const& format, std::string const& locale )
180 {
181  // gcc claims that version 4.8.1 was feature-complete for C+=11, see
182  // https://gcc.gnu.org/projects/cxx-status.html#cxx11. That fact that std::put_time and
183  // std::get_time are only available starting with gcc 5 determines that this is not true.
184  // Hence, to also support gcc < 5, we have to work around this limitation.
185 
186  // Prepare a locale and a stream
187  auto const loc = std::locale( locale.c_str() );
188  std::ostringstream oss{};
189  oss.imbue( loc );
190 
191  // Explicitly create a time facet that we can use to put the time to the stream.
192  std::time_put<char> const& tmput = std::use_facet<std::time_put<char>>( loc );
193  tmput.put( oss, oss, ' ', &time, &format[0], &format[0] + format.size() );
194  return oss.str();
195 
196  // Simple version that does not work with gcc < 5
197  // std::ostringstream oss{};
198  // oss.imbue(std::locale( locale.c_str() ));
199  // oss << std::put_time( &time, format.c_str() );
200  // return oss.str();
201 }
202 
203 std::string tm_to_string( std::tm const& time, std::string const& format )
204 {
205  return tm_to_string( time, format, "C" );
206 }
207 
208 std::string tm_to_string( std::tm const& time )
209 {
210  return tm_to_string( time, "%Y-%m-%dT%H:%M:%S", "C" );
211 }
212 
213 std::string tm_date_to_string( std::tm const& time )
214 {
215  return tm_to_string( time, "%Y-%m-%d", "C" );
216 }
217 
218 std::string tm_time_to_string( std::tm const& time )
219 {
220  return tm_to_string( time, "%H:%M:%S", "C" );
221 }
222 
223 // =================================================================================================
224 // Date/Time Conversion to std::tm
225 // =================================================================================================
226 
227 // Typical locales that we expect to see in scientific data.
228 static const std::array<std::string, 3> locales_ = {{ "C", "en_US.UTF-8", "" }};
229 
230 // Typical formats that we expect to see in scientific data.
231 static const std::array<std::string, 9> formats_ = {{
232  "%Y-%m-%d", "%Y%m%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y%m%dT%H%M%S",
233  "%Y%m%d %H%M%S", "%Y%m%d%H%M%S", "%H:%M:%S", "%H%M%S"
234 }};
235 
243  std::string const& str, std::string const& format, std::string const& locale, std::tm& t
244 ) {
245  // gcc claims that version 4.8.1 was feature-complete for C+=11, see
246  // https://gcc.gnu.org/projects/cxx-status.html#cxx11. That fact that std::put_time and
247  // std::get_time are only available starting with gcc 5 determines that this is not true.
248  // Hence, to also support gcc < 5, we have to work around this limitation.
249  // Still, it does not work with gcc 4.8, unfortunately.
250 
251  #if !( defined(__GNUC__) && (__GNUC__ < 5) && !defined(__clang__) && !defined(__INTEL_COMPILER) )
252 
253  // Init the tm object to all zeros, see https://en.cppreference.com/w/cpp/io/manip/get_time
254  // We are re-using the object when called from looping functions, so this is necessary.
255  t.tm_sec = 0;
256  t.tm_min = 0;
257  t.tm_hour = 0;
258  t.tm_mday = 0;
259  t.tm_mon = 0;
260  t.tm_year = 0;
261  t.tm_wday = 0;
262  t.tm_yday = 0;
263  t.tm_isdst = 0;
264 
265  // Prepare a locale and a stream
266  auto loc = std::locale( locale.c_str() );
267  std::istringstream iss( trim( str ));
268  iss.imbue( loc );
269 
270  // Explicitly create a time facet and other helper objects
271  // that we can use to get the time from the stream.
272  std::time_get<char> const& tmget = std::use_facet<std::time_get<char>>( loc );
273  std::ios::iostate state = std::ios_base::goodbit;
274 
275  // Run the conversion, using all provided information.
276  tmget.get(
277  iss, std::time_get<char>::iter_type(), iss, state, &t, &format[0], &format[0] + format.size()
278  );
279 
280  // Return whether that worked or failed, and whether we also reached the end of the stream.
281  // If we did not reach EOF, there is more data in the stream, which means, we only partially
282  // matched the string, so that it is not an actual fit.
283  // We do this by a hard comparison against the eof bit mask. We cannot use the iss.eof()
284  // function here, because the iss state is not set by our code above.
285  return ! iss.fail() && state == std::ios_base::eofbit;
286 
287  #else
288 
289  (void) str;
290  (void) format;
291  (void) locale;
292  (void) t;
293 
294  throw std::runtime_error(
295  "You compiled with " + info_compiler_family() + " " + info_compiler_version() +
296  ", which does not support time conversion functions std::get_time and std::time_get::get. " +
297  "Please upgrade to a newer compiler."
298  );
299 
300  #endif
301 
302  // Simple version that does not work with gcc < 5
303  // // Run the conversion, using all provided information.
304  // std::istringstream iss( trim( str ));
305  // iss.imbue( std::locale( locale.c_str() ));
306  // iss >> std::get_time( &t, format.c_str() );
307  //
308  // // Return whether that worked or failed, and whether we also reached the end of the stream.
309  // // If we did not reach EOF, there is more data in the stream, which means, we only partially
310  // // matched the string, so that it is not an actual fit.
311  // return ! iss.fail() && iss.eof();
312 }
313 
314 std::tm convert_to_tm( std::string const& str, std::string const& format, std::string const& locale )
315 {
316  std::tm t;
317  if( !convert_to_tm_( str, format, locale, t )) {
318  throw std::invalid_argument(
319  "Cannot convert string '" + str + "' to tm date/time object."
320  );
321  }
322  return t;
323 }
324 
325 std::tm convert_to_tm( std::string const& str, std::string const& format )
326 {
327  // See https://stackoverflow.com/a/73143713 for a potential subtle bug
328  // std::tm t;
329  struct std::tm t{};
330  for( auto const& locale : locales_ ) {
331  if( convert_to_tm_( str, format, locale, t )) {
332  return t;
333  }
334  }
335 
336  throw std::invalid_argument(
337  "Cannot convert string '" + str + "' to tm date/time object with given format."
338  );
339 }
340 
341 std::tm convert_to_tm( std::string const& str )
342 {
343  // Same as above, see https://stackoverflow.com/a/73143713
344  // std::tm t;
345  struct std::tm t{};
346 
347  // The formats that we try here are not dependent on the locale. If we introduce additional
348  // formats in the future, it might be necessary to also loop over those, for example the
349  // user-preferred locale, to make sure that local date/time formats can be parsed.
350  for( auto const& format : formats_ ) {
351  if( convert_to_tm_( str, format, "C", t )) {
352  return t;
353  }
354  }
355 
356  throw std::invalid_argument(
357  "Cannot convert string '" + str + "' to tm date/time object with guessed formats."
358  );
359 }
360 
361 bool is_convertible_to_tm( std::string const& str, std::string const& format, std::string const& locale )
362 {
363  // std::tm t;
364  struct std::tm t{};
365  return convert_to_tm_( str, format, locale, t );
366 }
367 
368 bool is_convertible_to_tm( std::string const& str, std::string const& format )
369 {
370  // If one of the locales works, the string is convertible.
371  // std::tm t;
372  struct std::tm t{};
373  for( auto const& locale : locales_ ) {
374  if( convert_to_tm_( str, format, locale, t )) {
375  return true;
376  }
377  }
378  return false;
379 }
380 
381 bool is_convertible_to_tm( std::string const& str )
382 {
383  // If one of the formats works, the string is convertible.
384  // std::tm t;
385  struct std::tm t{};
386  for( auto const& format : formats_ ) {
387  if( convert_to_tm_( str, format, "C", t )) {
388  return true;
389  }
390  }
391  return false;
392 }
393 
394 } // namespace utils
395 } // namespace genesis
genesis::utils::convert_to_tm
std::tm convert_to_tm(std::string const &str, std::string const &format, std::string const &locale)
Convert a std::string to a std::tm date/time object, if possible. Throw otherwise.
Definition: date_time.cpp:314
genesis::utils::current_date
std::string current_date()
Returns the current date as a string in the format "2014-12-31".
Definition: date_time.cpp:68
genesis::utils::convert_to_tm_
bool convert_to_tm_(std::string const &str, std::string const &format, std::string const &locale, std::tm &t)
Local helper function that does the heavy load of time conversion.
Definition: date_time.cpp:242
date_time.hpp
Provides functions for date and time access.
genesis::utils::trim
std::string trim(std::string const &s, std::string const &delimiters)
Return a copy of the input string, with trimmed white spaces (or any other delimiters).
Definition: string.cpp:827
genesis::utils::current_time
std::string current_time()
Returns the current time as a string in the format "13:37:42".
Definition: date_time.cpp:88
genesis::utils::tm_date_to_string
std::string tm_date_to_string(std::tm const &time)
Print the given std::tm object as a std::string containing only the date, using the ISO 8601 extended...
Definition: date_time.cpp:213
genesis::utils::locales_
static const std::array< std::string, 3 > locales_
Definition: date_time.cpp:228
string.hpp
Provides some commonly used string utility functions.
genesis::utils::time_to_tm
std::tm time_to_tm(std::time_t const &time, bool use_local_time)
Convert std::time_t object to a std::tm object.
Definition: date_time.cpp:147
info.hpp
genesis
Container namespace for all symbols of genesis in order to keep them separate when used as a library.
Definition: placement/formats/edge_color.cpp:42
genesis::utils::tm_mutex_
static std::mutex tm_mutex_
The std::localtime and std::gmtime functions are not thread safe, due to their shared internal state....
Definition: date_time.cpp:62
genesis::utils::tm_to_time
std::time_t tm_to_time(std::tm time, bool use_local_time)
Convert std::tm object to a std::time_t object.
Definition: date_time.cpp:108
genesis::utils::tm_time_to_string
std::string tm_time_to_string(std::tm const &time)
Print the given std::tm object as a std::string containing only the time, using the ISO 8601 extended...
Definition: date_time.cpp:218
genesis::utils::formats_
static const std::array< std::string, 9 > formats_
Definition: date_time.cpp:231
genesis::utils::tm_to_string
std::string tm_to_string(std::tm const &time, std::string const &format, std::string const &locale)
Print the given std::tm object as a std::string, using the format and locale.
Definition: date_time.cpp:179
genesis::utils::is_convertible_to_tm
bool is_convertible_to_tm(std::string const &str, std::string const &format, std::string const &locale)
Return whether a std::string is convertible to a std::tm date/time object, that is,...
Definition: date_time.cpp:361