Line data Source code
1 : /********************************************************************\
2 : * gnc-datetime.cpp -- Date and Time classes for GnuCash *
3 : * *
4 : * Copyright 2015 John Ralls <jralls@ceridwen.us> *
5 : * *
6 : * This program is free software; you can redistribute it and/or *
7 : * modify it under the terms of the GNU General Public License as *
8 : * published by the Free Software Foundation; either version 2 of *
9 : * the License, or (at your option) any later version. *
10 : * *
11 : * This program is distributed in the hope that it will be useful, *
12 : * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 : * GNU General Public License for more details. *
15 : * *
16 : * You should have received a copy of the GNU General Public License*
17 : * along with this program; if not, contact: *
18 : * *
19 : * Free Software Foundation Voice: +1-617-542-5942 *
20 : * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
21 : * Boston, MA 02110-1301, USA gnu@gnu.org *
22 : * *
23 : \********************************************************************/
24 :
25 : #include <config.h>
26 : #include "platform.h"
27 : #include <boost/date_time/gregorian/gregorian.hpp>
28 : #include <boost/date_time/posix_time/posix_time.hpp>
29 : #include <boost/date_time/local_time/local_time.hpp>
30 : #include <boost/locale.hpp>
31 : #include <boost/regex.hpp>
32 : #include <stdexcept>
33 : #include <unicode/smpdtfmt.h>
34 : #include <unicode/locid.h>
35 : #include <unicode/udat.h>
36 : #include <unicode/parsepos.h>
37 : #include <unicode/calendar.h>
38 : #include <libintl.h>
39 : #include <locale.h>
40 : #include<chrono>
41 : #include <map>
42 : #include <memory>
43 : #include <iostream>
44 : #include <sstream>
45 : #include <string>
46 : #include <vector>
47 : #include <optional>
48 : #include <charconv>
49 : #ifdef __MINGW32__
50 : #include <codecvt>
51 : #endif
52 : #include <gnc-locale-utils.hpp>
53 : #include "gnc-timezone.hpp"
54 : #include "gnc-datetime.hpp"
55 :
56 : #define N_(string) string //So that xgettext will find it
57 :
58 : using PTZ = boost::local_time::posix_time_zone;
59 : using Date = boost::gregorian::date;
60 : using Month = boost::gregorian::greg_month;
61 : using PTime = boost::posix_time::ptime;
62 : using LDT = boost::local_time::local_date_time;
63 : using Duration = boost::posix_time::time_duration;
64 : using LDTBase = boost::local_time::local_date_time_base<PTime, boost::date_time::time_zone_base<PTime, char>>;
65 : using boost::date_time::not_a_date_time;
66 : using time64 = int64_t;
67 :
68 : static const TimeZoneProvider ltzp;
69 : static const TimeZoneProvider* tzp = <zp;
70 :
71 : // For converting to/from POSIX time.
72 : static const PTime unix_epoch (Date(1970, boost::gregorian::Jan, 1),
73 : boost::posix_time::seconds(0));
74 : static const TZ_Ptr utc_zone(new boost::local_time::posix_time_zone("UTC-0"));
75 :
76 : /* Backdoor to enable unittests to temporarily override the timezone: */
77 : void _set_tzp(TimeZoneProvider& tz);
78 : void _reset_tzp();
79 :
80 : static Date gregorian_date_from_locale_string (const std::string& str);
81 :
82 : /* To ensure things aren't overly screwed up by setting the nanosecond clock for boost::date_time. Don't do it, though, it doesn't get us anything and slows down the date/time library. */
83 : #ifndef BOOST_DATE_TIME_HAS_NANOSECONDS
84 : static constexpr auto ticks_per_second = INT64_C(1000000);
85 : #else
86 : static constexpr auto ticks_per_second = INT64_C(1000000000);
87 : #endif
88 :
89 : /* Vector of date formats understood by gnucash and corresponding regex
90 : * and/or string->gregorian_date to parse each from an external source
91 : * Note: while the format names are using a "-" as separator, the
92 : * regexes will accept any of "-/.' " and will also work for dates
93 : * without separators.
94 : */
95 : const std::vector<GncDateFormat> GncDate::c_formats ({
96 : GncDateFormat {
97 : N_("y-m-d"),
98 : boost::gregorian::from_string,
99 : "(?:" // either y-m-d
100 : "(?<YEAR>[0-9]+)[-/.' ]+"
101 : "(?<MONTH>[0-9]+)[-/.' ]+"
102 : "(?<DAY>[0-9]+)"
103 : "|" // or CCYYMMDD
104 : "(?<YEAR>[0-9]{4})"
105 : "(?<MONTH>[0-9]{2})"
106 : "(?<DAY>[0-9]{2})"
107 : ")"
108 : },
109 : GncDateFormat {
110 : N_("d-m-y"),
111 : boost::gregorian::from_uk_string,
112 : "(?:" // either d-m-y
113 : "(?<DAY>[0-9]+)[-/.' ]+"
114 : "(?<MONTH>[0-9]+)[-/.' ]+"
115 : "(?<YEAR>[0-9]+)"
116 : "|" // or DDMMCCYY
117 : "(?<DAY>[0-9]{2})"
118 : "(?<MONTH>[0-9]{2})"
119 : "(?<YEAR>[0-9]{4})"
120 : ")"
121 : },
122 : GncDateFormat {
123 : N_("m-d-y"),
124 : boost::gregorian::from_us_string,
125 : "(?:" // either m-d-y
126 : "(?<MONTH>[0-9]+)[-/.' ]+"
127 : "(?<DAY>[0-9]+)[-/.' ]+"
128 : "(?<YEAR>[0-9]+)"
129 : "|" // or MMDDCCYY
130 : "(?<MONTH>[0-9]{2})"
131 : "(?<DAY>[0-9]{2})"
132 : "(?<YEAR>[0-9]{4})"
133 : ")"
134 : },
135 : // Note year is still checked for in the regexes below
136 : // This is to be able to raise an error if one is found for a yearless date format
137 : GncDateFormat {
138 : (N_("d-m")),
139 : "(?:" // either d-m(-y)
140 : "(?<DAY>[0-9]+)[-/.' ]+"
141 : "(?<MONTH>[0-9]+)(?:[-/.' ]+"
142 : "(?<YEAR>[0-9]+))?"
143 : "|" // or DDMM(CCYY)
144 : "(?<DAY>[0-9]{2})"
145 : "(?<MONTH>[0-9]{2})"
146 : "(?<YEAR>[0-9]+)?"
147 : ")"
148 : },
149 : GncDateFormat {
150 : (N_("m-d")),
151 : "(?:" // either m-d(-y)
152 : "(?<MONTH>[0-9]+)[-/.' ]+"
153 : "(?<DAY>[0-9]+)(?:[-/.' ]+"
154 : "(?<YEAR>[0-9]+))?"
155 : "|" // or MMDD(CCYY)
156 : "(?<MONTH>[0-9]{2})"
157 : "(?<DAY>[0-9]{2})"
158 : "(?<YEAR>[0-9]+)?"
159 : ")"
160 : },
161 : GncDateFormat { N_("Locale"), gregorian_date_from_locale_string },
162 : });
163 :
164 : /** Private implementation of GncDateTime. See the documentation for that class.
165 : */
166 : static LDT
167 61246 : LDT_from_unix_local(const time64 time)
168 : {
169 : try
170 : {
171 : PTime temp(unix_epoch.date(),
172 61246 : boost::posix_time::hours(time / 3600) +
173 122492 : boost::posix_time::seconds(time % 3600));
174 61246 : auto tz = tzp->get(temp.date().year());
175 122488 : return LDT(temp, tz);
176 61244 : }
177 2 : catch(boost::gregorian::bad_year&)
178 : {
179 2 : throw(std::invalid_argument("Time value is outside the supported year range."));
180 2 : }
181 0 : catch(std::out_of_range&)
182 : {
183 0 : throw(std::invalid_argument("Time value is outside the supported year range."));
184 0 : }
185 : }
186 : /* If a date-time falls in a DST transition the LDT constructor will
187 : * fail because either the date-time doesn't exist (when starting DST
188 : * because the transition skips an hour) or is ambiguous (when ending
189 : * because the transition hour is repeated). We try again an hour
190 : * later to be outside the DST transition. When starting DST that's
191 : * now the correct time but at the end of DST we need to set the
192 : * returned time back an hour.
193 : */
194 : static LDT
195 1 : LDT_with_pushup(const Date& tdate, const Duration& tdur, const TZ_Ptr tz,
196 : bool putback)
197 : {
198 1 : static const boost::posix_time::hours pushup{1};
199 1 : LDT ldt{tdate, tdur + pushup, tz, LDTBase::NOT_DATE_TIME_ON_ERROR};
200 1 : if (ldt.is_special())
201 : {
202 0 : std::string error{"Couldn't create a valid datetime at "};
203 0 : error += to_simple_string(tdate) + " ";
204 0 : error += to_simple_string(tdur) + " TZ ";
205 0 : error += tz->std_zone_abbrev();
206 0 : throw(std::invalid_argument{error});
207 0 : }
208 1 : if (putback)
209 1 : ldt -= pushup;
210 1 : return ldt;
211 0 : }
212 :
213 : static LDT
214 17316 : LDT_from_date_time(const Date& tdate, const Duration& tdur, const TZ_Ptr tz)
215 : {
216 :
217 : try
218 : {
219 17317 : LDT ldt(tdate, tdur, tz, LDTBase::EXCEPTION_ON_ERROR);
220 17315 : return ldt;
221 0 : }
222 1 : catch (const boost::local_time::time_label_invalid& err)
223 : {
224 0 : return LDT_with_pushup(tdate, tdur, tz, false);
225 0 : }
226 :
227 1 : catch (const boost::local_time::ambiguous_result& err)
228 : {
229 1 : return LDT_with_pushup(tdate, tdur, tz, true);
230 1 : }
231 :
232 0 : catch(boost::gregorian::bad_year&)
233 : {
234 0 : throw(std::invalid_argument("Time value is outside the supported year range."));
235 0 : }
236 0 : catch(std::out_of_range&)
237 : {
238 0 : throw(std::invalid_argument("Time value is outside the supported year range."));
239 0 : }
240 :
241 : }
242 :
243 : static LDT
244 6110 : LDT_from_date_daypart(const Date& date, DayPart part, const TZ_Ptr tz)
245 : {
246 : using hours = boost::posix_time::hours;
247 :
248 : static const Duration day_begin{0, 0, 0};
249 : static const Duration day_neutral{10, 59, 0};
250 : static const Duration day_end{23, 59, 59};
251 :
252 :
253 6110 : switch (part)
254 : {
255 2872 : case DayPart::start:
256 2872 : return LDT_from_date_time(date, day_begin, tz);
257 880 : case DayPart::end:
258 880 : return LDT_from_date_time(date, day_end, tz);
259 2358 : default: // To stop gcc from emitting a control reaches end of non-void function.
260 : case DayPart::neutral:
261 2358 : PTime pt{date, day_neutral};
262 4716 : LDT lt{pt, tz};
263 2358 : auto offset = lt.local_time() - lt.utc_time();
264 2358 : if (offset < hours(-10))
265 0 : lt -= hours(offset.hours() + 10);
266 2358 : if (offset > hours(13))
267 0 : lt += hours(13 - offset.hours());
268 2358 : return lt;
269 : }
270 : }
271 :
272 : static LDT
273 10692 : LDT_from_struct_tm(const struct tm tm)
274 : {
275 : try
276 : {
277 10692 : Date tdate{boost::gregorian::date_from_tm(tm)};
278 10692 : Duration tdur{boost::posix_time::time_duration(tm.tm_hour, tm.tm_min,
279 10692 : tm.tm_sec, 0)};
280 10692 : TZ_Ptr tz{tzp->get(tdate.year())};
281 21384 : return LDT_from_date_time(tdate, tdur, tz);
282 10692 : }
283 0 : catch(const boost::gregorian::bad_year&)
284 : {
285 0 : throw(std::invalid_argument{"Time value is outside the supported year range."});
286 0 : }
287 0 : catch(std::out_of_range&)
288 : {
289 0 : throw(std::invalid_argument("Time value is outside the supported year range."));
290 0 : }
291 : }
292 :
293 : void
294 0 : _set_tzp(TimeZoneProvider& new_tzp)
295 : {
296 0 : tzp = &new_tzp;
297 0 : }
298 :
299 : void
300 0 : _reset_tzp()
301 : {
302 0 : tzp = <zp;
303 0 : }
304 :
305 : class GncDateTimeImpl
306 : {
307 : public:
308 : /* Boost::date_time's localtime function relies on the C library's, so it may
309 : * suffer from the 2038 failure. std::chrono is supposed to be immune to that.
310 : */
311 10151 : GncDateTimeImpl() : m_time(LDT_from_unix_local(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count())) {}
312 51095 : GncDateTimeImpl(const time64 time) : m_time(LDT_from_unix_local(time)) {}
313 10692 : GncDateTimeImpl(const struct tm tm) : m_time(LDT_from_struct_tm(tm)) {}
314 : GncDateTimeImpl(const GncDateImpl& date, DayPart part = DayPart::neutral);
315 124 : GncDateTimeImpl(const std::string& str) : GncDateTimeImpl (str.c_str()) {};
316 : GncDateTimeImpl(const char* str);
317 : GncDateTimeImpl(PTime&& pt) : m_time(pt, tzp->get(pt.date().year())) {}
318 : GncDateTimeImpl(LDT&& ldt) : m_time(ldt) {}
319 :
320 : operator time64() const;
321 : operator struct tm() const;
322 0 : void now() { m_time = boost::local_time::local_sec_clock::local_time(tzp->get(boost::gregorian::day_clock::local_day().year())); }
323 : long offset() const;
324 15 : struct tm utc_tm() const { return to_tm(m_time.utc_time()); }
325 : std::unique_ptr<GncDateImpl> date() const;
326 : std::string format(const char* format) const;
327 : std::string format_zulu(const char* format) const;
328 : std::string format_iso8601() const;
329 : static std::string timestamp();
330 : private:
331 : LDT m_time;
332 : };
333 :
334 : /** Private implementation of GncDate. See the documentation for that class.
335 : */
336 : class GncDateImpl
337 : {
338 : public:
339 288 : GncDateImpl(): m_greg(boost::gregorian::day_clock::local_day()) {}
340 6198 : GncDateImpl(const int year, const int month, const int day) :
341 6198 : m_greg(year, static_cast<Month>(month), day) {}
342 207 : GncDateImpl(Date d) : m_greg(d) {}
343 : GncDateImpl(const std::string str, const std::string fmt);
344 :
345 0 : void today() { m_greg = boost::gregorian::day_clock::local_day(); }
346 : gnc_ymd year_month_day() const;
347 : std::string format(const char* format) const;
348 : std::string format_zulu(const char* format) const {
349 : return this->format(format);
350 : }
351 : private:
352 : Date m_greg;
353 :
354 : friend GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl&, DayPart);
355 : friend bool operator<(const GncDateImpl&, const GncDateImpl&);
356 : friend bool operator>(const GncDateImpl&, const GncDateImpl&);
357 : friend bool operator==(const GncDateImpl&, const GncDateImpl&);
358 : friend bool operator<=(const GncDateImpl&, const GncDateImpl&);
359 : friend bool operator>=(const GncDateImpl&, const GncDateImpl&);
360 : friend bool operator!=(const GncDateImpl&, const GncDateImpl&);
361 : };
362 :
363 : /* Needs to be separately defined so that the friend decl can grant
364 : * access to date.m_greg.
365 : */
366 6110 : GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl& date, DayPart part) :
367 6110 : m_time{LDT_from_date_daypart(date.m_greg, part,
368 12220 : tzp->get(date.m_greg.year()))} {}
369 :
370 : /* Member function definitions for GncDateTimeImpl.
371 : */
372 :
373 : static bool
374 15972 : parse_chars_into_num (const char* ptr, const char *end_ptr, int32_t& rv) noexcept
375 : {
376 15972 : auto result = std::from_chars (ptr, end_ptr, rv);
377 15972 : return (result.ec == std::errc() && result.ptr == end_ptr);
378 : }
379 :
380 : static std::optional<PTime>
381 2872 : fast_iso8601_utc_parse (const char* str)
382 : {
383 : int32_t year, month, mday, hour, min, sec;
384 2872 : const size_t len = str ? strnlen (str, 26) : 0;
385 :
386 : // parse iso-8601 utc format "YYYY-MM-DD HH:MM:SS +0000"
387 2872 : constexpr size_t expanded_iso_string_len = 25;
388 2661 : if (len == expanded_iso_string_len &&
389 7983 : parse_chars_into_num (str, str + 4, year) && str[ 4] == '-' &&
390 7983 : parse_chars_into_num (str + 5, str + 7, month) && str[ 7] == '-' &&
391 7983 : parse_chars_into_num (str + 8, str + 10, mday) && str[10] == ' ' &&
392 7983 : parse_chars_into_num (str + 11, str + 13, hour) && str[13] == ':' &&
393 7983 : parse_chars_into_num (str + 14, str + 16, min) && str[16] == ':' &&
394 10855 : parse_chars_into_num (str + 17, str + 19, sec) && str[19] == ' ' &&
395 2661 : !strcmp (str + 20, "+0000"))
396 : {
397 1562 : return PTime (boost::gregorian::date (year, month, mday),
398 2343 : boost::posix_time::time_duration (hour, min, sec));
399 : }
400 :
401 : // parse compressed iso-8601 format "YYYYMMDDHHMMSS"
402 2091 : constexpr size_t compact_iso_string_len = 14;
403 1 : if (len == compact_iso_string_len &&
404 2 : parse_chars_into_num (str, str + 4, year) &&
405 2 : parse_chars_into_num (str + 4, str + 6, month) &&
406 2 : parse_chars_into_num (str + 6, str + 8, mday) &&
407 2 : parse_chars_into_num (str + 8, str + 10, hour) &&
408 2093 : parse_chars_into_num (str + 10, str + 12, min) &&
409 1 : parse_chars_into_num (str + 12, str + 14, sec))
410 : {
411 2 : return PTime (boost::gregorian::date (year, month, mday),
412 3 : boost::posix_time::time_duration (hour, min, sec));
413 : }
414 :
415 2090 : return {};
416 : }
417 :
418 : static TZ_Ptr
419 2090 : tz_from_string(std::string str)
420 : {
421 2090 : if (str.empty()) return utc_zone;
422 1888 : std::string tzstr = "XXX" + str;
423 1888 : if (tzstr.length() > 6 && tzstr[6] != ':') //6 for XXXsHH, s is + or -
424 1880 : tzstr.insert(6, ":");
425 1888 : if (tzstr.length() > 9 && tzstr[9] != ':') //9 for XXXsHH:MM
426 : {
427 0 : tzstr.insert(9, ":");
428 : }
429 1888 : return TZ_Ptr(new PTZ(tzstr));
430 1888 : }
431 :
432 2873 : GncDateTimeImpl::GncDateTimeImpl(const char* str) :
433 2873 : m_time(unix_epoch, utc_zone)
434 : {
435 3655 : if (!str || !str[0]) return;
436 2872 : TZ_Ptr tzptr;
437 : try
438 : {
439 2872 : if (auto res = fast_iso8601_utc_parse (str))
440 : {
441 782 : m_time = LDT_from_date_time(res->date(), res->time_of_day(), utc_zone);
442 782 : return;
443 : }
444 2090 : static const boost::regex delim_iso("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d{0,9})?)\\s*([+-]\\d{2}(?::?\\d{2})?)?$");
445 2090 : static const boost::regex non_delim("^(\\d{14}(?:\\.\\d{0,9})?)\\s*([+-]\\d{2}\\s*(:?\\d{2})?)?$");
446 2090 : PTime pdt;
447 2090 : boost::cmatch sm;
448 2090 : if (regex_match(str, sm, non_delim))
449 : {
450 0 : std::string time_str(sm[1]);
451 0 : time_str.insert(8, "T");
452 0 : pdt = boost::posix_time::from_iso_string(time_str);
453 0 : }
454 2090 : else if (regex_match(str, sm, delim_iso))
455 : {
456 2090 : pdt = boost::posix_time::time_from_string(sm[1]);
457 : }
458 : else
459 : {
460 0 : throw(std::invalid_argument("The date string was not formatted in a way that GncDateTime(const char*) knows how to parse."));
461 : }
462 2090 : std::string tzstr("");
463 2090 : if (sm[2].matched)
464 1888 : tzstr += sm[2];
465 2090 : tzptr = tz_from_string(tzstr);
466 2090 : m_time = LDT_from_date_time(pdt.date(), pdt.time_of_day(), tzptr);
467 2090 : }
468 0 : catch(boost::gregorian::bad_year&)
469 : {
470 0 : throw(std::invalid_argument("The date string was outside of the supported year range."));
471 0 : }
472 0 : catch(std::out_of_range&)
473 : {
474 0 : throw(std::invalid_argument("The date string was outside of the supported year range."));
475 0 : }
476 : /* Bug 767824: A GLib bug in parsing the UTC timezone on Windows may have
477 : * created a bogus timezone of a random number of minutes. Since there are
478 : * no fractional-hour timezones around the prime meridian we can safely
479 : * check for this in files by resetting to UTC if there's a
480 : * less-than-an-hour offset.
481 : */
482 2090 : auto offset = tzptr->base_utc_offset().seconds();
483 2090 : if (offset != 0 && std::abs(offset) < 3600)
484 0 : m_time = m_time.local_time_in(utc_zone);
485 2872 : }
486 :
487 29776 : GncDateTimeImpl::operator time64() const
488 : {
489 29776 : auto duration = m_time.utc_time() - unix_epoch;
490 29776 : auto secs = duration.ticks();
491 29776 : secs /= ticks_per_second;
492 29776 : return secs;
493 : }
494 :
495 37302 : GncDateTimeImpl::operator struct tm() const
496 : {
497 37302 : struct tm time = to_tm(m_time);
498 : #if HAVE_STRUCT_TM_GMTOFF
499 37302 : time.tm_gmtoff = offset();
500 : #endif
501 37302 : return time;
502 : }
503 :
504 : long
505 37338 : GncDateTimeImpl::offset() const
506 : {
507 37338 : auto offset = m_time.local_time() - m_time.utc_time();
508 74676 : return offset.total_seconds();
509 : }
510 :
511 : std::unique_ptr<GncDateImpl>
512 207 : GncDateTimeImpl::date() const
513 : {
514 414 : return std::unique_ptr<GncDateImpl>(new GncDateImpl(m_time.local_time().date()));
515 : }
516 :
517 : /* The 'O', 'E', and '-' format modifiers are not supported by
518 : * boost's output facets. Remove them.
519 : */
520 : static inline std::string
521 3409 : normalize_format (const std::string& format)
522 : {
523 3409 : bool is_pct = false;
524 3409 : std::string normalized;
525 3409 : std::remove_copy_if(
526 : format.begin(), format.end(), back_inserter(normalized),
527 24842 : [&is_pct](char e){
528 24842 : bool r = (is_pct && (e == 'E' || e == 'O' || e == '-'));
529 24842 : is_pct = e == '%';
530 24842 : return r;
531 : });
532 3409 : return normalized;
533 0 : }
534 : #ifdef __MINGW32__
535 : constexpr size_t DATEBUFLEN = 100;
536 : static std::string
537 : win_date_format(std::string format, struct tm tm)
538 : {
539 : #pragma GCC diagnostic push
540 : #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
541 : wchar_t buf[DATEBUFLEN];
542 : memset(buf, 0, DATEBUFLEN * sizeof(wchar_t));
543 : std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> conv;
544 : [[maybe_unused]] auto numchars = wcsftime(buf, DATEBUFLEN - 1, conv.from_bytes(format).c_str(), &tm);
545 : return conv.to_bytes(buf);
546 : #pragma GCC diagnostic pop
547 : }
548 :
549 : /* Microsoft's strftime uses the time zone flags differently from
550 : * boost::date_time so we need to handle any before passing the
551 : * format string to strftime.
552 : */
553 : inline std::string
554 : win_format_tz_abbrev (std::string format, TZ_Ptr tz, bool is_dst)
555 : {
556 : size_t pos = format.find("%z");
557 : if (pos != std::string::npos)
558 : {
559 : auto tzabbr = tz->has_dst() && is_dst ? tz->dst_zone_abbrev() :
560 : tz->std_zone_abbrev();
561 : format.replace(pos, 2, tzabbr);
562 : }
563 : return format;
564 : }
565 :
566 : inline std::string
567 : win_format_tz_name (std::string format, TZ_Ptr tz, bool is_dst)
568 : {
569 : size_t pos = format.find("%Z");
570 : if (pos != std::string::npos)
571 : {
572 : auto tzname = tz->has_dst() && is_dst ? tz->dst_zone_name() :
573 : tz->std_zone_name();
574 : format.replace(pos, 2, tzname);
575 : }
576 : return format;
577 : }
578 :
579 : inline std::string
580 : win_format_tz_posix (std::string format, TZ_Ptr tz)
581 : {
582 : size_t pos = format.find("%ZP");
583 : if (pos != std::string::npos)
584 : format.replace(pos, 2, tz->to_posix_string());
585 : return format;
586 : }
587 :
588 : #endif
589 : std::string
590 3333 : GncDateTimeImpl::format(const char* format) const
591 : {
592 : #ifdef __MINGW32__
593 : auto tz = m_time.zone();
594 : auto tm = static_cast<struct tm>(*this);
595 : auto sformat = win_format_tz_abbrev(format, tz, tm.tm_isdst);
596 : sformat = win_format_tz_name(sformat, tz, tm.tm_isdst);
597 : sformat = win_format_tz_posix(sformat, tz);
598 : return win_date_format(sformat, tm);
599 : #else
600 : using Facet = boost::local_time::local_time_facet;
601 9999 : auto output_facet(new Facet(normalize_format(format).c_str()));
602 3333 : std::stringstream ss;
603 3333 : ss.imbue(std::locale(gnc_get_locale(), output_facet));
604 3333 : ss << m_time;
605 6666 : return ss.str();
606 : #endif
607 3333 : }
608 :
609 : std::string
610 0 : GncDateTimeImpl::format_zulu(const char* format) const
611 : {
612 : #ifdef __MINGW32__
613 : auto tz = utc_zone;
614 : auto tm = static_cast<struct tm>(*this);
615 : auto sformat = win_format_tz_abbrev(format, tz, tm.tm_isdst);
616 : sformat = win_format_tz_name(sformat, tz, tm.tm_isdst);
617 : sformat = win_format_tz_posix(sformat, tz);
618 : return win_date_format(sformat, utc_tm());
619 : #else
620 : using Facet = boost::local_time::local_time_facet;
621 0 : auto zulu_time = LDT{m_time.utc_time(), utc_zone};
622 0 : auto output_facet(new Facet(normalize_format(format).c_str()));
623 0 : std::stringstream ss;
624 0 : ss.imbue(std::locale(gnc_get_locale(), output_facet));
625 0 : ss << zulu_time;
626 0 : return ss.str();
627 : #endif
628 0 : }
629 :
630 : std::string
631 20777 : GncDateTimeImpl::format_iso8601() const
632 : {
633 20777 : auto str = boost::posix_time::to_iso_extended_string(m_time.utc_time());
634 20777 : str[10] = ' ';
635 41554 : return str.substr(0, 19);
636 20777 : }
637 :
638 : std::string
639 50 : GncDateTimeImpl::timestamp()
640 : {
641 50 : GncDateTimeImpl gdt;
642 50 : auto str = boost::posix_time::to_iso_string(gdt.m_time.local_time());
643 100 : return str.substr(0, 8) + str.substr(9, 15);
644 50 : }
645 :
646 : struct ICUResources
647 : {
648 : std::unique_ptr<icu::DateFormat> formatter;
649 : std::unique_ptr<icu::Calendar> calendar;
650 : };
651 :
652 : static ICUResources&
653 0 : get_icu_resources()
654 : {
655 0 : static ICUResources rv;
656 :
657 0 : if (!rv.formatter)
658 : {
659 0 : icu::Locale locale;
660 0 : if (auto lc_time_locale = setlocale (LC_TIME, nullptr))
661 : {
662 0 : std::string localeStr(lc_time_locale);
663 0 : if (size_t dotPos = localeStr.find('.'); dotPos != std::string::npos)
664 0 : localeStr = localeStr.substr(0, dotPos);
665 :
666 0 : locale = icu::Locale::createCanonical (localeStr.c_str());
667 0 : }
668 :
669 0 : rv.formatter.reset(icu::DateFormat::createDateInstance(icu::DateFormat::kDefault, locale));
670 0 : if (!rv.formatter)
671 0 : throw std::invalid_argument("Cannot create date formatter.");
672 :
673 0 : UErrorCode status = U_ZERO_ERROR;
674 0 : rv.calendar.reset(icu::Calendar::createInstance(locale, status));
675 0 : if (U_FAILURE(status))
676 0 : throw std::invalid_argument("Cannot create calendar instance.");
677 :
678 0 : rv.calendar->setLenient(false);
679 0 : }
680 :
681 0 : return rv;
682 : }
683 :
684 : static Date
685 0 : gregorian_date_from_locale_string (const std::string& str)
686 : {
687 0 : ICUResources& resources = get_icu_resources();
688 :
689 0 : icu::UnicodeString input = icu::UnicodeString::fromUTF8(str);
690 0 : icu::ParsePosition parsePos;
691 0 : UDate date = resources.formatter->parse(input, parsePos);
692 0 : if (parsePos.getErrorIndex() != -1 || parsePos.getIndex() != input.length())
693 0 : throw std::invalid_argument ("Cannot parse string");
694 :
695 0 : UErrorCode status = U_ZERO_ERROR;
696 0 : resources.calendar->setTime(date, status);
697 0 : if (U_FAILURE(status))
698 0 : throw std::invalid_argument ("Cannot set calendar time");
699 :
700 0 : return Date (resources.calendar->get(UCAL_YEAR, status),
701 0 : resources.calendar->get(UCAL_MONTH, status) + 1,
702 0 : resources.calendar->get(UCAL_DATE, status));
703 0 : }
704 :
705 : /* Member function definitions for GncDateImpl.
706 : */
707 5 : GncDateImpl::GncDateImpl(const std::string str, const std::string fmt) :
708 5 : m_greg(boost::gregorian::day_clock::local_day()) /* Temporarily initialized to today, will be used and adjusted in the code below */
709 : {
710 5 : auto iter = std::find_if(GncDate::c_formats.cbegin(), GncDate::c_formats.cend(),
711 15 : [&fmt](const GncDateFormat& v){ return (v.m_fmt == fmt); } );
712 10 : if (iter == GncDate::c_formats.cend())
713 0 : throw std::invalid_argument(N_("Unknown date format specifier passed as argument."));
714 :
715 5 : if (iter->m_str_to_date)
716 : {
717 : try
718 : {
719 5 : m_greg = (*iter->m_str_to_date)(str);
720 5 : return;
721 : }
722 0 : catch (...) {} // with any string->date exception, try regex
723 : }
724 :
725 0 : if (iter->m_re.empty())
726 0 : throw std::invalid_argument ("No regex pattern available");
727 :
728 0 : boost::smatch what;
729 0 : if(!boost::regex_search(str, what, iter->m_re)) // regex didn't find a match
730 0 : throw std::invalid_argument (N_("Value can't be parsed into a date using the selected date format."));
731 :
732 : // Bail out if a year was found with a yearless format specifier
733 0 : auto fmt_has_year = (fmt.find('y') != std::string::npos);
734 0 : if (!fmt_has_year && (what.length("YEAR") != 0))
735 0 : throw std::invalid_argument (N_("Value appears to contain a year while the selected format forbids this."));
736 :
737 : int year;
738 0 : if (fmt_has_year)
739 : {
740 : /* The input dates have a year, so use that one */
741 0 : year = std::stoi (what.str("YEAR"));
742 :
743 : /* We assume two-digit years to be in the range 1969 - 2068. */
744 0 : if (year < 69)
745 0 : year += 2000;
746 0 : else if (year < 100)
747 0 : year += 1900;
748 : }
749 : else /* The input dates have no year, so use current year */
750 0 : year = m_greg.year(); // Can use m_greg here as it was already initialized in the initializer list earlier
751 :
752 0 : m_greg = Date(year,
753 0 : static_cast<Month>(std::stoi (what.str("MONTH"))),
754 0 : std::stoi (what.str("DAY")));
755 0 : }
756 :
757 : gnc_ymd
758 495 : GncDateImpl::year_month_day() const
759 : {
760 495 : auto boost_ymd = m_greg.year_month_day();
761 990 : return {boost_ymd.year, boost_ymd.month.as_number(), boost_ymd.day};
762 : }
763 :
764 : std::string
765 76 : GncDateImpl::format(const char* format) const
766 : {
767 : #ifdef __MINGW32__
768 : return win_date_format(format, to_tm(m_greg));
769 : #else
770 : using Facet = boost::gregorian::date_facet;
771 76 : std::stringstream ss;
772 : //The stream destructor frees the facet, so it must be heap-allocated.
773 228 : auto output_facet(new Facet(normalize_format(format).c_str()));
774 76 : ss.imbue(std::locale(gnc_get_locale(), output_facet));
775 76 : ss << m_greg;
776 152 : return ss.str();
777 : #endif
778 76 : }
779 :
780 0 : bool operator<(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg < b.m_greg; }
781 0 : bool operator>(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg > b.m_greg; }
782 0 : bool operator==(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg == b.m_greg; }
783 0 : bool operator<=(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg <= b.m_greg; }
784 0 : bool operator>=(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg >= b.m_greg; }
785 0 : bool operator!=(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg != b.m_greg; }
786 :
787 : /* =================== Presentation-class Implementations ====================*/
788 : /* GncDateTime */
789 :
790 10101 : GncDateTime::GncDateTime() : m_impl(new GncDateTimeImpl) {}
791 51095 : GncDateTime::GncDateTime(const time64 time) :
792 51095 : m_impl(new GncDateTimeImpl(time)) {}
793 10692 : GncDateTime::GncDateTime(const struct tm tm) :
794 10692 : m_impl(new GncDateTimeImpl(tm)) {}
795 124 : GncDateTime::GncDateTime(const std::string& str) :
796 124 : m_impl(new GncDateTimeImpl(str)) {}
797 2749 : GncDateTime::GncDateTime(const char* str) :
798 2749 : m_impl(new GncDateTimeImpl(str)) {}
799 80869 : GncDateTime::~GncDateTime() = default;
800 :
801 6110 : GncDateTime::GncDateTime(const GncDate& date, DayPart part) :
802 6110 : m_impl(new GncDateTimeImpl(*(date.m_impl), part)) {}
803 :
804 : void
805 0 : GncDateTime::now()
806 : {
807 0 : m_impl->now();
808 0 : }
809 :
810 29776 : GncDateTime::operator time64() const
811 : {
812 29776 : return m_impl->operator time64();
813 : }
814 :
815 37302 : GncDateTime::operator struct tm() const
816 : {
817 37302 : return m_impl->operator struct tm();
818 : }
819 :
820 : long
821 36 : GncDateTime::offset() const
822 : {
823 36 : return m_impl->offset();
824 : }
825 :
826 : struct tm
827 15 : GncDateTime::utc_tm() const
828 : {
829 15 : return m_impl->utc_tm();
830 : }
831 :
832 : GncDate
833 207 : GncDateTime::date() const
834 : {
835 207 : return GncDate(m_impl->date());
836 : }
837 :
838 : std::string
839 3333 : GncDateTime::format(const char* format) const
840 : {
841 3333 : return m_impl->format(format);
842 : }
843 :
844 : std::string
845 0 : GncDateTime::format_zulu(const char* format) const
846 : {
847 0 : return m_impl->format_zulu(format);
848 : }
849 :
850 : std::string
851 20777 : GncDateTime::format_iso8601() const
852 : {
853 20777 : return m_impl->format_iso8601();
854 : }
855 :
856 : std::string
857 50 : GncDateTime::timestamp()
858 : {
859 50 : return GncDateTimeImpl::timestamp();
860 : }
861 :
862 : /* GncDate */
863 288 : GncDate::GncDate() : m_impl{new GncDateImpl} {}
864 6198 : GncDate::GncDate(int year, int month, int day) :
865 6198 : m_impl(new GncDateImpl(year, month, day)) {}
866 5 : GncDate::GncDate(const std::string str, const std::string fmt) :
867 5 : m_impl(new GncDateImpl(str, fmt)) {}
868 207 : GncDate::GncDate(std::unique_ptr<GncDateImpl> impl) :
869 414 : m_impl(std::move(impl)) {}
870 0 : GncDate::GncDate(const GncDate& a) :
871 0 : m_impl(new GncDateImpl(*a.m_impl)) {}
872 0 : GncDate::GncDate(GncDate&&) = default;
873 6681 : GncDate::~GncDate() = default;
874 :
875 : GncDate&
876 0 : GncDate::operator=(const GncDate& a)
877 : {
878 0 : m_impl.reset(new GncDateImpl(*a.m_impl));
879 0 : return *this;
880 : }
881 : GncDate&
882 0 : GncDate::operator=(GncDate&&) = default;
883 :
884 : void
885 0 : GncDate::today()
886 : {
887 0 : m_impl->today();
888 0 : }
889 :
890 : std::string
891 76 : GncDate::format(const char* format)
892 : {
893 76 : return m_impl->format(format);
894 : }
895 :
896 : gnc_ymd
897 495 : GncDate::year_month_day() const
898 : {
899 495 : return m_impl->year_month_day();
900 : }
901 :
902 0 : bool operator<(const GncDate& a, const GncDate& b) { return *(a.m_impl) < *(b.m_impl); }
903 0 : bool operator>(const GncDate& a, const GncDate& b) { return *(a.m_impl) > *(b.m_impl); }
904 0 : bool operator==(const GncDate& a, const GncDate& b) { return *(a.m_impl) == *(b.m_impl); }
905 0 : bool operator<=(const GncDate& a, const GncDate& b) { return *(a.m_impl) <= *(b.m_impl); }
906 0 : bool operator>=(const GncDate& a, const GncDate& b) { return *(a.m_impl) >= *(b.m_impl); }
907 0 : bool operator!=(const GncDate& a, const GncDate& b) { return *(a.m_impl) != *(b.m_impl); }
|